2026-05-26
Session e refresh token in SPA: pattern e errori comuni
Storage, rotazione e minacce realistiche quando una SPA consuma API con cookie o bearer token.
Una SPA (React, Vue, Svelte…) che parla con la tua API deve decidere dove tenere access token brevi e refresh token lunghi. Le slide dicono “usa cookie HttpOnly”; in pratica molte app mettono JWT in localStorage per comodità—ed espongono XSS long-lived.
Questo articolo è framework-agnostico; in codice puoi applicarlo con Auth.js, Lucia, better-auth o session server-side custom—the point è il modello di minaccia, non il brand.
Minacce principali
- XSS: script malevolo nel DOM legge
localStorage/sessionStorage. Gli HttpOnly cookie non sono leggibili da JS → preferiti per refresh (e spesso session id). - CSRF: browser invia cookie automaticamente. Mitigazioni: cookie
SameSite=LaxoStrictper flussi “typical navigation”, CSRF token o pattern double submit perSameSite=Nonecross-site, oppure evitare cookie su API pubbliche cross-origin usando solo bearer da memoria (con tradeoff XSS più duro).
Pattern consigliato (alto livello)
- Access token: breve TTL (5–15 min), tenuto in memoria JS (closure/store volatile) quando possibile. Se serve persistenza tra tab, valuta cookie di sessione wrapper lato API piuttosto che localStorage.
- Refresh token: cookie HttpOnly + Secure + path stretto
/auth/refresh, renew con POST dedicata. Rotazione: ogni refresh emette nuovo refresh e invalida il precedente server-side (store conjtio session table). - Revoca: blacklist / versione sessione sul server così un refresh rubato smette dopo un round.
Set-Cookie: refresh=...; HttpOnly; Secure; Path=/auth/refresh; SameSite=Lax; Max-Age=...
Cosa evitare
- Refresh in
localStoragesalvo threat model esplicito (es. solo mobile embedded WebView con altre guardie). - Lunghi access token “perché così facciamo meno refresh”—composti il rischio device rubato.
- Multiple refresh concorrenti senza dedup: quattro tab possono generare quattro sessioni—usa mutex client (
refreshPromisecondiviso) o endpoint idempotente.
SPA + API su altro dominio
Se frontend è app.example.com e API api.example.com:
- Cookie cross-subdomain:
Domain=.example.com+Secure+SameSitecoerente con flusso OAuth/PKCE che usi. - Alternativa moderna: Backend-for-Frontend stesso-site che fa proxy—riduce complessità CORS/cookie.
Logout
Cancella cookie refresh con Max-Age=0, invalida server-side jti, e azzera store client in memoria. Se ti basta logout locale senza chiamare API, il refresh rimasto valido è ancora pericoloso.
RFC 9700 (BCP 240): refresh per client pubblici
Il RFC 9700 (Best Current Practice for OAuth 2.0 Security, BCP 240) è la “linea guida OAuth2” aggiornata (2025). Per questo articolo, la sezione rilevante è la replay detection sui refresh token:
- Per client pubblici (tipico: SPA senza secret) l’authorization server deve usare refresh token rotation oppure token sender-constrained (DPoP/mTLS). Con la rotazione, ogni refresh risponde con nuovo refresh e invalida il precedente; se qualcuno riusa un refresh già consumato, il server rileva competizione legittima vs attaccante e può revocare la catena.
- Testo utile da citare in review: “If a refresh token is compromised and subsequently used by both the attacker and the legitimate client, one of them will present an invalidated refresh token…” (RFC 9700 §4.14).
Non è “paper teorico”: è lo standard di riferimento quando integri IdP seri (Auth0, Keycloak, Azure AD, …) con grant browser-based.
Riferimenti
OWASP Session Management Cheat Sheet, RFC 9700, e RFC 6749 per terminologia refresh/OAuth—adatta al tuo grant; molte SPA usano session custom invece di OAuth puro.
Sintesi
Refresh HttpOnly, access corto in memoria, rotazione + revoca server, dedup refresh. localStorage per secret lunghi è la scorciatoia più costosa che vediamo in produzione.