Skip to content

Authentication (JWT)

Everything is driven by src/core/auth/: a non-React session state machine, used by guards (beforeLoad), the HTTP wrapper and hooks.

  1. Login — POST /auth/login {login, mot_de_passe} (snake_case, generated schema DTO) → {accessToken, accessExpiresIn, refreshToken, refreshExpiresIn, utilisateur}. Then GET /auth/me → {id, login, role, permissions} (source of permissions, see Authorization).
  2. Silent refresh — scheduled at accessExpiresIn - 30 s, rotating the refresh token (the consumed refresh is replaced, never replayed). Single-flight: if several requests get a 401 in parallel, only one goes through the refresh.
  3. 401 on a request — the wrapper (core/api/http.ts) attempts a refresh then replays the request once. Failure → onSessionExpired() → local purge.
  4. Logout — refresh token revocation (best-effort), then local purge and redirect to /login.

Public two-screen flow (src/features/auth/components/):

  1. /mot-de-passe-oublie (MotDePasseOubliePage) — the “Mot de passe oublié ?” link on the login page → POST /auth/mot-de-passe-oublie {login}. The backend emails a signed link pointing back to the frontend.
  2. /reinitialiser-mot-de-passe?token=… (ReinitialiserMotDePassePage) — the emailed link sets the new password → POST /auth/reinitialiser-mot-de-passe {token, mot_de_passe}. The backend closes the account’s active sessions: the user signs in again afterwards.

Both routes are public — listed in NO_AUTH_PATHS (core/api/http.ts): no bearer, no refresh/retry. The same flow is triggered while signed in from “Mon compte” (resident portal and client space, portail-compte-page.tsx) via the “Modifier mon mot de passe” button — the current session is then closed once the new password is set.

Project decision (validated 2026-08-18): persistent — createLocalStorageTokenStore. The session survives a page reload: at client startup, auth.restore() re-reads the persisted tokens, performs a refresh (rotation) then /auth/me to restore the user. On failure, the _authenticated guard redirects to /login?next=… and the login page reconnects then sends the user back to the original URL.

Memory localStorage (chosen)
Survives reload No (re-login) Yes
XSS attack surface Reduced (nothing in localStorage) Larger

Why this choice: persistence was requested by the user (session kept across reloads). The trade-off: an injected script (XSS) could read the tokens in persistent storage — deduplicated refresh and strict backend rotation limit exploitation.

  • Tokens are never logged (neither console, nor Sentry, nor a UI field).
  • No token in .env, the bundle, or URLs.
  • Storage is injectable; every feature uses useAuth() / useCurrentUser() — never direct TokenStorage access.
  • Session restore goes through auth.restore() (deduplicated with the refresh) — never replay a refresh token (the backend revokes the session).
  • ❌ Switch back to memory without an explicitly validated decision.
  • ❌ fetch('/auth/...') outside the wrapper (core/api/http.ts).
  • ❌ Re-implement refresh inside a feature (the singleton already does it).