Disconnetti un utente da un’app basata su sessione e il server lo dimentica alla richiesta successiva. Disconnettilo da un’app basata su JWT e, a meno che tu non abbia costruito un meccanismo apposito, il token che ha in mano resta valido finché non scade. Questa è la vera decisione dietro “JWT vs sessione”. Il resto ne consegue.
Cosa memorizza davvero una sessione
Una sessione mantiene lo stato dell’utente sul server e al browser passa solo una chiave di lookup.
- L’utente effettua il login con una password.
- Il server crea un record di sessione (ID utente, ruoli, tutto ciò che serve) e lo salva in memoria, in un database o su Redis, indicizzato con un ID di sessione casuale.
- Il server invia quell’ID al browser come cookie, marcato
HttpOnlycosì il JavaScript lato client non può leggerlo. - A ogni richiesta, il browser rimanda automaticamente il cookie. Il server cerca l’ID nello store e recupera i dati della sessione.
Il cookie non porta nessuna informazione sull’utente, solo una stringa opaca che il server risolve a ogni richiesta. Elimina il record nello store e la sessione scompare alla richiesta successiva, su ogni dispositivo.
Cosa contiene un JWT
Un JSON Web Token evita il lookup mettendo lo stato direttamente nel token, firmato in modo che non possa essere manomesso.
Un JWT è composto da tre parti codificate in base64url e separate da punti: header.payload.signature. Il payload è un oggetto JSON di claim. L’RFC 7519 definisce un breve elenco di claim registrati:
| Claim | Nome | Significato |
|---|---|---|
iss | Issuer | Chi ha emesso il token |
sub | Subject | A chi si riferisce il token |
aud | Audience | A chi è destinato il token |
exp | Expiration | Rifiuta il token dopo questo momento |
nbf | Not before | Rifiuta il token prima di questo momento |
iat | Issued at | Quando è stato creato il token |
jti | JWT ID | ID univoco, utile per revoca e controlli anti-replay |
Il server firma header e payload con un secret (HMAC) o una chiave privata (RSA/ECDSA) e invia il risultato al client. Qualsiasi server che possieda la chiave corrispondente, o anche solo la chiave pubblica per la firma asimmetrica, può verificare la firma e fidarsi dei claim. Nessun round trip verso il database, nessun session store condiviso. In un sistema con più servizi, ciascuno può verificare un token per conto proprio.
Il rovescio della medaglia: chiunque possieda il token può leggere il payload. Base64 è una codifica, non una cifratura. Non inserire mai una password, un secret o dati personali che non stamperesti in un log dentro il payload di un JWT.
Come configurare l’autenticazione a sessione in Express
Una configurazione tipica in Node usa express-session come middleware e connect-redis perché le sessioni sopravvivano a un riavvio del server e funzionino su più istanze. Se Redis è nuovo per te, come funziona il caching con Redis copre le basi per farlo girare.
| |
| |
req.session.destroy() rimuove la voce da Redis. La richiesta successiva con quel cookie non trova nulla e viene trattata come logout. Nessuna blacklist di token, niente da ritoccare dopo.
Come configurare l’autenticazione JWT in Node
Lo stesso flusso con jsonwebtoken, emettendo un access token di breve durata e un refresh token separato:
| |
| |
jwt.verify controlla la firma e i claim exp/nbf in un’unica chiamata e lancia un’eccezione se uno dei due fallisce. Questo è l’intero controllo di autenticazione, senza alcuno store coinvolto.
L’opzione algorithms: ["HS256"] conta. Senza di essa, un token firmato con un algoritmo diverso da quello previsto potrebbe comunque passare, ed è così che diverse librerie JWT si sono ritrovate con vulnerabilità di “alg confusion”. Fissa nel codice l’elenco di algoritmi accettati in fase di verifica e rifiuta sempre alg: "none".
Il refresh token è ciò che rende vivibile un access token di 15 minuti. Il client lo conserva e, quando l’access token scade, lo scambia per una nuova coppia. Salva il jti del refresh token lato server così puoi revocare quella singola catena senza toccare gli access token già in circolazione.
Perché i JWT sono difficili da revocare
Una sessione è un record in uno store: eliminalo e l’utente viene disconnesso ovunque. Un JWT è un claim autosufficiente che qualsiasi verificatore accetta finché exp non dice il contrario. Il logout, il ban di un utente o il downgrade del suo ruolo non cambiano nulla per un token già emesso, perché il server che verifica non ha nessun record da eliminare.
Le mitigazioni più comuni:
- Vita breve dell’access token. 15-30 minuti è l’intervallo più usato, così un token rubato o scaduto ha una finestra piccola.
- Rotazione del refresh token. Ogni refresh emette un nuovo refresh token e invalida quello vecchio; riusare un refresh token vecchio revoca tutta la catena. OAuth 2.1 richiede questo meccanismo (o token sender-constrained) per i client pubblici.
- Una denylist mirata. Per uccidere un token specifico prima che scada, indicizza la denylist sul
jtipiù l’iss, non sulla stringa grezza del token. L’OWASP JWT Cheat Sheet spiega perché metterla sulla stringa stessa non è affidabile. Una denylist controllata a ogni richiesta rimette anche una query sul database a ogni richiesta, cioè esattamente il costo che i JWT dovevano eliminare.
Se la tua app finisce comunque per fare un lookup a ogni verifica, una sessione sarebbe stata la scelta più semplice.
Dove salvare un JWT nel browser
In un’app browser, metti il token in un cookie HttpOnly e Secure, non in localStorage. Qualsiasi script che gira sulla pagina può leggere localStorage, quindi un solo buco XSS consegna tutti i token che contiene. Il costo: una volta che il token viaggia in un cookie, il browser lo invia automaticamente, e ti ritrovi a difenderti dal CSRF con SameSite e token CSRF, esattamente come con le sessioni. Il confronto completo è in LocalStorage, SessionStorage o Cookie: Cosa Usare.
Quando usare le sessioni e quando usare i JWT
| Situazione | Usa |
|---|---|
| Una sola app server-rendered, un solo backend | Sessioni: più semplici, revoca immediata |
| Logout istantaneo o cambi di permessi istantanei | Sessioni, oppure JWT con una denylist su jti |
| Più servizi backend che verificano l’utente in autonomia | JWT: nessun session store condiviso da richiamare |
| App mobile o client API di terze parti | JWT: i bearer token si adattano a un’API HTTP stateless |
Nessuno dei due è più sicuro in astratto. Una sessione su Redis e un JWT di 15 minuti con refresh token ruotati tengono entrambi. La differenza è architetturale: quanti servizi devono fidarsi del claim e quanto in fretta devi poterlo revocare.
Con un solo backend, scegli le sessioni di default: la revoca è gratis. Passa ai JWT il giorno in cui un secondo servizio deve sapere chi è l’utente senza richiamare quello che l’ha autenticato.