7.4 KiB
Cookie Auth
Purpose
Browser authentication uses backend-owned HttpOnly cookies: a short-lived
access cookie carrying a signed JWT, and a long-lived opaque refresh cookie
backed by hashed, rotating refresh-token rows. The product frontend never
reads, stores, or sends auth tokens manually. This document covers the cookie
session transport, refresh rotation, CSRF/origin protection, and sign-out; the
profile and permission model is documented in backend/docs/auth-profile.md.
Slice Files (by layer)
- Route:
src/routes/auth.ts(mounted at/api/authinsrc/index.ts)./signin/local,/refresh, and/signoutare unauthenticated routes that read/set/clear cookies;/meis JWT-authenticated. - Controller:
src/api/controllers/auth.controller.ts— sets cookies viacookies.setSessionCookies, reads the refresh cookie viacookies.extractRefreshCookie, clears viacookies.clearSessionCookies. - Service (BLL):
src/services/auth.ts(createSession,refreshSession,revokeSession) withSessionOptionsinsrc/services/auth.types.ts. - Cookie helpers:
src/auth/cookies.ts. - Passport JWT strategy:
src/auth/auth.ts(reads the access cookie viacookies.extractAccessCookie). - Origin middleware:
src/middlewares/csrf-origin.ts. - Repositories (DAL):
src/db/api/auth_refresh_tokens.ts(AuthRefreshTokensDBApi),src/db/api/users.ts(UsersDBApi.findBy). - Model:
src/db/models/auth_refresh_tokens.ts. - Shared used:
shared/config(cookie/CORS/token config),shared/jwt(jwtSign),shared/constants/auth.ts(cookie names, TTLs, token bytes, hash algorithm, unsafe methods),shared/errors/forbidden.ts.
API
Base path /api/auth.
POST /api/auth/signin/local->200the current user profile. Validates credentials (AuthService.signin), creates a session, sets the access and refresh HttpOnly cookies.POST /api/auth/refresh->200the current user profile. Reads the refresh cookie, rotates it (AuthService.refreshSession), sets fresh cookies. ReturnsForbiddenErrorwhen the refresh cookie is missing/invalid/expired/revoked.POST /api/auth/signout->204 No Content. Revokes the active refresh token (AuthService.revokeSession) and clears both cookies. Missing/already-revoked tokens are a no-op (still clears cookies and returns204).GET /api/auth/me(JWT-authenticated) ->200the current user profile, authenticated from the access cookie.- OAuth callbacks (
/signin/google/callback,/signin/microsoft/callback) set session cookies and redirect toconfig.uiUrlwithout token query parameters.
Access Rules
- Protected API routes authenticate through the access cookie. The Passport JWT
strategy (
src/auth/auth.ts) extracts the token withcookies.extractAccessCookie, verifies it withconfig.secret_key, loads the user by email, and rejects disabled users. - The JWT payload is
{ user: { id, email } }, signed byjwtSign. /refreshand/signoutare not Passport-guarded; they authenticate solely from the refresh cookie value.
CSRF / Origin Protection
src/middlewares/csrf-origin.ts(csrfOrigin) is applied to all/apiroutes viaapp.use('/api', csrfOrigin)insrc/index.ts.- Safe methods pass through; only unsafe methods are checked
(
UNSAFE_HTTP_METHODS=POST,PUT,PATCH,DELETE). - The source origin is taken from the
Originheader, falling back to theRefererheader (parsed to its origin). - The request is allowed when
config.auth.allowAllOriginsis true and a source origin is present, or when the source origin is inconfig.auth.allowedOrigins. Otherwise it is rejected withForbiddenError. allowAllOriginsis enabled only in thedev_stage(production-like, non-production) environment whenALLOWED_ORIGINSis not set; strict production requires an explicit allow-list.
Refresh Tokens
Access/refresh rotation (src/services/auth.ts + auth_refresh_tokens):
createSessionmints a signed access JWT and an opaque refresh token (crypto.randomBytes(config.auth.refreshTokenBytes).toString('base64url')), stores the SHA-256 hash (config.auth.refreshTokenHashAlgorithm) inauth_refresh_tokenswithuserId,organizationId,familyId,previousTokenId,userAgent,ipAddress, andexpiresAt(now + config.auth.refreshTokenMaxAgeMs). Only the hash is persisted.refreshSessionruns in a transaction: looks up the token by hash, then rejects withForbiddenErrorand:- revokes the whole family (
revokeFamily) when the token is already revoked (reuse detection); - revokes the single token when it is expired;
- revokes the family when the user is missing or disabled.
On success it creates a new session in the same
familyId(settingpreviousTokenId) and marks the old token revoked withreplacedByTokenIdpointing at the new row (rotation).
- revokes the whole family (
revokeSessionlooks up the token by hash and revokes it (replacedByTokenIdnull); missing or already-revoked tokens are a no-op.
Cookie Behavior
src/auth/cookies.ts:
- Access cookie name:
config.auth.accessCookieName(envAUTH_ACCESS_COOKIE_NAME, defaultAUTH_COOKIE_NAME=school_chain_session). Max-ageconfig.auth.accessTokenMaxAgeMs(envAUTH_COOKIE_MAX_AGE_MS, defaultJWT_EXPIRES_IN_MS= 15 minutes). - Refresh cookie name:
config.auth.refreshCookieName(envAUTH_REFRESH_COOKIE_NAME, defaultAUTH_REFRESH_COOKIE_NAME=school_chain_refresh). Max-ageconfig.auth.refreshTokenMaxAgeMs(envAUTH_REFRESH_TOKEN_MAX_AGE_MS, defaultREFRESH_TOKEN_EXPIRES_IN_MS= 14 days). - Both cookies are set with
httpOnly: true,path=config.auth.cookiePath(/),sameSite=config.auth.cookieSameSite,secure=config.auth.cookieSecure, anddomain=config.auth.cookieDomainwhen configured. clearSessionCookiesclears both cookies using the same path/domain.- Cookies are read by manually parsing the
Cookieheader (extractCookie/extractAccessCookie/extractRefreshCookie).
Configuration
src/shared/config/index.ts (config.auth), from constants in
src/shared/constants/auth.ts and environment values:
ALLOWED_ORIGINS->allowedOriginsAUTH_ACCESS_COOKIE_NAME->accessCookieNameAUTH_REFRESH_COOKIE_NAME->refreshCookieNameAUTH_COOKIE_SAME_SITE->cookieSameSite(defaultlax)AUTH_COOKIE_SECURE->cookieSecure(default: true in production-like envs)AUTH_COOKIE_MAX_AGE_MS->accessTokenMaxAgeMsAUTH_REFRESH_TOKEN_MAX_AGE_MS->refreshTokenMaxAgeMsAUTH_COOKIE_DOMAIN->cookieDomain
Validation in config: AUTH_COOKIE_SECURE must be true when
AUTH_COOKIE_SAME_SITE is none and in any production-like environment, and
ALLOWED_ORIGINS must be set in production.
Secrets remain in environment variables only, especially SECRET_KEY and
OAuth/email credentials.
Security Rules
- Auth tokens must not be returned in response bodies.
- Auth tokens must not be placed in redirect URLs.
- Protected browser API routes authenticate through the HttpOnly access cookie.
- Only the refresh-token hash is stored in the database.
- Unsafe methods are protected by Origin/Referer validation against the configured allow-list.
- Refresh-token reuse triggers family revocation.
Tests
None yet (no auth unit/e2e test under backend/src).
Related
- Profile and permission model:
backend/docs/auth-profile.md. - Frontend:
frontend/docs/auth-integration.md.