Vai al contenuto
Allin

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

Daniel OkonkwoSviluppatore front-end e redattore Tech presso Allin

Performance web · Formati di file

Verificato su 6 fonti

Vedi il profilo
In breve

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.

Le tre parti di un token HS256 reale da 264 caratteri: che cosa contiene ciascuna, chi può leggerla e che cosa copre la firma
ParteContenutoDimensione in questo tokenLeggibile senza chiave?Coperta dalla firma?
Headeralg e typ — quale algoritmo l'ha firmato36 caratteriSì, del tuttoSì — ma il verificatore non deve fidarsi ciecamente di alg
PayloadI claim: sub, iss, exp, iat, jti e ciò che aggiungi183 caratteri per 137 byte di JSONSì — è il punto che quasi tutti mancanoSì — non è modificabile senza la chiave
FirmaHMAC-SHA-256 su header.payload, o una firma RSA/ECDSA43 caratteri per 32 byte grezziSì, ma da sola non significa nullaÈ la firma
Che cosa mancaLa riservatezza e ogni modo di annullare il token in anticipoZero byte sono spesi per questoNon applicabileUsa JWE per la prima, un token di rinnovo o una lista di negazione per il secondo
Generatore di JWTCostruisci e firma con HMAC un JSON Web Token nel browser — HS256, HS384 o HS512.Prova lo strumento

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
SpiegazioneCos'è un JWT (JSON Web Token)?Un JWT è un token compatto e firmato che trasporta identità tra servizi. Ecco le sue tre parti, l'uso per l'autenticazione e i limiti di sicurezza.SpiegazioneEntropia delle password: che cosa un misuratore di robustezza non può sapereL'entropia misura il processo che ha prodotto una password, non i caratteri che contiene. H = L x log2(R) è vera solo quando ogni carattere è stato scelto davvero a caso — ed è esattamente per questo che un misuratore che valuta una password inventata da un umano in base alle classi di caratteri sta misurando la cosa sbagliata.ConfrontoMD5, SHA-1, SHA-256: quale hash, e per che cosaMD5 è rotto e MD5 va benissimo, a seconda di quale delle tre proprietà di sicurezza ti serviva. Ecco che cosa significano davvero resistenza alle collisioni, alla seconda preimmagine e alla preimmagine, quale algoritmo conserva quale, e perché nessuno di essi deve avvicinarsi a una password.SpiegazioneCos'è una funzione di hash? (MD5, SHA-256)Una funzione di hash trasforma qualsiasi input in un'impronta di dimensione fissa. Ecco cosa fa, le sue proprietà chiave, gli usi comuni e quali algoritmi sono sicuri.SpiegazioneCos'è un UUID (e quando usarlo)?Un UUID è un identificatore a 128 bit unico senza autorità centrale. Ecco come appare, perché è utile, le versioni e quando usarne uno.GuidaCostruire un URL con parametri che sopravvive a un copia-incollaTre codifiche, una differenza visibile: %20 o +. La modalità modulo del generatore riproduce URLSearchParams byte per byte su diciassette valori — ma dagli un URL di base con un frammento e ogni parametro finisce dentro l'hash, dove nessun server lo vede.

Strumenti correlati

Fonti

Hai notato un errore in questo articolo?