import type { HttpHandlerFn, HttpInterceptorFn, HttpRequest } from '@angular/common/http'; import { inject } from '@angular/core'; import { AUTH_BFF_BASE_URL, AUTH_CSRF_COOKIE_NAME } from './auth.config'; /** * Double-submit CSRF helper interceptor (per ADR-0009). Reads the * BFF-issued `__Host-portal_csrf` cookie (or its dev twin * `portal_csrf`) and copies the value into an `X-CSRF-Token` header * on every BFF request whose method mutates state. * * GET / HEAD / OPTIONS pass through without a header — the BFF * skips CSRF on safe methods anyway, and adding the header would * just be noise. * * Non-BFF origins pass through untouched. Same posture as the other * BFF-only interceptors in this lib. * * If the cookie is missing (anonymous, freshly-cleared session, * etc.), the request goes through without the header — the BFF * skips CSRF on anonymous requests, and on the authenticated path * the BFF will answer 403 which the SPA can surface as "please * sign in again". */ const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']); export const csrfInterceptor: HttpInterceptorFn = ( req: HttpRequest, next: HttpHandlerFn, ) => { const bffBaseUrl = inject(AUTH_BFF_BASE_URL); const cookieName = inject(AUTH_CSRF_COOKIE_NAME); if (!req.url.startsWith(bffBaseUrl)) return next(req); if (SAFE_METHODS.has(req.method.toUpperCase())) return next(req); const token = readCookie(cookieName); if (!token) return next(req); return next(req.clone({ setHeaders: { 'X-CSRF-Token': token } })); }; function readCookie(name: string): string | null { // `document.cookie` is the only browser API to read non-HttpOnly // cookies. The CSRF cookie is deliberately *not* HttpOnly so we // can do exactly this; the session cookie remains HttpOnly. const cookies = typeof document !== 'undefined' ? document.cookie : ''; for (const raw of cookies.split(';')) { const eq = raw.indexOf('='); if (eq < 0) continue; const key = raw.slice(0, eq).trim(); if (key === name) { return decodeURIComponent(raw.slice(eq + 1)); } } return null; }