token cookie as the primary session mechanism, and the same JWT is also returned in the response body so clients can store it as a Bearer token fallback. This dual approach exists because HttpOnly cookie reliability varies across platforms and CORS setups — native apps, server-to-server calls, and some cross-origin browser configurations cannot use cookies reliably, so the Authorization header path ensures those environments still work without any special server configuration.
The authMiddleware always checks the cookie first. If no cookie is present it looks for an Authorization: Bearer <token> header. Either path leads to the same verified JWT payload.
Register
Create a new TenderCheck AI account.Request Body
string
required
Full display name. Minimum 2 characters.
string
required
A valid email address. Stored in lowercase.
string
required
Must satisfy all of the following rules (validated server-side via Zod):
- Minimum 8 characters (
PASSWORD_MIN_LENGTH = 8) - At least one uppercase letter (A–Z)
- At least one number (0–9)
- At least one special character (any non-alphanumeric character)
string
Optional company or organisation name.
Success Response — 201 Created
The server auto-logs the user in after registration: it issues a JWT, sets the HttpOnly cookie, and returns the token in the body.
string
Confirmation string:
"User registered successfully".string
Signed JWT. Store in
localStorage as auth_token for the Bearer token fallback.object
The newly created user record.
token HttpOnly cookie is also set on the response automatically — no extra step needed in a browser context.
Error Responses
When a duplicate email is submitted,
AuthService throws a plain Error("User already exists"). This is caught by the global error handler and returned as an HTTP 500 with the message "Something went wrong". User-facing code should treat any non-201 response from this endpoint as a registration failure and prompt the user to try a different email or log in instead.Login
Authenticate an existing user with email and password.This endpoint is rate-limited to 300 requests per 60-second window. Exceeding the limit returns HTTP
429. See Rate Limiting for details.Request Body
string
required
The account email address.
string
required
The account password.
boolean
When
true, the token cookie is set with maxAge = 30 days. When false or omitted, the cookie is a session cookie that is cleared when the browser is closed.Success Response — 200 OK
token cookie is set on the response in addition to the token field in the body.
Error Responses
Incorrect credentials cause
AuthService.login to throw a plain Error("Invalid credentials"), which the global error handler returns as HTTP 500 with "Something went wrong". This is an internal design detail — production error handling maps all unrecognised errors to a safe 500 to prevent credential enumeration.Making Authenticated Requests
For protected endpoints, the frontend sends both the HttpOnly cookie and theAuthorization header on every request. The backend accepts whichever is present.
authMiddleware resolves the token:
- Checks
req.cookies.token(HttpOnly cookie path). - If no cookie, checks
req.headers.authorizationfor aBearer <token>string. - If neither is present, responds with HTTP 401
"No authorization token provided". - If a token is found but is invalid or expired, responds with HTTP 401
"Invalid or expired token".
Get Current User
Retrieve the authenticated user’s profile. Use this on page load to restore an existing session without requiring the user to log in again.Authorization: Bearer <token> header.
Success Response — 200 OK
object
The JWT payload decoded from the active token. Contains
userId (the only claim signed into the token by AuthService).string
The token currently stored in the HttpOnly cookie. Returned so the client can sync
localStorage if it was cleared (e.g., after a hard refresh).The
user object returned by /api/auth/me contains only the claims that were signed into the JWT. AuthService signs tokens with { userId: user.id } only — email and name are not included in the JWT payload and are therefore not present in this response. To get the full user profile (including name and email), use the data returned from /api/auth/login or /api/auth/register and cache it client-side.Error Responses
Logout
Clear the server-side session cookie.Success Response — 200 OK
token HttpOnly cookie is cleared with clearCookie. The frontend should also remove the localStorage token immediately after a successful logout:
Google OAuth (PKCE)
Sign in or register using a Google account via the Authorization Code + PKCE flow.This endpoint is rate-limited to 300 requests per 60-second window — the same limiter as
/api/auth/login. See Rate Limiting for details.Flow
1
Initiate on the frontend
The frontend generates a PKCE
codeVerifier and codeChallenge pair, then redirects the user to Google’s authorisation endpoint with response_type=code and the code_challenge.2
Google redirects back
After the user consents, Google redirects back to the frontend with a one-time
code in the URL query string.3
Send code + verifier to backend
The frontend POSTs the
code and the original codeVerifier (never exposed to Google) to /api/auth/google/callback.4
Backend exchanges and issues session
The backend exchanges the code for Google tokens, extracts user info from the ID token, finds or creates the user in the database, and returns a TenderCheck AI JWT + sets the HttpOnly cookie.
Request Body
string
required
The one-time authorization code returned by Google’s OAuth consent screen.
string
required
The PKCE code verifier generated by the frontend before initiating the OAuth flow. Never sent to Google directly — only to this endpoint.
Success Response — 200 OK
rememberMe = true (30-day maxAge) for Google logins.
Error Responses
Required Backend Environment Variables
Why PKCE? The PKCE (Proof Key for Code Exchange) extension prevents authorization code interception attacks. The
codeVerifier is a cryptographic secret known only to the initiating client — even if an attacker intercepts the code in the redirect URL, they cannot exchange it without the matching verifier. This is why TenderCheck AI uses the Authorization Code + PKCE flow rather than the legacy Implicit flow (which exposed access tokens directly in the URL).Password Reset
Request a password reset link for an account.Request Body
string
required
The email address associated with the account.
Success Response — 200 OK
This endpoint always returns HTTP 200, regardless of whether the email is registered. This is intentional — it prevents user enumeration attacks where an attacker could determine which email addresses have accounts by probing the API.
Error Responses
JWT Token Details
All JWTs are signed and verified using theJWT_SECRET environment variable.
The JWT is signed with only the
userId claim:
authMiddleware attaches the decoded payload to req.user with the following TypeScript shape — note that email and role will only be populated if they were signed into the token by a future version of the service: