Un JWT té aspecte de cosa xifrada: tres blocs de lletres i números sense sentit separats per punts. Aquest aspecte és l'arrel de la meitat dels errors. Hi ha aplicacions que hi posen dades personals convençudes que ningú no les llegeix, servidors que accepten un token sense signatura perquè la capçalera diu que no cal, i codi que «comprova» el token llegint-lo sense verificar-lo.
Cap d'aquests errors no trenca la criptografia. Tots són errors de confiança: el servidor es creu una cosa que li ha dit el client. El laboratori «Autenticació i JWT» els posa en cinc sales, amb tokens que es signen i es verifiquen de debò dins la pàgina, perquè l'error es vegi en lloc d'explicar-se.
A qui va adreçat
| Formació | material del Curs d'especialització en Ciberseguretat en entorns de les TI; encaixa en la part de seguretat web i autenticació segura |
| Requisits | saber què és una petició HTTP i una capçalera, i llegir una mica de JavaScript |
| Durada | uns 20 minuts de joc; amb la posada en comú, una sessió |
| Format | individual, sense compte enrere, cinc sales i 1300 punts possibles |
| Idiomes | ES · CA · EN |
La teoria
Què és un JWT
Un JSON Web Token (RFC 7519) és una manera compacta de transportar un conjunt d'afirmacions —claims— entre dues parts. En la forma habitual, signada, són tres parts separades per punts:
capçalera.càrrega.signatura
| Part | Què porta | Exemple |
|---|---|---|
| Capçalera | el tipus i l'algorisme de signatura | {"alg":"HS256","typ":"JWT"} |
| Càrrega (payload) | els claims: qui és, què pot fer, fins quan | {"sub":"42","role":"user","exp":1767225600} |
| Signatura | el resultat de signar capçalera.càrrega amb una clau | 32 bytes en HS256 |
Capçalera i càrrega són JSON codificat en Base64url (RFC 4648, secció
5): el Base64 de sempre amb - i _ en lloc de + i /, i sense el
farciment =. És una codificació, no un xifratge: converteix bytes en
text que es pot posar en una URL o en una capçalera, i es desfà sense cap
clau.
Els claims registrats que cal conèixer:
| Claim | Significat |
|---|---|
iss | qui ha emès el token |
sub | de qui parla el token |
aud | a qui va destinat |
exp | a partir de quan deixa de valer |
nbf | abans de quan encara no val |
iat | quan es va emetre |
jti | identificador únic, útil per revocar |
Les dates són NumericDate: segons des de l'1 de gener de 1970 en UTC, no mil·lisegons.
Signar no és xifrar: JWS i JWE
El JWT és el format de la càrrega; el que la protegeix és una altra especificació:
- JWS (RFC 7515) signa. Garanteix integritat i autenticitat: ningú sense la clau no pot canviar la càrrega sense que la signatura deixi de quadrar. No garanteix confidencialitat: qualsevol que tingui el token el llegeix.
- JWE (RFC 7516) xifra. És un format diferent, de cinc parts, i es fa servir molt menys.
Els algorismes són a l'RFC 7518 (JWA). Els dos grans grups:
| Família | Exemples | Clau per signar | Clau per verificar |
|---|---|---|---|
| HMAC (simètric) | HS256, HS384, HS512 | un secret compartit | el mateix secret |
| Signatura asimètrica | RS256, PS256, ES256, EdDSA | la clau privada | la clau pública |
Conseqüència pràctica: a la càrrega d'un JWT signat no hi va res que l'usuari no pugui veure. Si cal confidencialitat, JWE o, molt més senzill, un token opac que sigui només una referència a dades desades al servidor.
alg: none i la confusió d'algorismes
L'RFC 7518 defineix el valor none per a un JWS sense protecció,
pensat per a contextos on la integritat ja està garantida per un altre
mitjà. Una biblioteca que llegeix el camp alg de la capçalera i hi obeeix
pot acceptar un token sense signatura com si fos vàlid. El problema de fons
és que la capçalera l'escriu el client: deixar-li decidir com es
verifica és preguntar al sospitós si cal escorcollar-lo.
La mateixa família inclou la confusió d'algorismes. Un servidor que
espera RS256 verifica amb la clau pública. Si accepta també HS256 i fa
servir «la clau que tingui configurada», l'atacant pot signar amb HMAC fent
servir com a secret la clau pública, que per definició coneix.
La defensa és la mateixa per a totes dues: l'algorisme el fixa el
servidor, amb una llista blanca, i no es dedueix mai del token. Si es fa
servir kid per triar la clau, es valida contra un conjunt tancat; mai com
a ruta de fitxer ni dins d'una consulta. L'RFC 8725 (JSON Web Token
Best Current Practices) recull aquestes regles i és la lectura obligada
després del laboratori.
Secrets febles i entropia
Un token HS256 es pot atacar fora de línia: amb un token vàlid a la mà,
l'atacant prova secrets candidats fins que la signatura quadra, sense fer
cap petició al servidor. Per això l'RFC 7518 exigeix per a HMAC una clau
com a mínim tan llarga com la sortida del hash: 256 bits per a HS256.
Una paraula, una frase o el nom del projecte no ho són.
El mateix val per als tokens opacs, els identificadors de sessió que el servidor busca a la seva taula: valen exactament el que val la seva entropia, és a dir, quants valors igual de probables podrien haver sortit.
| Com es genera | Entropia real |
|---|---|
| comptador, autoincrement | gairebé zero: es prediu el següent |
marca de temps, Date.now() | uns quants bits si se sap l'hora aproximada |
| hash del correu o de l'id d'usuari | la de la dada d'origen, que sol ser pública |
Math.random() | no és un generador criptogràfic: el seu estat es pot reconstruir |
| CSPRNG, 16 bytes | 128 bits |
| CSPRNG, 32 bytes | 256 bits |
La guia d'OWASP sobre gestió de sessions demana almenys 64 bits
d'entropia; 128 bits o més és el marge còmode i el que demana el
laboratori. Per fer-se'n una idea: 2¹²⁸ són uns 3,4 × 10³⁸ valors. A un
bilió d'intents per segon, recórrer-ne la meitat portaria de l'ordre de
5 × 10¹⁸ anys. A Node es generen amb crypto.randomBytes(32); al
navegador, amb crypto.getRandomValues().
I una regla d'implementació: els tokens i les signatures es comparen en
temps constant (crypto.timingSafeEqual a Node), perquè el temps de
resposta no expliqui quants caràcters coincidien.
Autenticar a cada ruta, autoritzar a cada acció
Autenticar és saber qui ets; autoritzar és decidir què pots fer.
Totes dues coses han de passar al servidor i a totes les rutes. L'error
més comú no és criptogràfic: és una ruta a la qual ningú no va posar el
guardià. Rutes de depuració, /v1/ oblidada quan es va publicar /v2/,
mètriques, webhooks, panells d'administració «que ningú no coneix».
La causa és el model per defecte. Si el router permet i la protecció s'afegeix ruta a ruta, cada ruta nova és una ocasió d'oblit. L'alternativa és denegar per defecte: un middleware global i una llista blanca explícita de rutes públiques, amb un test que recorri l'arbre de rutes i falli si n'hi ha alguna que no sigui ni protegida ni a la llista.
A l'OWASP API Security Top 10 (2023) això apareix repartit en diverses entrades: API2 (autenticació trencada), API5 (autorització a nivell de funció trencada), API8 (configuració de seguretat incorrecta) i API9 (inventari d'API mal gestionat). A l'OWASP Top 10 d'aplicacions web de 2021, la primera posició és precisament el control d'accés trencat (A01).
decode() davant de verify()
A la biblioteca jsonwebtoken de Node, jwt.decode() només llegeix:
parteix el token, desfà el Base64url i retorna la càrrega. No comprova la
signatura, ni la caducitat, ni res. jwt.verify() recalcula la signatura
amb la clau, comprova l'algorisme i els claims temporals, i només llavors
retorna la càrrega.
// malament: la càrrega és text que envia el client
const u = jwt.decode(token);
// bé: signatura, algorisme i claims comprovats
const u = jwt.verify(token, CLAU, {
algorithms: ['HS256'], issuer: 'la-meva-api', audience: 'la-meva-app'
});
Compte amb els noms, que no són universals: a PyJWT la funció es diu
jwt.decode() i sí que verifica quan se li passen la clau i els
algorismes, tret que es desactivi explícitament. El que cal mirar no és el
nom, sinó si la signatura es comprova.
I cap validació al navegador no compta com a control de seguretat: el client és a les mans de qui el fa servir. És experiència d'usuari, res més.
Caducitat, revocació i claims
Un JWT signat és autocontingut: el servidor no necessita consultar res per acceptar-lo. Aquest és el seu avantatge i el seu problema, perquè tampoc no hi ha on revocar-lo. La pràctica habitual:
- Caducitat curta al token d'accés (
exp), de minuts. - Un token de refresc desat i revocable al servidor, que es rota a cada ús; si n'apareix un de ja usat, s'invalida tota la família.
- Si cal revocar abans d'hora, una llista de denegació per
jti, que torna una mica d'estat al servidor. - Verificar també
issiaud: una signatura vàlida d'un altre emissor, o un token emès per a una altra aplicació, continua sent un token aliè. - Una tolerància petita de rellotge (uns quants segons) per a
expinbf, no minuts.
On es desa el token al navegador
Galeta HttpOnly; Secure; SameSite | localStorage | |
|---|---|---|
| El llegeix JavaScript? | no | sí, qualsevol script de la pàgina |
| Davant d'un XSS | l'atacant pot fer peticions, però no endur-se el token | se l'endú |
| Davant d'un CSRF | el navegador l'envia sola: cal SameSite (i token anti-CSRF si és None) | no s'envia sol |
| S'envia | automàticament al domini | a mà, a Authorization: Bearer |
Per a una aplicació web amb el seu propi backend, l'opció per defecte
raonable és una galeta HttpOnly, Secure i SameSite=Lax o Strict,
amb el prefix __Host- si és possible. Cap opció no arregla un XSS: només
canvia el que l'atacant es pot endur.
Referències
- RFC 7519 (JWT), 7515 (JWS), 7516 (JWE), 7517 (JWK), 7518 (JWA) i 8725 (bones pràctiques).
- OWASP ASVS, capítols d'autenticació, gestió de sessions i control d'accés; la versió 5.0 en dedica un de propi als tokens autocontinguts.
- OWASP Cheat Sheet Series: Session Management, JSON Web Token i Authorization.
- OWASP API Security Top 10 (2023).
Com funciona el laboratori
Entres en un edifici abandonat com a Mode Déu: pots veure l'interior dels sistemes. Cada sala té un repte i, quan el resols, una revelació amb l'explicació i una fitxa de defensa amb què cal fer al teu codi.
- El Cofre Transparent. Signat no és xifrat: què hi ha realment dins d'un JWT i qui ho pot llegir.
- La Signatura Trencada. Una farga de tokens i dos servidors, un d'antic i un de ben configurat: què passa quan el servidor obeeix la capçalera.
- La Clau de Guarda-roba. Tokens opacs i entropia: un generador feble davant d'un de criptogràfic, i per què la diferència és de categoria.
- La Porta sense Guardià. Un mapa de rutes i les seves respostes HTTP: la ruta que es va quedar sense middleware.
- La Sala del Mirall. Dos servidors gairebé idèntics i un token manipulat:
decode()davant deverify().
Cada sala val 200 punts, amb bonus per rapidesa (+60 o +30). Cada intent fallit en resta 15, la pista escrita 50, els Raigs X —el codi intern de la sala— 25 la primera vegada, i el descodificador d'emergència de la primera sala 75. Cap sala no baixa de zero, i el que dedueixis pel teu compte no costa res: pots fer servir la consola del navegador o les eines de hash, HMAC i inspector de JWT de Recursos.
Errors habituals
- «És en Base64, així que està protegit.» Base64 és un format, no un cadenat.
- «Si la signatura no quadra, la biblioteca el rebutjarà.» Només si se li demana que verifiqui, i amb quins algorismes.
- Desar a la càrrega el que l'usuari no hauria de veure: dades personals, claus internes, permisos que no li toca conèixer.
- Fer servir el nom del projecte com a secret HMAC, o el mateix a desenvolupament i a producció, o deixar-lo al repositori.
- Fabricar identificadors amb
Math.random(), l'hora o un comptador. - Protegir les rutes una a una i confiar que ningú no se n'oblidi cap.
- Confondre autenticació amb autorització: estar identificat no vol dir poder-ho fer tot.
- Validar al client i donar la feina per feta.
- Tokens que no caduquen, o que caduquen en dies.
Notes per al professorat
- És individual i dura uns 20 minuts. Funciona bé com a entrada d'una sessió de seguretat web: primer es juga i després s'explica, amb l'experiència d'haver forjat un token encara fresca.
- No hi ha xarxa: és una sola pàgina HTML amb la seva pròpia implementació de SHA-256 i HMAC-SHA256. Es pot obrir des d'un llapis USB en una aula sense connexió, i no envia res enlloc.
- El projector serveix per a la posada en comú: obrir les fitxes de defensa de cada sala i llegir-les contra un fragment de codi real de l'alumnat o d'un projecte de classe.
- La puntuació premia no demanar ajuda, però l'ajuda no és trampa. Si algú acaba amb molta penalització, que hi torni intentant pagar menys.
- Per a alumnat que programa, una ampliació natural és buscar en un projecte propi on s'emet i on es verifica el token, amb quin algorisme i amb quin secret.
Preguntes per a la posada en comú:
- Quina part d'un JWT pot llegir qualsevol, i quina part només la pot produir qui té la clau?
- Per què l'error d'
alg: noneno és un error de criptografia? - Què ha de passar en un projecte perquè una ruta nova neixi protegida sense que ningú se n'hagi de recordar?
- Si la teva aplicació desa el token a
localStorage, quines conseqüències té un XSS? I si el desa en una galetaHttpOnly?