JWT and HttpOnly cookies
Authentication tokens are placed by the backend in HttpOnly cookies, which JavaScript cannot read. The frontend no longer stores them in localStorage: an XSS flaw cannot steal them.
Lifetimes, routes and key management: see JWT authentication.
Cookies set by the backend
| Cookie | Content | Path | Lifetime | Attributes |
|---|---|---|---|---|
BEARER | JWT access token | / | 1 hour | HttpOnly; Secure and SameSite=Strict over HTTPS, Lax over local HTTP |
REFRESH_TOKEN | refresh token | /api/token | 30 days | same |
XSRF-TOKEN | anti-CSRF token | / | 1 hour, renewed | readable by JavaScript, by design; same Secure and SameSite |
They are set by JWTCookieSubscriber on sign-in, 2FA verification, magic link and refresh responses, and cleared on POST /api/token/invalidate. The sign-in response body still contains the token, for command-line clients.
Token reading order
BEARERcookie (browser).Authorization: Bearer …header (scripts, tools, integrations).
The refresh token is read the same way: REFRESH_TOKEN cookie, otherwise the refresh_token field of the body.
CSRF protection
A cookie is sent automatically with every request to the domain. So that a third-party site cannot trigger actions on the user's behalf, mutations (POST, PUT, PATCH, DELETE) authenticated by cookie must carry the X-CSRF-TOKEN header equal to the XSRF-TOKEN cookie (CsrfProtectionSubscriber). The frontend Axios client adds it by itself.
Exempt: sign-in, 2FA verification, refresh, sign-out, forgotten password, magic link, health and public routes (/api/public/, /api/cms/public/). Calls authenticated by the Authorization header are not concerned.
The check is on by default; CSRF_ENABLED=false turns it off, in development only.
Frontend side
The Axios client (frontend/src/services/api/client.ts) sends cookies (withCredentials: true) and keeps the access token in memory only. On page reload, it calls /api/token/refresh before the first request: the REFRESH_TOKEN cookie is enough to restore the session.
Check
In the browser developer tools, Application → Cookies tab: BEARER and REFRESH_TOKEN must appear with the HttpOnly box ticked, and Secure on the servers.
curl -si -X POST https://sig-preprod.ucp-pch.org/api/login \
-H "Content-Type: application/json" \
-d '{"email":"<EMAIL>","password":"<PASSWORD>"}' | grep -i "^set-cookie"