# 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/auth` in `src/index.ts`). `/signin/local`, `/refresh`, and `/signout` are unauthenticated routes that read/set/clear cookies; `/me` is JWT-authenticated. - Controller: `src/api/controllers/auth.controller.ts` — sets cookies via `cookies.setSessionCookies`, reads the refresh cookie via `cookies.extractRefreshCookie`, clears via `cookies.clearSessionCookies`. - Service (BLL): `src/services/auth.ts` (`createSession`, `refreshSession`, `revokeSession`) with `SessionOptions` in `src/services/auth.types.ts`. - Cookie helpers: `src/auth/cookies.ts`. - Passport JWT strategy: `src/auth/auth.ts` (reads the access cookie via `cookies.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` -> `200` the current user profile. Validates credentials (`AuthService.signin`), creates a session, sets the access and refresh HttpOnly cookies. - `POST /api/auth/refresh` -> `200` the current user profile. Reads the refresh cookie, rotates it (`AuthService.refreshSession`), sets fresh cookies. Returns `ForbiddenError` when 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 returns `204`). - `GET /api/auth/me` (JWT-authenticated) -> `200` the current user profile, authenticated from the access cookie. - OAuth callbacks (`/signin/google/callback`, `/signin/microsoft/callback`) set session cookies and redirect to `config.uiUrl` without token query parameters. ## Access Rules - Protected API routes authenticate through the access cookie. The Passport JWT strategy (`src/auth/auth.ts`) extracts the token with `cookies.extractAccessCookie`, verifies it with `config.secret_key`, loads the user by email, and rejects disabled users. - The JWT payload is `{ user: { id, email } }`, signed by `jwtSign`. - `/refresh` and `/signout` are not Passport-guarded; they authenticate solely from the refresh cookie value. ## CSRF / Origin Protection - `src/middlewares/csrf-origin.ts` (`csrfOrigin`) is applied to all `/api` routes via `app.use('/api', csrfOrigin)` in `src/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 `Origin` header, falling back to the `Referer` header (parsed to its origin). - The request is allowed when `config.auth.allowAllOrigins` is true and a source origin is present, or when the source origin is in `config.auth.allowedOrigins`. Otherwise it is rejected with `ForbiddenError`. - `allowAllOrigins` is enabled only in the `dev_stage` (production-like, non-production) environment when `ALLOWED_ORIGINS` is not set; strict production requires an explicit allow-list. ## Refresh Tokens Access/refresh rotation (`src/services/auth.ts` + `auth_refresh_tokens`): - `createSession` mints a signed access JWT and an opaque refresh token (`crypto.randomBytes(config.auth.refreshTokenBytes).toString('base64url')`), stores the SHA-256 hash (`config.auth.refreshTokenHashAlgorithm`) in `auth_refresh_tokens` with `userId`, `organizationId`, `familyId`, `previousTokenId`, `userAgent`, `ipAddress`, and `expiresAt` (`now + config.auth.refreshTokenMaxAgeMs`). Only the hash is persisted. - `refreshSession` runs in a transaction: looks up the token by hash, then rejects with `ForbiddenError` and: - 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` (setting `previousTokenId`) and marks the old token revoked with `replacedByTokenId` pointing at the new row (rotation). - `revokeSession` looks up the token by hash and revokes it (`replacedByTokenId` null); missing or already-revoked tokens are a no-op. ## Cookie Behavior `src/auth/cookies.ts`: - Access cookie name: `config.auth.accessCookieName` (env `AUTH_ACCESS_COOKIE_NAME`, default `AUTH_COOKIE_NAME` = `school_chain_session`). Max-age `config.auth.accessTokenMaxAgeMs` (env `AUTH_COOKIE_MAX_AGE_MS`, default `JWT_EXPIRES_IN_MS` = 15 minutes). - Refresh cookie name: `config.auth.refreshCookieName` (env `AUTH_REFRESH_COOKIE_NAME`, default `AUTH_REFRESH_COOKIE_NAME` = `school_chain_refresh`). Max-age `config.auth.refreshTokenMaxAgeMs` (env `AUTH_REFRESH_TOKEN_MAX_AGE_MS`, default `REFRESH_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`, and `domain` = `config.auth.cookieDomain` when configured. - `clearSessionCookies` clears both cookies using the same path/domain. - Cookies are read by manually parsing the `Cookie` header (`extractCookie` / `extractAccessCookie` / `extractRefreshCookie`). ## Configuration `src/shared/config/index.ts` (`config.auth`), from constants in `src/shared/constants/auth.ts` and environment values: - `ALLOWED_ORIGINS` -> `allowedOrigins` - `AUTH_ACCESS_COOKIE_NAME` -> `accessCookieName` - `AUTH_REFRESH_COOKIE_NAME` -> `refreshCookieName` - `AUTH_COOKIE_SAME_SITE` -> `cookieSameSite` (default `lax`) - `AUTH_COOKIE_SECURE` -> `cookieSecure` (default: true in production-like envs) - `AUTH_COOKIE_MAX_AGE_MS` -> `accessTokenMaxAgeMs` - `AUTH_REFRESH_TOKEN_MAX_AGE_MS` -> `refreshTokenMaxAgeMs` - `AUTH_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`.