Authentication (JWT)
Everything is driven by src/core/auth/: a non-React session state machine,
used by guards (beforeLoad), the HTTP wrapper and hooks.
Lifecycle
Section titled “Lifecycle”- Login —
POST /auth/login {login, mot_de_passe}(snake_case, generated schema DTO) →{accessToken, accessExpiresIn, refreshToken, refreshExpiresIn, utilisateur}. ThenGET /auth/me→{id, login, role, permissions}(source of permissions, see Authorization). - 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. - 401 on a request — the wrapper (
core/api/http.ts) attempts a refresh then replays the request once. Failure →onSessionExpired()→ local purge. - Logout — refresh token revocation (best-effort), then local purge and
redirect to
/login.
Self-service password reset
Section titled “Self-service password reset”Public two-screen flow (src/features/auth/components/):
/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./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.
Token storage
Section titled “Token storage”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 directTokenStorageaccess. - Session restore goes through
auth.restore()(deduplicated with the refresh) — never replay a refresh token (the backend revokes the session).
Don’ts
Section titled “Don’ts”- ❌ 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).
