Skip to main content
TenderCheck AI uses a hybrid authentication strategy: the server sets an HttpOnly 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.
The 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

The HttpOnly 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 the Authorization header on every request. The backend accepts whichever is present.
How the authMiddleware resolves the token:
  1. Checks req.cookies.token (HttpOnly cookie path).
  2. If no cookie, checks req.headers.authorization for a Bearer <token> string.
  3. If neither is present, responds with HTTP 401 "No authorization token provided".
  4. If a token is found but is invalid or expired, responds with HTTP 401 "Invalid or expired token".
For file upload endpoints (/api/tenders/analyze, /api/tenders/:id/validate-proposal), do not set a Content-Type header manually — fetch will set the correct multipart/form-data boundary automatically when you pass a FormData body. The Authorization header is still required if you’re not relying on cookies.

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.
Requires: Cookie or 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.
No request body is required.

Success Response — 200 OK

The 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

The HttpOnly cookie is also set with 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 the JWT_SECRET environment variable.
The JWT is signed with only the userId claim:
The 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:
Storing the token in localStorage is supported as a fallback, but it exposes the token to JavaScript running on the page (XSS risk). Prefer the HttpOnly cookie path wherever possible — it is inaccessible to JavaScript and is sent automatically on every same-origin (or credentialed cross-origin) request. Only use localStorage as a secondary fallback for environments where cookies cannot be used.