Gestione delle sessioni per app web: cookie, JWT e refresh
Confronto sulla gestione delle sessioni nelle app web: sessioni via cookie, JWT e refresh token, con modelli di minaccia concreti e requisiti realistici di logout.

Cosa fa davvero la gestione delle sessioni
Una sessione è il modo in cui la tua app risponde a una domanda dopo che qualcuno ha effettuato l'accesso: "Chi sei adesso?". Una volta che quella risposta è affidabile, l'app può decidere cosa l'utente può vedere, cosa può modificare e quali azioni devono essere bloccate.
"Rimanere connessi" è anche una scelta di sicurezza. Stai decidendo per quanto tempo un'identità utente deve rimanere valida, dove vive la prova dell'identità e cosa succede se quella prova viene copiata.
La maggior parte delle configurazioni per app web si basa su tre elementi di base:
- Sessioni server basate su cookie: il browser memorizza un cookie e il server cerca la sessione a ogni richiesta.
- Token di accesso JWT: il client invia un token firmato che il server può verificare senza interrogare il database.
- Refresh token: un'identità con durata più lunga usata per ottenere nuovi token di accesso a breve durata.
Questi non sono stili in competizione quanto modi diversi di gestire gli stessi compromessi: velocità vs controllo, semplicità vs flessibilità, e "possiamo invalidarlo subito?" vs "scade da solo?".
Un modo utile per valutare qualsiasi progetto: se un attaccante ruba ciò che la tua app usa come prova (un cookie o un token), cosa può fare e per quanto tempo? Le sessioni con cookie spesso vincono quando hai bisogno di un forte controllo lato server, come il logout forzato o il blocco immediato. I JWT possono andar bene per controlli senza stato tra servizi, ma diventano difficili quando serve una revoca immediata.
Nessuna opzione vince sempre. L'approccio giusto dipende dal tuo modello di minacce, da quanto sono stringenti i requisiti di logout e da quanta complessità il tuo team può realisticamente mantenere.
Modelli di minaccia che cambiano la risposta giusta
Un buon design delle sessioni dipende meno dal "miglior" tipo di token e più dagli attacchi che devi effettivamente sopravvivere.
Se un attaccante ruba dati dalla memoria del browser (come localStorage), i token di accesso JWT sono facili da prendere perché il JavaScript della pagina può leggerli. Un cookie rubato è diverso: se è impostato come HttpOnly, il codice normale della pagina non può leggerlo, quindi gli attacchi "prendi il token" diventano più difficili. Ma se l'attaccante ha il dispositivo (portatile perso, malware, computer condiviso), i cookie possono comunque essere copiati dal profilo del browser.
L'XSS (codice dell'attaccante che gira nella tua pagina) cambia tutto. Con l'XSS, l'attaccante potrebbe non avere bisogno di rubare nulla. Può usare la sessione già autenticata della vittima per eseguire azioni. I cookie HttpOnly aiutano a prevenire la lettura dei segreti della sessione, ma non impediscono a un attaccante di fare richieste dalla pagina.
Il CSRF (un sito diverso che provoca azioni indesiderate) minaccia soprattutto le sessioni basate su cookie, perché i browser allegano automaticamente i cookie. Se ti affidi ai cookie, servono difese chiare contro il CSRF: impostazioni SameSite intenzionali, token anti-CSRF e gestione attenta delle richieste che cambiano stato. I JWT inviati nell'header Authorization sono meno esposti al CSRF classico, ma rimangono esposti all'XSS se sono memorizzati dove il JavaScript può leggerli.
Gli attacchi di replay (riutilizzo di una credenziale rubata) sono il punto di forza delle sessioni lato server: puoi invalidare immediatamente un ID di sessione. I JWT a breve durata riducono il tempo utile per il replay, ma non lo impediscono mentre il token è valido.
I dispositivi condivisi e i telefoni persi trasformano il "disconnetti" in un vero modello di minaccia. Le decisioni di solito si riducono a domande come: un utente può forzare il logout dagli altri dispositivi, quanto deve essere rapido l'effetto, cosa succede se un refresh token viene rubato e permetti le sessioni "ricordami"? Molti team applicano standard più rigidi agli accessi del personale rispetto ai clienti, cambiando timeout e aspettative di revoca.
Sessioni con cookie: come funzionano e cosa proteggono
Le sessioni basate su cookie sono la configurazione classica. Dopo il login, il server crea un record di sessione (spesso un ID più campi come user ID, tempo di creazione e scadenza). Il browser memorizza solo l'ID di sessione in un cookie. A ogni richiesta il browser rimanda quel cookie e il server cerca la sessione per decidere chi è l'utente.
Il grande vantaggio di sicurezza è il controllo. La sessione è convalidata sul server ogni volta. Se devi buttare fuori qualcuno, cancelli o disabiliti il record di sessione server-side e smette di funzionare immediatamente, anche se l'utente ha ancora il cookie.
Molta della protezione viene dalle impostazioni del cookie:
- HttpOnly: impedisce al JavaScript di leggere il cookie.
- Secure: invia il cookie solo su HTTPS.
- SameSite: limita quando il browser invia il cookie nelle richieste cross-site.
Dove memorizzi lo stato della sessione influisce sulla scalabilità. Tenere le sessioni in memoria dell'app è semplice, ma rompe quando esegui più server o fai frequenti riavvii. Un database è ottimo per durabilità. Redis è comune quando vuoi lookup veloci e molte sessioni attive. Il punto chiave è lo stesso: il server deve poter trovare e convalidare la sessione a ogni richiesta.
Le sessioni con cookie sono una scelta solida quando ti serve un comportamento di logout rigoroso, come cruscotti del personale o portali clienti dove un admin deve poter forzare il logout dopo un cambiamento di ruolo. Se un dipendente se ne va, disabilitare le sue sessioni server-side termina immediatamente l'accesso, senza aspettare l'esaurimento dei token.
Token di accesso JWT: punti forti e insidie
Un JWT (JSON Web Token) è una stringa firmata che contiene alcune affermazioni sull'utente (ad esempio user ID, ruolo, tenant) più un tempo di scadenza. La tua API verifica la firma e la scadenza localmente, senza chiamare il database, poi autorizza la richiesta.
Per questo i JWT sono popolari in prodotti API-first, app mobili e sistemi dove più servizi devono validare la stessa identità. Se hai molte istanze backend, ognuna può verificare lo stesso token e ottenere la stessa risposta.
Punti forti
I token di accesso JWT sono veloci da controllare e facili da passare con le chiamate API. Se il frontend chiama molti endpoint, un access token a breve durata può mantenere il flusso semplice: verifica la firma, leggi l'user ID, continua.
Esempio: un portale clienti chiama "Elenca fatture" e "Aggiorna profilo" su servizi separati. Un JWT può portare l'ID cliente e un ruolo come customer, così ogni servizio può autorizzare la richiesta senza cercare la sessione ogni volta.
Insidie
Il compromesso principale è la revoca. Se un token è valido per un'ora, di solito è valido ovunque per quell'ora, anche se l'utente preme "logout" o un admin disabilita l'account, a meno che tu non aggiunga controlli server-side extra.
I JWT perdono anche in modi ordinari. Punti di fallimento comuni includono localStorage (l'XSS può leggerlo), memoria del browser (estensioni dannose), log e report di errore, proxy e strumenti di analytics che catturano gli header, e token copiati in chat di supporto o screenshot.
Per questo i token di accesso JWT funzionano meglio per accessi a breve termine, non per il "login per sempre". Mantienili minimali (niente dati personali sensibili all'interno), mantieni scadenze brevi e assumiti che un token rubato sarà utilizzabile finché non scade.
Refresh token: rendere pratici i setup con JWT
I token di accesso JWT sono pensati per essere a breve durata. Questo è buono per la sicurezza, ma crea un problema pratico: gli utenti non dovrebbero dover effettuare il login di continuo ogni pochi minuti. I refresh token risolvono questo permettendo all'app di ottenere silenziosamente un nuovo access token quando quello vecchio scade.
Dove memorizzi il refresh token conta ancora più di dove memorizzi l'access token. In un'app web basata su browser, il default più sicuro è un cookie HttpOnly e Secure così il JavaScript non può leggerlo. Il local storage è più semplice da implementare, ma è anche più facile da rubare se hai un bug XSS. Se il tuo modello di minacce include l'XSS, evita di mettere segreti di lunga durata in uno storage accessibile via JavaScript.
La rotazione è ciò che rende i refresh token praticabili in sistemi reali. Invece di usare lo stesso refresh token per settimane, lo scambi ogni volta che viene usato: il client presenta il refresh token A, il server emette un nuovo access token più il refresh token B, e il refresh token A diventa invalido.
Un setup di rotazione semplice segue di solito alcune regole:
- Mantieni gli access token corti (minuti, non ore).
- Memorizza i refresh token server-side con stato e tempo dell'ultimo uso.
- Ruota a ogni refresh e invalida il token precedente.
- Associa i refresh token a un dispositivo o browser quando possibile.
- Registra gli eventi di refresh in modo da poter investigare abusi.
Il rilevamento del riuso è l'allarme chiave. Se il refresh token A è già stato scambiato, ma lo vedi di nuovo in seguito, assume che sia stato copiato. Una risposta comune è revocare l'intera sessione (e spesso tutte le sessioni per quell'utente) e richiedere un nuovo login, perché non puoi sapere quale copia sia quella reale.
Per il logout, ti serve qualcosa che il server possa far rispettare. Di solito significa una tabella di sessione (o una lista di revoca) che marca i refresh token come revocati. Gli access token possono ancora funzionare fino alla scadenza, ma puoi mantenere quella finestra piccola mantenendo gli access token a breve durata.
Requisiti di logout e cosa è realmente applicabile
Il logout sembra semplice finché non lo definisci. Ci sono di solito due richieste diverse: "disconnetti questo dispositivo" (un browser o un telefono) e "disconnetti ovunque" (tutte le sessioni attive su dispositivi diversi).
C'è anche una questione di tempi. "Logout immediato" significa che l'app smette di accettare la credenziale in questo momento. "Logout dopo scadenza" significa che l'app smette di accettarla quando la sessione o il token corrente scade naturalmente.
Con le sessioni basate su cookie, il logout immediato è semplice perché il server possiede la sessione. Cancelli il cookie sul client e invalidi il record di sessione server-side. Se qualcuno ha copiato il valore del cookie prima, è la rifiuto del server che applica il logout.
Con un'autenticazione solo JWT (token di accesso senza stato e senza lookup server), non puoi garantire veramente il logout immediato. Un JWT rubato rimane valido finché non scade, perché il server non ha dove verificare "questo token è revocato?". Puoi aggiungere una denylist, ma allora mantieni stato e lo controlli, il che toglie molta della semplicità originale.
Un pattern pratico è trattare gli access token come a breve durata e far rispettare il logout tramite i refresh token. L'access token può avere una finestra di tolleranza di alcuni minuti, ma il refresh token è ciò che mantiene viva una sessione. Se un portatile viene rubato, revocare la famiglia di refresh token taglia rapidamente l'accesso futuro.
Quello che puoi realisticamente promettere agli utenti:
- Disconnetti questo dispositivo: revoca quella sessione o quel refresh token e cancella cookie o storage locali.
- Disconnetti ovunque: revoca tutte le sessioni o tutte le famiglie di refresh token dell'account.
- Effetto "immediato": garantito con sessioni server, best-effort con access token fino alla loro scadenza.
- Eventi di logout forzato: cambio password, account disabilitato, downgrade del ruolo.
Per i cambi di password e la disabilitazione dell'account, non fare affidamento sul "l'utente si disconnetterà". Memorizza una versione della sessione a livello di account (o un timestamp "token valido dopo"). Ad ogni refresh (e talvolta ad ogni richiesta), confrontalo. Se è cambiato, nega l'accesso e richiedi nuovamente l'accesso.
Passo dopo passo: scegliere un approccio alle sessioni per la tua app
Se vuoi mantenere semplice il design delle sessioni, decidi prima le tue regole e poi scegli la meccanica. La maggior parte dei problemi nasce quando i team scelgono JWT o cookie perché sono popolari, non perché corrispondono ai rischi e ai requisiti di logout.
Inizia elencando tutti i posti dove un utente effettua l'accesso. Un'app browser si comporta diversamente da una nativa mobile, uno strumento amministrativo interno o un'integrazione partner. Ognuno cambia cosa può essere memorizzato in sicurezza, come i login vengono rinnovati e cosa dovrebbe significare "logout".
Un ordine pratico che funziona per la maggior parte dei team:
- Elenca i tuoi client: web, iOS/Android, strumenti interni, accesso di terze parti.
- Scegli un modello di minaccia predefinito: XSS, CSRF, dispositivo rubato.
- Decidi cosa deve garantire il logout: questo dispositivo, tutti i dispositivi, logout forzato dall'admin.
- Scegli un pattern di base: sessioni basate su cookie (server ricorda) o access token + refresh token.
- Imposta timeout e regole di risposta: scadenza idle vs scadenza assoluta, più cosa fai quando vedi riuso sospetto.
Poi documenta le promesse esatte che il tuo sistema fa. Esempio: "Le sessioni web scadono dopo 30 minuti di inattività o 7 giorni assoluti. L'admin può forzare il logout entro 60 secondi. Il telefono perso può essere disabilitato da remoto." Quelle frasi contano più della libreria che usi.
Infine, aggiungi monitoraggio che corrisponda al tuo pattern. Per i setup con token, un segnale forte è il riuso del refresh token (lo stesso refresh token usato due volte). Trattalo come furto probabile, revoca la famiglia di sessioni e avvisa l'utente.
Errori comuni che portano al takeover di account
La maggior parte dei takeover non sono "hack intelligenti". Sono vittorie semplici causate da errori prevedibili nella gestione delle sessioni. Una buona gestione delle sessioni riguarda soprattutto il non dare agli attaccanti un modo semplice per rubare o riprodurre le credenziali.
Una trappola comune è mettere gli access token in localStorage sperando di non avere mai XSS. Se qualsiasi script gira sulla tua pagina (una dipendenza vulnerabile, un widget iniettato, un commento memorizzato), può leggere localStorage e inviare il token fuori. I cookie con l'attributo HttpOnly riducono quel rischio perché il JavaScript non può leggerli.
Un'altra trappola è rendere i JWT a lunga durata per evitare i refresh token. Un access token di 7 giorni è una finestra di riutilizzo di 7 giorni se perde. Un access token breve più un refresh token ben gestito è più difficile da abusare, specialmente quando puoi tagliare il refresh.
I cookie portano il loro stesso problema: dimenticare le difese CSRF. Se la tua app usa sessioni con cookie e accetti richieste che cambiano stato senza protezione CSRF, un sito malevolo può ingannare un browser autenticato a inviare richieste valide.
Altri errori che emergono spesso dopo revisioni degli incidenti:
- I refresh token non ruotano mai, o ruotano ma non si rileva il riuso.
- Supporti più metodi di login (sessione cookie e bearer token) ma la regola del server su "quale prevale" non è chiara.
- I token finiscono nei log (console del browser, eventi analytics, log server), dove vengono copiati e conservati.
Un esempio concreto: un addetto al supporto incolla un "log di debug" in un ticket. Il log include un header Authorization. Chiunque abbia accesso al ticket può riprodurre quel token e agire come l'utente. Tratta i token come password: non stamparli, non memorizzarli e mantienili a breve durata.
Controlli rapidi prima della pubblicazione
La maggior parte dei bug sulle sessioni non riguarda crittografia complicata. Riguardano una bandierina mancante, un token che vive troppo a lungo o un endpoint che avrebbe dovuto richiedere una riautenticazione.
Prima del rilascio, fai un passaggio rapido concentrandoti su cosa può fare un attaccante con un cookie o token rubato. È uno dei modi più veloci per migliorare la sicurezza senza riscrivere tutto il sistema di auth.
Checklist pre-release
Esegui questi controlli in staging e poi ancora in produzione:
- Mantieni gli access token a breve durata (minuti) e conferma che l'API li rifiuta effettivamente dopo la scadenza.
- Tratta i refresh token come password: memorizzali dove il JavaScript non può leggerli se possibile, inviali solo all'endpoint di refresh e ruotali dopo ogni uso.
- Se usi cookie per l'autenticazione, verifica le flag: HttpOnly attivo, Secure attivo e SameSite impostato intenzionalmente. Conferma anche che l'ambito del cookie (domain e path) non sia più ampio del necessario.
- Se i cookie autenticano le richieste, aggiungi difese CSRF e conferma che gli endpoint che cambiano stato falliscano senza il segnale CSRF.
- Rendi la revoca reale: dopo reset della password o disabilitazione dell'account, le sessioni esistenti dovrebbero smettere di funzionare rapidamente (cancellazione sessione server-side, invalidazione refresh token o controllo "versione sessione").
Dopo quello, testa le promesse di logout. "Disconnetti" spesso significa "rimuovi la sessione locale", ma gli utenti si aspettano di più.
Un test pratico: effettua il login su laptop e telefono, poi cambia la password. Il laptop dovrebbe essere costretto a uscire alla sua successiva richiesta, non ore dopo. Se offri "disconnetti ovunque" e una lista dispositivi, conferma che ogni dispositivo mappa a una sessione distinta o a un record di refresh token che puoi revocare.
Esempio: un portale clienti con account del personale e logout forzato
Immagina una piccola azienda con un portale clienti web (i clienti controllano fatture, aprono ticket) e un'app mobile per il personale sul campo (lavori, note, foto). Il personale a volte lavora in cantine senza segnale, quindi l'app deve funzionare offline per un po'. Gli admin vogliono anche un grande pulsante rosso: se un tablet viene perso o un appaltatore se ne va, possono forzare un logout.
Aggiungi tre minacce comuni: tablet condivisi nei furgoni (qualcuno dimentica di disconnettersi), phishing (un membro dello staff inserisce le credenziali in una pagina fasulla) e un occasionale bug XSS nel portale (uno script gira nel browser e tenta di rubare tutto ciò che può).
Un setup pratico qui è token di accesso a breve durata più refresh token rotanti, con revoca server-side. Ti dà chiamate API veloci e tolleranza offline, permettendo comunque agli admin di tagliare gli accessi.
Ecco come potrebbe apparire:
- Durata access token: 5–15 minuti.
- Rotazione refresh token: ad ogni refresh viene restituito un nuovo refresh token e quello precedente è invalidato.
- Memorizza i refresh token in modo sicuro: sul web, tieni il refresh token in un cookie HttpOnly e Secure; sul mobile, tienilo nello storage sicuro del sistema operativo.
- Traccia i refresh token server-side: memorizza un record del token (utente, dispositivo, ora di emissione, ultimo uso, flag revoked). Se un token ruotato viene riutilizzato, trattalo come furto e revoca l'intera catena.
Il logout forzato diventa applicabile: l'admin revoca il record del refresh token per quel dispositivo (o tutti i dispositivi per quell'utente). Il dispositivo rubato può continuare a usare l'access token corrente fino alla sua scadenza, ma non può ottenerne uno nuovo. Quindi il tempo massimo per tagliare completamente l'accesso è la durata del tuo access token.
Per un dispositivo perso, definisci la regola in linguaggio semplice: "Entro 10 minuti, l'app smetterà di sincronizzare e richiederà di effettuare nuovamente l'accesso." Il lavoro offline può rimanere sul dispositivo, ma la successiva sincronizzazione online dovrebbe fallire finché l'utente non effettua il login.
Prossimi passi: implementare, testare e mantenere la soluzione
Scrivi cosa significa "logout" in linguaggio di prodotto semplice. Per esempio: "Il logout rimuove l'accesso su questo dispositivo", "Il logout ovunque espelle tutti i dispositivi entro 1 minuto" o "La modifica della password disconnette le altre sessioni". Quelle promesse decidono se hai bisogno di stato sessione server-side, liste di revoca o token a breve durata.
Trasforma le promesse in un piccolo piano di test. I bug su token e sessioni spesso sembrano a posto nei demo, poi falliscono nella vita reale (modalità sospensione, reti instabili, dispositivi multipli).
Checklist di test pratica
Esegui test che coprano i casi più disordinati:
- Scadenza: l'accesso si interrompe quando l'access token o la sessione scade, anche se il browser rimane aperto.
- Revoca: dopo "disconnetti ovunque", la vecchia credenziale fallisce alla richiesta successiva.
- Rotazione: la rotazione del refresh token emette un nuovo refresh token e invalida quello vecchio.
- Rilevamento riuso: riprodurre un vecchio refresh token scatena una risposta di blocco.
- Multi-dispositivo: le regole per "solo dispositivo corrente" vs "tutti i dispositivi" sono applicate e l'interfaccia corrisponde.
Dopo i test, fai un semplice esercizio di attacco con il tuo team. Scegli tre scenari e percorri tutti i passaggi: un bug XSS che può leggere token, un tentativo CSRF contro sessioni con cookie e un telefono rubato con una sessione attiva. Stai verificando se il design corrisponde alle promesse.
Se devi muoverti in fretta, riduci il codice personalizzato. AppMaster (appmaster.io) è una opzione quando vuoi un backend generato e pronto per la produzione più app web e native, così puoi mantenere regole come scadenza, rotazione e logout forzato coerenti tra i client.
Programa una revisione di follow-up dopo il lancio. Usa ticket di supporto reali e incidenti per aggiustare timeout, limiti di sessione e comportamento di "disconnetti ovunque", poi riesegui la stessa checklist così le correzioni non regrediscano silenziosamente.


