Che cosa contiene davvero un JSON Web Token

Dentro un JWT, con header, payload e firma spiegati, i claim standard, e perché decodificare un token da Base64url non equivale a verificarlo.

Un JSON Web Token (JWT) è fatto di tre blocchi di testo uniti da punti. Ogni blocco è codificato in Base64url, e i primi due sono semplicemente JSON. Chiunque abbia il token può leggere quello che c'è dentro. In un normale JWT non c'è nulla di cifrato.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9      <- header
eyJzdWIiOiIxMjM0NSIsIm5hbWUiOiJBbGV4In0   <- payload
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFW     <- signature

Uniti dai punti, formano il token che incolli in una richiesta. Decodifica le prime due parti e ottieni del normale JSON:

{ "alg": "HS256", "typ": "JWT" }
{ "sub": "12345", "name": "Alex" }

Un payload reale di solito porta con sé anche dei timestamp e un destinatario:

{
  "iss": "https://auth.example.com",
  "sub": "12345",
  "aud": "api.example.com",
  "iat": 1756000000,
  "exp": 1756003600,
  "roles": ["editor"]
}

L'header descrive come il token è firmato. Due campi contano più degli altri:

  • alg indica l'algoritmo di firma, per esempio HS256 (HMAC con SHA-256, che usa un segreto condiviso) oppure RS256 (firma RSA con SHA-256, apposta con una chiave privata e verificata con una pubblica).
  • kid, l'ID della chiave, è facoltativo e dice al destinatario quale fra più chiavi è stata usata, così le chiavi si possono ruotare senza rompere i token già emessi.

typ di solito vale semplicemente JWT e non ha alcun significato dal punto di vista della sicurezza.

Il payload e i suoi claim

Il payload è un oggetto JSON fatto di claim, che è semplicemente il termine usato dalla specifica per le affermazioni sul soggetto del token. Alcuni nomi di claim sono registrati dallo standard, e sono tutti brevi:

ClaimNomeChe cosa contiene
issEmittenteChi ha creato il token, di solito un URL o il nome di un servizio
subSoggettoA chi si riferisce il token, di solito un ID utente stabile
audDestinatarioA chi è destinato il token, così un servizio può rifiutare i token destinati a un altro
expScadenzaL'istante dopo il quale il token va rifiutato
nbfNon prima diL'istante prima del quale il token va rifiutato
iatEmesso ilQuando il token è stato creato
jtiID del JWTUn identificatore univoco, utile per tracciare o bloccare singoli token

exp, nbf e iat sono tutti valori NumericDate, cioè secondi trascorsi dal 1° gennaio 1970 UTC. Sono secondi, non millisecondi. Se un timestamp si decodifica in una data dell'anno 57.000, stai leggendo dei millisecondi e devi dividere per mille.

Tutto il resto nel payload è ciò che l'emittente ci ha messo: ruoli, permessi, un indirizzo email, l'ID di un tenant. Dato che il nome di un claim privato potrebbe un giorno collidere con uno registrato, gli emittenti spesso danno ai propri uno spazio dei nomi con un prefisso simile a un URL.

La firma

La firma viene calcolata sul testo esatto delle prime due parti unite da un punto, usando l'algoritmo indicato nell'header e una chiave. Cambia un solo carattere dell'header o del payload e la firma non corrisponde più.

Con HS256 lo stesso segreto sia firma sia verifica, quindi chiunque possa controllare un token può anche coniarne uno. Con RS256 o ES256 firma una chiave privata e verifica una chiave pubblica, ed è questo che permette a un fornitore di identità di distribuire token che molti servizi separati possono validare senza custodire alcun segreto.

Base64url non è cifratura

Base64url è la stessa idea di Base64, con due differenze: usa - e _ al posto di + e /, così il risultato è sicuro dentro un URL, e il riempimento finale con = di solito viene eliminato. È una codifica, un modo reversibile di scrivere dei byte come testo. Non fornisce alcuna riservatezza.

Quindi non mettere mai nulla di sensibile nel payload di un JWT: niente password, niente numeri di carta, nessuna annotazione interna sull'utente. Dai per scontato che chi possiede il token, e chiunque lo legga da un file di log o dall'archivio del browser, veda ogni singolo campo.

Esiste un formato a parte, JWE, che il payload lo cifra davvero, e ha cinque parti invece di tre. Se il tuo token ha tre parti, è firmato e leggibile.

Decodificare non è verificare

Questo è il punto che conta. Un decodificatore ti mostra che cosa dice un token. Non ti dice se il token è autentico. La verifica è un passaggio separato e richiede la chiave.

Per fidarsi davvero di un token, un server deve:

  1. Controllare la firma rispetto alla chiave corretta.
  2. Pretendere l'algoritmo che si aspetta, invece di fidarsi del campo alg presente nel token. Saltare questo passaggio apre a due attacchi classici: impostare alg a none e togliere la firma, e prendere la chiave pubblica RSA di un servizio e usarla come segreto HMAC, così che un verificatore RS256 venga ingannato ed esegua HS256.
  3. Controllare exp e, se presente, nbf, tollerando un piccolo scarto fra gli orologi.
  4. Controllare che iss e aud corrispondano a quello che questo servizio si aspetta.

Solo a quel punto i claim significano qualcosa. Fino ad allora il payload è una stringa non verificata arrivata dalla rete.

Note pratiche

  • I token si inviano di solito in un header HTTP come Authorization: Bearer <token>, quindi tieni piccolo il payload. Ogni claim che aggiungi viene spedito a ogni richiesta.
  • Un token firmato resta valido finché non scade. Non esiste un modo integrato per revocarlo, ed è per questo che i token di accesso tendono ad avere vite brevi, spesso di qualche minuto, con un token di aggiornamento separato usato per ottenerne di nuovi.
  • Se un token sembra malformato, conta prima di tutto i punti. Due punti e tre parti sono un normale JWT firmato. Un token troncato incollato da un terminale è una causa molto frequente di «firma non valida».
  • Decodificare un token per leggerne la scadenza, il soggetto o i ruoli durante il debug è del tutto sicuro e non richiede alcun segreto. È esattamente a questo che serve il payload.