Qué contiene en realidad un JSON Web Token
Un JSON Web Token (JWT) son tres bloques de texto unidos por puntos. Cada bloque está codificado en Base64url, y los dos primeros son simplemente JSON. Cualquiera que tenga el token puede leer lo que hay dentro. En un JWT normal no hay nada cifrado.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 <- header
eyJzdWIiOiIxMjM0NSIsIm5hbWUiOiJBbGV4In0 <- payload
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFW <- signature
Unidos por puntos, eso es el token que pegas en una petición. Decodifica las dos primeras partes y obtienes JSON corriente:
{ "alg": "HS256", "typ": "JWT" }
{ "sub": "12345", "name": "Alex" }
Una carga útil real suele llevar también marcas de tiempo y una audiencia:
{
"iss": "https://auth.example.com",
"sub": "12345",
"aud": "api.example.com",
"iat": 1756000000,
"exp": 1756003600,
"roles": ["editor"]
}
La cabecera
La cabecera describe cómo está firmado el token. Hay dos campos que importan por encima del resto:
algnombra el algoritmo de firma, por ejemploHS256(HMAC con SHA-256, usando un secreto compartido) oRS256(firma RSA con SHA-256, firmada con una clave privada y comprobada con una pública).kid, el identificador de clave, es opcional y le dice al receptor cuál de varias claves se usó, de modo que las claves se pueden rotar sin romper los tokens ya emitidos.
typ suele ser simplemente JWT y no tiene ningún significado de seguridad.
La carga útil y sus claims
La carga útil es un objeto JSON de claims, que es sencillamente la palabra que usa la especificación para las afirmaciones sobre el sujeto del token. Algunos nombres de claim están registrados por el estándar, y todos son cortos:
| Claim | Nombre | Qué contiene |
|---|---|---|
iss | Emisor | Quién creó el token, normalmente una URL o el nombre de un servicio |
sub | Sujeto | De quién trata el token, normalmente un identificador de usuario estable |
aud | Audiencia | Para quién está destinado el token, para que un servicio pueda rechazar tokens dirigidos a otro |
exp | Hora de expiración | El instante a partir del cual el token debe rechazarse |
nbf | No antes de | El instante antes del cual el token debe rechazarse |
iat | Emitido en | Cuándo se creó el token |
jti | Identificador del JWT | Un identificador único, útil para rastrear o bloquear tokens concretos |
exp, nbf e iat son todos valores NumericDate, es decir, segundos desde el 1 de enero de 1970 UTC. Son segundos, no milisegundos. Si una marca de tiempo se decodifica como una fecha del año 57.000, estás leyendo milisegundos y tienes que dividir entre mil.
Todo lo demás de la carga útil es lo que haya puesto ahí el emisor: roles, permisos, una dirección de correo, un identificador de inquilino. Como el nombre de un claim privado podría chocar algún día con uno registrado, los emisores suelen darles un espacio de nombres propio con un prefijo con forma de URL.
La firma
La firma se calcula sobre el texto exacto de las dos primeras partes unidas por un punto, usando el algoritmo de la cabecera y una clave. Cambia un solo carácter de la cabecera o de la carga útil y la firma deja de coincidir.
Con HS256 el mismo secreto sirve para firmar y para verificar, así que cualquiera que pueda comprobar un token también puede acuñar uno. Con RS256 o ES256 una clave privada firma y una clave pública verifica, que es lo que permite a un proveedor de identidad repartir tokens que muchos servicios distintos pueden validar sin guardar ningún secreto.
Base64url no es cifrado
Base64url es la misma idea que Base64, con dos diferencias: usa - y _ en lugar de + y / para que el resultado sea seguro dentro de una URL, y normalmente se elimina el relleno final de =. Es una codificación, una manera reversible de escribir bytes como texto. No aporta ninguna confidencialidad en absoluto.
Así que no pongas nunca nada sensible en la carga útil de un JWT: ni contraseñas, ni números de tarjeta, ni notas internas sobre el usuario. Da por hecho que quien tenga el token, y cualquiera que lo lea de un archivo de registro o del almacenamiento del navegador, puede ver todos los campos.
Existe un formato aparte, JWE, que sí cifra la carga útil, y tiene cinco partes en lugar de tres. Si tu token tiene tres partes, está firmado y es legible.
Decodificar no es verificar
Este es el punto que importa. Un decodificador te enseña lo que dice un token. No te dice si el token es auténtico. La verificación es un paso aparte y necesita la clave.
Para confiar de verdad en un token, un servidor tiene que:
- Comprobar la firma contra la clave correcta.
- Exigir el algoritmo que espera, en lugar de fiarse del campo
algdel token. Dos ataques clásicos vienen de saltarse esto: poneralganoney quitar la firma, y coger la clave pública RSA de un servicio y usarla como secreto HMAC para engañar a un verificador deRS256y que ejecuteHS256. - Comprobar
expy, si está presente,nbf, permitiendo un pequeño desfase de reloj. - Comprobar que
issyaudcoinciden con lo que espera este servicio.
Solo entonces los claims significan algo. Hasta ese momento, la carga útil es una cadena sin verificar que llegó por la red.
Notas prácticas
- Los tokens se envían normalmente en una cabecera HTTP como
Authorization: Bearer <token>, así que mantén la carga útil pequeña. Cada claim que añades se envía en cada petición. - Un token firmado sigue siendo válido hasta que expira. No hay ninguna forma incorporada de revocarlo, y por eso los tokens de acceso suelen tener vidas cortas, a menudo de minutos, con un token de refresco aparte que sirve para obtener otros nuevos.
- Si un token parece mal formado, cuenta primero los puntos. Dos puntos y tres partes es un JWT firmado normal. Un token truncado al pegarlo desde una terminal es una causa muy habitual de «firma no válida».
- Decodificar un token para leer su expiración, su sujeto o sus roles mientras depuras es completamente seguro y no requiere ningún secreto. Para eso está exactamente la carga útil.