import type { CookieOptions } from 'express'; /** * Name of the CSRF cookie that pairs with the `__Host-portal_session` * cookie. Per ADR-0009 §"Double-submit CSRF": the BFF generates a * random token at session creation and writes it both to the session * payload (server-side) and to this cookie (readable by the SPA). * Every mutating request must echo the value back in an * `X-CSRF-Token` header; the middleware compares the header against * the *session-stored* token, not the cookie — the cookie is just * the SPA's UX way of reading the value it's expected to send back. * * Same env-conditional naming as the session cookie: * - production: `__Host-portal_csrf` (prefix mandates Secure + * Path=/ + no Domain — defeats subdomain cookie injection) * - development: `portal_csrf` (no Secure available on HTTP) */ const PRODUCTION_NAME = '__Host-portal_csrf'; const DEVELOPMENT_NAME = 'portal_csrf'; export function csrfCookieName(): string { return process.env['NODE_ENV'] === 'production' ? PRODUCTION_NAME : DEVELOPMENT_NAME; } /** * Cookie options for the CSRF cookie. * * Crucially **NOT** `HttpOnly`: the SPA needs JavaScript access to * read the token and echo it back in the `X-CSRF-Token` header. The * tradeoff is acceptable because the cookie is _not_ the auth * credential — the session id cookie (`__Host-portal_session`, which * IS HttpOnly) is. An XSS that reads this CSRF cookie still cannot * impersonate the user without also stealing the session cookie. * * `SameSite=Lax` (not Strict): matches the session cookie so the * two travel together on the SPA→BFF cross-origin same-site fetch * (different ports = different origin but same registrable domain). * Strict would drop on top-level POST navigations originating from * a different origin, which is correct for raw CSRF defense but * loses parity with the session cookie's policy. The double-submit * pattern itself is what gives the protection here; SameSite=Lax is * a belt-and-braces layer. */ export function csrfCookieOptions(maxAgeMs: number): CookieOptions { const isProduction = process.env['NODE_ENV'] === 'production'; return { httpOnly: false, sameSite: 'lax', secure: isProduction, path: '/', maxAge: maxAgeMs, }; }