Che cosa c'è dentro un JWT — e che cosa non protegge
Pubblicato il 06/07/2026 · 16 min di lettura · Strumenti per sviluppatori
Daniel Okonkwo — Sviluppatore front-end e redattore Tech presso Allin
Performance web · Formati di file
Verificato su 6 fonti
Un JSON Web Token sono tre stringhe codificate in base64url unite da punti: header, payload, firma. La firma prova che le prime due parti non sono state alterate da qualcuno senza la chiave. Non le nasconde. Prendi questo token HS256 reale — eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFkYSBMb3ZlbGFjZSIsInJvbGUiOiJlZGl0b3IiLCJpYXQiOjE3ODY1NzkyMDAsImV4cCI6MTc4NjU4MjgwMCwiaXNzIjoiaHR0cHM6Ly9hbGxpbi5leGFtcGxlIiwianRpIjoiYTFiMmMzIn0.bu5qB6d_D3--WUj4b77tnh-wdH6FhKhLcbh2nvNmyl4 — e decodifica la sezione centrale senza alcuna chiave. Ne esce {"sub":"1234567890","name":"Ada Lovelace","role":"editor","iat":1786579200,"exp":1786582800,"iss":"https://allin.example","jti":"a1b2c3"}. Base64url è una codifica, non un cifrario. La prima parte nomina l'algoritmo, la terza è un tag da 43 caratteri sulle prime due, e il token intero è di 264 caratteri che viaggiano a ogni richiesta. Tutto ciò che metti in un payload JWT — indirizzi email, identificatori interni, flag di permesso — è leggibile dal browser, da qualsiasi proxy che registri l'header e da chiunque legga il token su uno schermo. Ne seguono due conseguenze. Non mettere nulla di riservato in un payload; usa JWE se hai davvero bisogno che il contenuto sia nascosto, ed è una specifica diversa. E ricorda che la firma non revoca: un token rubato resta valido finché non passa il suo exp, ed è per questo che i token di accesso di breve durata si accoppiano a un token di rinnovo revocabile.
Un JWT è firmato, non cifrato. Chiunque abbia il token può decodificare il payload e leggerne ogni claim. Ecco un token reale, decodificato senza alcuna chiave, più i tre attacchi che la firma deve fermare e l'unico problema che non può risolvere.
Tre parti, due punti
Ogni JWT ha lo stesso scheletro: header, punto, payload, punto, firma. L'header è un minuscolo oggetto JSON che nomina l'algoritmo — qui {"alg":"HS256","typ":"JWT"}, che in base64url diventa eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. Il payload è un altro oggetto JSON con i claim. La firma è calcolata sulle prime due parti codificate unite dal loro punto, ed è per questo che non puoi mai riordinarle né riformattarle: ciò che è stato firmato sono esattamente quei byte.
La RFC 7519 riserva sette nomi di claim, e conoscerli evita molte reinvenzioni. iss è l'emittente, sub il soggetto, aud il pubblico previsto, exp la scadenza, nbf il momento prima del quale non vale, iat l'ora di emissione e jti un identificatore univoco del token. I tre claim temporali sono valori NumericDate: semplici secondi dall'epoca Unix, non millisecondi, il che è una fonte affidabile di errori di fattore 1000 in JavaScript. Nel token qui sopra iat è 1786579200 ed exp è 1786582800: una differenza di 3 600, dunque un'ora di vita.
La dimensione cresce più in fretta di quanto ci si aspetti. Quel payload sono 137 byte di JSON e diventano 183 caratteri una volta codificati, perché base64 costa sempre quattro caratteri in uscita ogni tre byte in ingresso — un sovrapprezzo del 33 %. Il token intero è di 264 caratteri e viaggia a ogni singola richiesta. Passa a RS256 con una chiave da 2048 bit e la sola firma balza da 32 byte grezzi a 256, cioè 342 caratteri base64url: il token pressoché raddoppia. Infilare un array di permessi nel payload è il modo in cui i team finiscono per sbattere contro i limiti di header dei proxy.
Decodificalo tu stesso: il payload è in chiaro
Copia la sezione centrale del token — la sequenza di caratteri fra i due punti — e decodificala in base64url. Nessuna chiave, nessuna libreria, nessun permesso. Torna indietro {"sub":"1234567890","name":"Ada Lovelace","role":"editor","iat":1786579200,"exp":1786582800,"iss":"https://allin.example","jti":"a1b2c3"}. Ogni claim, in chiaro, ruolo compreso. Se fosse stato un indirizzo email, un numero cliente interno, un livello di abbonamento o un flag di funzionalità, sarebbe altrettanto visibile.
La confusione è comprensibile, perché il token sembra testo cifrato. Non lo è. Base64url esiste per far passare byte arbitrari attraverso canali che tollerano solo un insieme ristretto di caratteri — URL, header, nomi di file. Non c'è chiave, quindi non c'è nulla da tenere segreto né nulla da rompere. La firma protegge l'integrità: se qualcuno modifica il payload, la terza parte non verifica più e il server rifiuta il token. L'integrità non è la riservatezza, e un JWT consegna solo la prima.
La regola pratica segue direttamente. Tratta un payload JWT come una bacheca pubblica che per caso è a prova di manomissione. Mettici identificatori, non segreti. Metti un identificatore utente anziché un'email; un nome di ruolo anziché il ragionamento che c'è dietro; nulla che non stamperesti all'esterno di una busta. E conservalo di conseguenza: un cookie HttpOnly, Secure, SameSite lo mette fuori portata degli script di pagina, mentre localStorage lo consegna a qualunque script riesca a girare sulla tua origin.
Perché base64url e non base64
Il base64 standard usa sessantaquattro caratteri che terminano con + e /, e riempie l'output con = fino a un multiplo di quattro. Tutti e tre sono ostili a URL e header: + significa spazio nei dati di form, / è un separatore di percorso e = è un separatore chiave-valore in una query string. La RFC 4648 definisce quindi un alfabeto adatto alle URL che scambia + con -, / con _ e toglie del tutto il riempimento. È base64url, ed è ciò che usano tutte le parti di un JWT.
La firma nel token qui sopra lo mostra allo scoperto: bu5qB6d_D3--WUj4b77tnh-wdH6FhKhLcbh2nvNmyl4 contiene un trattino basso e tre trattini. Riconvertiti in base64 standard, quegli stessi 32 byte si leggono bu5qB6d/D3++WUj4b77tnh+wdH6FhKhLcbh2nvNmyl4=. Se un giorno decodifichi una parte di JWT con una funzione base64 comune e ottieni spazzatura, è per questo: devi prima riportare - a + e _ a /, e rimettere il riempimento se il decodificatore lo pretende.
Che cosa ferma la firma e che cosa la sconfigge
Cambia un carattere del payload e la firma non corrisponde più, perché HMAC-SHA-256 sulla stringa header.payload modificata produce qualcosa di completamente diverso. Rifirmare il payload manomesso richiede il segreto, che il client non ha. Fin qui funziona. I fallimenti storici non sono attacchi alla crittografia; sono attacchi al verificatore, ed entrambi i classici nascono dal fidarsi dell'header.
Il primo è alg: none. La RFC 7515 definisce un JWS non protetto il cui header recita {"alg":"none"} e la cui parte di firma è semplicemente vuota — il token finisce con un punto nudo. Le librerie che leggevano alg dall'header e smistavano di conseguenza accettavano un token simile e trattavano i suoi claim come verificati. Un attaccante riscrive il payload a piacere, mette alg a none, toglie la firma ed entra. La correzione non è rifiutare la stringa none; è smettere di chiedere al token quale algoritmo usare. Il verificatore sa già quale algoritmo e quale chiave si aspetta, e tutto il resto viene rifiutato prima che il parsing prosegua.
Il secondo è la confusione fra HS256 e RS256, ed è più sottile. Con RS256 il server tiene una chiave privata per firmare e pubblica quella pubblica corrispondente per verificare. Se un verificatore prende l'algoritmo dall'header, un attaccante può mettere alg a HS256 e firmare il token con la chiave pubblica come se fosse un segreto HMAC. Il verificatore esegue allora diligentemente HMAC con quella stessa chiave pubblica — che possiede, perché è pubblica — e il tag corrisponde. Funziona: usare come chiave di HMAC-SHA-256 il testo PEM di una chiave pubblica produce un token che un verificatore ingenuo accetta. Fissare l'algoritmo atteso chiude entrambi gli attacchi in un colpo solo, ed è per questo che la RFC 8725 ne fa una raccomandazione di punta.
Un terzo errore non richiede alcuna crittografia: verificare la firma e poi dimenticare di controllare i claim. Un token strutturalmente valido il cui exp è passato la settimana scorsa resta strutturalmente valido. Lo stesso vale per uno emesso da un altro tenant, o destinato a un altro pubblico. Controlla sempre exp rispetto all'ora corrente con una piccola tolleranza di deriva dell'orologio, iss rispetto all'emittente atteso e aud rispetto al tuo identificatore — la firma dice chi ha scritto i claim, non se si applicano ancora a te.
Il problema che nessuna firma risolve: la revoca
Tutto il fascino di un JWT sta nel fatto che il server non deve consultare nulla. Il token porta con sé i propri claim e la propria prova, quindi qualsiasi nodo con la chiave lo verifica in microsecondi senza toccare un database. È anche la sua debolezza strutturale, e le due cose sono inseparabili: un server che non consulta alcuno stato non può sapere che hai licenziato l'utente cinque minuti fa. Il token resta valido finché non arriva exp, e nulla nella specifica offre un modo per accorciarlo.
La risposta standard è una divisione in due token. Il token di accesso è un JWT con un exp deliberatamente breve — da cinque a quindici minuti è l'intervallo abituale — e viene controllato senza stato a ogni richiesta. Il token di rinnovo è di lunga durata, opaco, conservato lato server e scambiato con un nuovo token di accesso quando quello breve si esaurisce. La revoca avviene sul token di rinnovo, che ha stato ed è quindi annullabile. La finestra di esposizione si riduce alla vita residua del token di accesso, esattamente ciò che exp è stato scelto per limitare.
Se ti serve una revoca più rapida di così, devi reintrodurre stato, e conviene farlo deliberatamente. Una lista di negazione indicizzata sul claim jti consente di annullare singoli token; le voci possono sparire non appena passa l'exp corrispondente, quindi la lista resta piccola. Ruotare la chiave di firma invalida tutti i token in un colpo, ed è lo strumento grossolano di fronte a un sospetto di compromissione della chiave. Entrambi ti costano una consultazione, e a quel punto è legittimo chiedersi se un identificatore di sessione opaco in un cookie non sarebbe stato più semplice fin dall'inizio.
JWE è l'altra specifica, e non è un'impostazione del JWT
Quando i claim devono davvero essere nascosti, la risposta è JSON Web Encryption, definita nella RFC 7516. Un JWE è una serializzazione diversa con cinque parti anziché tre — header protetto, chiave cifrata, vettore di inizializzazione, testo cifrato e tag di autenticazione — e fornisce riservatezza e integrità insieme, perché usa cifratura autenticata. Puoi annidare le due cose, firmando un JWT e cifrando poi il risultato, che è ciò che la RFC 7519 chiama JWT annidato.
In pratica la maggior parte dei team non ha bisogno di JWE e non dovrebbe ricorrervi per prima cosa. Se il payload contiene qualcosa che preferiresti nessuno leggesse, la risposta giusta di solito è toglierlo dal payload. Sostituisci il valore sensibile con un identificatore opaco che il server delle risorse sappia risolvere, e il problema di riservatezza sparisce insieme alla gestione di chiavi in più, alla superficie di libreria in più e ai modi di guasto in più. Ricorri a JWE quando un token deve attraversare una parte che lo inoltra senza leggerlo — è il caso per cui è stato progettato.
Una lista di controllo per il verificatore
Fissa l'algoritmo prima di analizzare qualsiasi cosa, e rifiuta un token il cui header non concordi. Verifica la firma con un confronto a tempo costante. Poi controlla exp, e nbf se presente, rispetto all'ora corrente con una tolleranza di deriva non superiore a un minuto. Controlla iss rispetto all'emittente esatto di cui ti fidi e aud rispetto al tuo identificatore. Solo dopo tutto questo dovresti leggere i claim applicativi e, anche allora, tratta role o scope come un'affermazione dell'emittente e non come l'ultima parola: il server delle risorse resta padrone delle proprie decisioni di autorizzazione.
| Parte | Contenuto | Dimensione in questo token | Leggibile senza chiave? | Coperta dalla firma? |
|---|---|---|---|---|
| Header | alg e typ — quale algoritmo l'ha firmato | 36 caratteri | Sì, del tutto | Sì — ma il verificatore non deve fidarsi ciecamente di alg |
| Payload | I claim: sub, iss, exp, iat, jti e ciò che aggiungi | 183 caratteri per 137 byte di JSON | Sì — è il punto che quasi tutti mancano | Sì — non è modificabile senza la chiave |
| Firma | HMAC-SHA-256 su header.payload, o una firma RSA/ECDSA | 43 caratteri per 32 byte grezzi | Sì, ma da sola non significa nulla | È la firma |
| Che cosa manca | La riservatezza e ogni modo di annullare il token in anticipo | Zero byte sono spesi per questo | Non applicabile | Usa JWE per la prima, un token di rinnovo o una lista di negazione per il secondo |
Domande frequenti
- Un JWT è cifrato?
- No. Un JWT standard è firmato, che è una garanzia diversa. Header e payload sono codificati in base64url, una codifica senza chiave e senza segreto, quindi chiunque abbia il token può leggerli. Decodificare il payload del token di esempio di questo articolo senza alcuna chiave restituisce {"sub":"1234567890","name":"Ada Lovelace","role":"editor","iat":1786579200,"exp":1786582800,"iss":"https://allin.example","jti":"a1b2c3"} — ogni claim in chiaro. Ciò che la firma ti compra è l'evidenza di manomissione: cambia un carattere e la terza parte smette di verificare, e il server rifiuta. Se hai davvero bisogno di nascondere il contenuto, JSON Web Encryption (RFC 7516) è la specifica adatta, ed è un formato separato con cinque parti anziché tre, non un flag da attivare su un JWT. Nella maggior parte dei progetti la mossa migliore è tenere i valori riservati fuori dal payload e trasportare un identificatore opaco.
- Si può modificare il payload di un JWT senza il segreto?
- Può cambiare i caratteri, ma il risultato non verificherà — a patto che il tuo verificatore sia scritto correttamente. La firma è calcolata sulla stringa esatta header.payload, quindi ogni modifica produce una discrepanza e il token viene rifiutato. Due difetti del verificatore annullano quella protezione. Il primo è fidarsi del campo alg nell'header: un token che dichiarava {"alg":"none"} con la parte di firma vuota è stato storicamente accettato da librerie che smistavano in base all'header, lasciando all'attaccante la libertà di riscrivere il payload. Il secondo è la confusione HS256/RS256, in cui l'attaccante converte un token asimmetrico in HMAC e lo firma con la chiave pubblica del server stesso, che il verificatore usa poi come segreto HMAC; questo produce davvero un tag coincidente. Entrambi si chiudono con la stessa misura: decidi nel tuo codice l'algoritmo e la chiave attesi prima del parsing, e rifiuta tutto ciò che non concorda. La RFC 8725 lo indica come raccomandazione primaria.
- Come disconnetto un utente se un JWT non è revocabile?
- Cancellare il token sul client termina la sessione per quel browser, e per molti prodotti basta davvero. Non basta se il token può essere stato copiato, perché un token firmato resta valido fino al suo exp qualunque cosa faccia il client. La struttura standard è un JWT di accesso di breve durata — da cinque a quindici minuti — accoppiato a un token di rinnovo opaco di lunga durata conservato lato server. Il logout cancella il token di rinnovo, così la sessione non può essere rinnovata e muore entro la vita residua del token di accesso. Se ti serve più rapidità, aggiungi stato deliberatamente: una lista di negazione indicizzata su jti annulla singoli token, e le voci si possono eliminare appena passa l'exp corrispondente, così non cresce mai senza limite. Ruotare la chiave di firma uccide in un colpo tutti i token in circolazione ed è la risposta giusta a un sospetto di compromissione. Ognuna di queste reintroduce una consultazione, che è il prezzo della revoca che volevi.
- Dove dovrei conservare un JWT in un browser?
- In un cookie marcato HttpOnly, Secure e SameSite, in quasi tutti i casi. HttpOnly mette il token fuori portata di JavaScript, così un difetto di cross-site scripting in qualsiasi punto della tua origin non può leggerlo né spedirlo altrove; Secure lo tiene fuori dalle connessioni in chiaro; SameSite blocca la falsificazione di richiesta cross-site a cui i cookie ti esporrebbero altrimenti. localStorage è l'alternativa comune e la più debole, perché qualsiasi script in esecuzione sulla tua pagina — compreso uno tirato dentro da una dipendenza compromessa — può leggere ogni sua chiave. L'argomento a favore di localStorage è di solito che il token va allegato a mano alle chiamate API cross-origin, cosa che i cookie rendono scomoda; è un vincolo reale, ma conviene risolverlo con un proxy della stessa origin anziché rendendo il token leggibile dagli script. Qualunque cosa scegli, tieni il payload libero da qualsiasi dato sensibile: l'archiviazione decide chi può rubare il token, non chi può leggerlo una volta rubato.
- Quanto dovrebbe durare un JWT?
- Abbastanza breve perché la sua vita non annullabile sia un'esposizione accettabile. Poiché un token firmato resta valido fino a exp qualunque cosa accada dalla tua parte, exp è tutta l'ampiezza della finestra in cui un token rubato funziona ancora. Da cinque a quindici minuti è l'intervallo comune per i token di accesso, e il token di esempio di questo articolo usa un'ora — iat 1786579200, exp 1786582800, una differenza di esattamente 3 600 secondi. Qualsiasi durata misurata in giorni è di fatto una credenziale permanente con una firma attaccata. La vita lunga la porta invece il token di rinnovo, e può essere lunga proprio perché è opaco, conservato lato server e revocabile. Contano due dettagli implementativi. I valori NumericDate sono secondi, non millisecondi, quindi confrontare exp con Date.now() in JavaScript senza dividere per 1000 è il difetto classico. E prevedi una piccola tolleranza di deriva dell'orologio, dell'ordine di trenta-sessanta secondi, o qualche token verrà rifiutato da un server il cui orologio è leggermente avanti rispetto all'emittente.
- Meglio un JWT o un semplice cookie di sessione?
- Per una singola applicazione con un database, un semplice identificatore di sessione in un cookie è di solito la scelta più semplice e più solida. È una stringa opaca casuale, non rivela nulla, e il logout è la cancellazione di una riga con effetto immediato. Il JWT si guadagna la sua complessità quando la verifica deve avvenire dove l'archivio di sessione non è — più servizi dietro un gateway, un'API di terze parti che accetta le tue asserzioni di identità, una funzione all'edge che non può permettersi un viaggio verso il database. È un vantaggio architetturale reale ed è la ragione d'essere dei JWT. Non adottarne uno per un monolite che ha già una tabella di sessioni, e sii onesto sullo scambio: l'assenza di stato ti compra una verifica distribuita rapida e ti costa la revoca istantanea, e non puoi tenerti entrambe. I team che aggiungono una consultazione di lista di negazione a ogni richiesta hanno pagato la complessità del JWT e restituito il suo unico beneficio strutturale.
Articoli che potrebbero interessarti
Tutte le guide →Strumenti correlati
Fonti
Hai notato un errore in questo articolo?