Authentication
Overview
The platform uses a two-step PKCE (Proof Key for Code Exchange, RFC 7636) authentication flow adapted for email-based OTP login. Rather than redirecting to a third-party identity provider, the server issues a one-time password to the user’s email address and validates it against a cryptographic code challenge, combining the security properties of PKCE with a passwordless sign-in experience.
Auth Flow
The complete authentication sequence proceeds as follows:
Client Server
| |
| 1. Generate codeVerifier (43-128 chars) |
| Hash with SHA-256 -> codeChallenge |
| (base64url encoded) |
| |
| 2. POST /login |
| { emailAddress, codeChallenge } |
|---------------------------------------------->|
| | 3. Validate input via Zod
| | Look up account (must be ACTIVE)
| | Generate OTP, store with codeChallenge
| | Queue OTP email via BullMQ
| |
| 4. Response: |
| { accessToken (15d), refreshToken (7d) } |
|<----------------------------------------------|
| |
| 5. POST /verify |
| { otp, codeVerifier } |
| Authorization: Bearer <accessToken> |
|---------------------------------------------->|
| | 6. Decode token for emailAddress
| | Find pending OTP record
| | Re-hash codeVerifier with SHA-256
| | Compare against stored codeChallenge
| |
| 7. If valid, OTP marked as verified |
|<----------------------------------------------|
Step-by-Step Breakdown
Client generates PKCE pair. The client creates a
codeVerifier, a random string between 43 and 128 characters drawn from the unreserved URI character set. It then hashes the verifier with SHA-256 and base64url-encodes the result to produce thecodeChallenge.Client sends login request. A
POST /loginrequest carries the user’semailAddressand thecodeChallenge.Server processes login. The server validates the request body with Zod, looks up the account (which must be in an
ACTIVEstate), generates a one-time password, stores it alongside thecodeChallenge, and enqueues an email delivery job through BullMQ.Server returns tokens. The response includes an
accessToken(default expiry 15 days) and arefreshToken(default expiry 7 days).Client sends verification request. After receiving the OTP via email, the client sends
POST /verifywith theotpand the originalcodeVerifierin the request body, along with theaccessTokenin theAuthorizationheader.Server verifies PKCE challenge. The server decodes the bearer token to extract the
emailAddress, locates the pending OTP record, re-hashes the suppliedcodeVerifierusing SHA-256, and compares the result to the storedcodeChallenge.OTP marked as verified. If the derived challenge matches the stored challenge, the OTP is marked as verified and the authentication is complete.
JWT Tokens
Token operations are implemented using the jsonwebtoken library, configured through environment variables.
Configuration
| Setting | Source | Default |
|---|---|---|
| Signing secret | JWT_SECRET env var |
– |
| Access token expiry | ACCESS_TOKEN_EXPIRES env var |
15d |
| Refresh token expiry | REFRESH_TOKEN_EXPIRES env var |
7d |
Token Functions
The token utility module exposes the following functions:
signToken(payload)– Signs a token with a short 5-minute expiry. Used for transient, operation-scoped tokens.generateAccessToken(payload)– Issues an access token using the configuredACCESS_TOKEN_EXPIRESduration.generateRefreshToken(payload)– Issues a refresh token using the configuredREFRESH_TOKEN_EXPIRESduration.verifyToken(token)– Verifies the signature and decodes the token payload. Throws on invalid or expired tokens.
Token Refresh Flow
When an access token nears expiry, the client can obtain a new token pair:
- The client sends
POST /tokenwith the current refresh token in theAuthorizationheader. - The server extracts the refresh token and looks up the corresponding stored record via
TokenModel. - If the stored token is valid, the server issues a new access token and refresh token pair.
Token Validation Middleware
The validateToken middleware protects routes that require authentication:
- Extracts the Bearer token from the
Authorizationheader. - Calls
verifyToken()to validate the signature and decode the payload. - On success, attaches the decoded payload to the request and passes control to the next handler.
- On failure, returns
401 Unauthorized(missing or malformed token) or403 Forbidden(expired or invalid token).
getDecodedToken Helper
The getDecodedToken(request) helper retrieves the decoded JWT payload that was attached to the request object by the validateToken middleware. This provides downstream handlers with convenient access to the authenticated user’s identity.
PKCE Implementation
The PKCE utilities are located in libs/auth/pkce.ts and provide the following functions:
generateCodeVerifier(length = 128)
Generates a cryptographically random string of the specified length (default 128 characters) using characters from the unreserved URI character set (A-Z, a-z, 0-9, -, ., _, ~).
generateCodeChallenge(verifier)
Accepts a code verifier string, computes its SHA-256 hash, and returns the result as a base64url-encoded string. This is the S256 challenge method defined in RFC 7636.
verifyCodeChallenge(verifier, challenge)
Re-derives the code challenge from the supplied verifier using SHA-256 and base64url encoding, then performs a constant-time comparison against the provided challenge string. Returns true if they match.
isValidCodeVerifier(verifier)
Validates that a code verifier conforms to the expected format using the regular expression:
/^[A-Za-z0-9\-._~]{43,128}$/
Returns true if the verifier is between 43 and 128 characters long and contains only unreserved URI characters.
Code Locations
| Component | Path |
|---|---|
| PKCE utilities | libs/auth/pkce.ts |
| Token functions | libs/token/token.ts |
| Validation middleware | libs/token/validateToken.ts |
| Auth service | services/auth/src/ |