Authentification (JWT)
Tout est piloté par src/core/auth/ : machine à états de session non-React,
utilisée par les guards (beforeLoad), le wrapper HTTP et les hooks.
Cycle de vie
Section intitulée « Cycle de vie »- Login —
POST /auth/login {login, mot_de_passe}(snake_case, DTO du schéma généré) →{accessToken, accessExpiresIn, refreshToken, refreshExpiresIn, utilisateur}. PuisGET /auth/me→{id, login, role, permissions}(source des permissions, Autorisation). - Refresh silencieux — planifié à
accessExpiresIn - 30 s, rotation du refresh token (le refresh consommé est remplacé, jamais rejoué). Single-flight : si plusieurs requêtes reçoivent 401 en parallèle, une seule passe au refresh. - 401 sur une requête — le wrapper (
core/api/http.ts) tente un refresh puis rejoue la requête une fois. Échec →onSessionExpired()→ purge locale. - Logout — révocation du refresh token (best-effort) puis purge locale et
redirection
/login.
Réinitialisation de mot de passe (self-service)
Section intitulée « Réinitialisation de mot de passe (self-service) »Flux public en deux écrans (src/features/auth/components/) :
/mot-de-passe-oublie(MotDePasseOubliePage) — lien « Mot de passe oublié ? » de la page login →POST /auth/mot-de-passe-oublie {login}. Le backend envoie un email avec un lien signé vers le frontend./reinitialiser-mot-de-passe?token=…(ReinitialiserMotDePassePage) — le lien de l’email pose le nouveau mot de passe →POST /auth/reinitialiser-mot-de-passe {token, mot_de_passe}. Le backend clôt les sessions actives du compte : l’utilisateur se reconnecte ensuite.
Les deux routes sont publiques — listées dans NO_AUTH_PATHS
(core/api/http.ts) : jamais de bearer ni de refresh/retry. Le même flux est
déclenché connecté depuis « Mon compte » (portail résident et espace client,
portail-compte-page.tsx) via le bouton « Modifier mon mot de passe » — la
session en cours est alors fermée quand le nouveau mot de passe est posé.
Stockage des tokens
Section intitulée « Stockage des tokens »Décision projet (validée le 2026-08-18) : persistant —
createLocalStorageTokenStore. La session survit au rechargement de la page :
au démarrage du client, auth.restore() relit les tokens persistés, fait un
refresh (rotation) puis /auth/me pour rétablir l’utilisateur. En cas d’échec,
le guard _authenticated redirige vers /login?next=… et la page de login
reconnecte puis renvoie vers l’URL d’origine.
| Mémoire | localStorage (choisi) | |
|---|---|---|
| Survie au reload | Non (re-login) | Oui |
| Surface d’attaque XSS | Réduite (rien en localStorage) | Plus grande |
Pourquoi ce choix : la persistance était demandée par l’utilisateur (session conservée après actualisation). Le compromis : un script injecté (XSS) pourrait lire les tokens dans le stockage persistant — le refresh dédupliqué et la rotation stricte côté backend limitent l’exploitation.
- Les tokens ne sont jamais loggés (ni
console, ni sentry, ni champ UI). - Aucun token dans le
.env, le bundle, ou les URLs. - Le stockage est injectable ; toute feature utilise
useAuth()/useCurrentUser()— jamais d’accès direct auTokenStorage. - La restauration de session passe par
auth.restore()(dédupliquée avec le refresh) — ne jamais rejouer un refresh token (le backend révoque la session).
À ne pas faire
Section intitulée « À ne pas faire »- ❌ Revenir à la mémoire sans décision explicitement validée.
- ❌ Faire un
fetch('/auth/...')hors du wrapper (core/api/http.ts). - ❌ Ré-implementer le refresh dans une feature (le singleton le fait déjà).
