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

  1. 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 the codeChallenge.

  2. Client sends login request. A POST /login request carries the user’s emailAddress and the codeChallenge.

  3. Server processes login. The server validates the request body with Zod, looks up the account (which must be in an ACTIVE state), generates a one-time password, stores it alongside the codeChallenge, and enqueues an email delivery job through BullMQ.

  4. Server returns tokens. The response includes an accessToken (default expiry 15 days) and a refreshToken (default expiry 7 days).

  5. Client sends verification request. After receiving the OTP via email, the client sends POST /verify with the otp and the original codeVerifier in the request body, along with the accessToken in the Authorization header.

  6. Server verifies PKCE challenge. The server decodes the bearer token to extract the emailAddress, locates the pending OTP record, re-hashes the supplied codeVerifier using SHA-256, and compares the result to the stored codeChallenge.

  7. 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 configured ACCESS_TOKEN_EXPIRES duration.
  • generateRefreshToken(payload) – Issues a refresh token using the configured REFRESH_TOKEN_EXPIRES duration.
  • 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:

  1. The client sends POST /token with the current refresh token in the Authorization header.
  2. The server extracts the refresh token and looks up the corresponding stored record via TokenModel.
  3. 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:

  1. Extracts the Bearer token from the Authorization header.
  2. Calls verifyToken() to validate the signature and decode the payload.
  3. On success, attaches the decoded payload to the request and passes control to the next handler.
  4. On failure, returns 401 Unauthorized (missing or malformed token) or 403 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/