Qué contiene en realidad un JSON Web Token

Por dentro de un JWT, con la cabecera, la carga útil y la firma explicadas, los claims estándar y por qué decodificar un token en Base64url no es lo mismo que verificarlo.

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:

  • alg nombra el algoritmo de firma, por ejemplo HS256 (HMAC con SHA-256, usando un secreto compartido) o RS256 (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:

ClaimNombreQué contiene
issEmisorQuién creó el token, normalmente una URL o el nombre de un servicio
subSujetoDe quién trata el token, normalmente un identificador de usuario estable
audAudienciaPara quién está destinado el token, para que un servicio pueda rechazar tokens dirigidos a otro
expHora de expiraciónEl instante a partir del cual el token debe rechazarse
nbfNo antes deEl instante antes del cual el token debe rechazarse
iatEmitido enCuándo se creó el token
jtiIdentificador del JWTUn 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:

  1. Comprobar la firma contra la clave correcta.
  2. Exigir el algoritmo que espera, en lugar de fiarse del campo alg del token. Dos ataques clásicos vienen de saltarse esto: poner alg a none y quitar la firma, y coger la clave pública RSA de un servicio y usarla como secreto HMAC para engañar a un verificador de RS256 y que ejecute HS256.
  3. Comprobar exp y, si está presente, nbf, permitiendo un pequeño desfase de reloj.
  4. Comprobar que iss y aud coinciden 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.