Un JSON Web Token (JWT) es una cadena compacta de tres partes separadas por puntos —cabecera, payload y firma— codificadas en Base64URL, definida en el RFC 7519. Se usa para transmitir de forma autocontenida la identidad de un usuario y sus claims entre un servidor de autenticación y las APIs que confían en él, sin necesidad de consultar una base de datos de sesiones en cada petición. El riesgo habitual no está en el formato sino en su uso: confundir codificación con cifrado (el payload de un JWT es legible por cualquiera, no está cifrado), aceptar el algoritmo `none`, o guardar secretos débiles para firmar con HS256. Este generador y decodificador de JWT construye y valida tokens de ejemplo en el navegador, sin enviar la clave secreta a ningún servidor.
Estructura de un JWT: header, payload y firma
Un JWT real como eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c3JfOGYyMWEiLCJuYW1lIjoiTGF1cmEgR29tZXoiLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3MTcwMDAwMDAsImV4cCI6MTcxNzAwMzYwMH0.mxmoQPxXFdvX5naVSDSB0G_xP1WWXNyyOw2JMYr4jfc se descompone en tres bloques. El primero decodifica a {"alg":"HS256","typ":"JWT"}, el segundo a {"sub":"usr_8f21a","name":"Laura Gomez","role":"admin","iat":1717000000,"exp":1717003600} y el tercero es la firma HMAC-SHA256 calculada sobre header.payload con una clave secreta compartida. Cualquiera puede decodificar Base64URL y leer el payload sin conocer la clave; lo que la firma garantiza es que el contenido no se ha modificado desde que el emisor lo firmó, no que sea confidencial.
Claims estándar: iss, sub, aud, exp, nbf, iat
El RFC 7519 define claims registrados que no son obligatorios pero sí interoperables entre librerías: iss identifica al emisor, sub al sujeto (normalmente el ID de usuario), aud la audiencia esperada (qué API debe aceptar este token), exp el timestamp Unix de expiración, nbf el instante a partir del cual es válido ("not before") y iat cuándo se emitió. Un servidor de recursos que valide un JWT debe comprobar exp y, si aplica, aud, porque un token válido para una API no debería aceptarse en otra sin verificación explícita. Los claims personalizados (role, permissions, tenant_id) se añaden libremente al payload, pero conviene mantenerlo pequeño porque viaja en cada petición como cabecera Authorization.
HS256 frente a RS256: cuándo usar cada algoritmo
HS256 firma y verifica con la misma clave secreta simétrica: es rápido y sencillo, pero exige que todo servicio que valide el token conozca ese secreto, lo que amplía la superficie de exposición si hay varios microservicios. RS256 (y su variante moderna EdDSA con Ed25519) usa un par de claves asimétricas: el emisor firma con la clave privada y cualquier servicio puede verificar con la clave pública, sin necesidad de compartir ningún secreto. En arquitecturas de microservicios o cuando terceros deben validar tus tokens sin poder emitirlos, RS256 o EdDSA es la opción correcta; HS256 basta para un backend monolítico donde emisor y verificador son el mismo proceso.
El fallo del algoritmo none y la confusión de claves
Una vulnerabilidad histórica en librerías JWT permitía tokens con alg: none, donde el emisor declara que no hay firma y algunas implementaciones aceptaban el token igualmente sin verificar nada. Otra variante, la confusión de algoritmo, ocurre cuando un servidor configurado para verificar RS256 recibe un token con alg: HS256 y, si la librería es vulnerable, usa la clave pública RSA (que es pública y conocida) como si fuera el secreto HMAC, permitiendo a un atacante firmar tokens arbitrarios. La mitigación es siempre la misma: el verificador debe fijar explícitamente qué algoritmo espera y rechazar cualquier token que declare uno distinto, nunca confiar en el campo alg del propio token.
Expiración corta y refresh tokens
Como un JWT es autocontenido y normalmente no se consulta contra una lista de revocación, no se puede invalidar de forma inmediata sin infraestructura adicional. La práctica estándar es emitir access tokens de vida corta (5-15 minutos) firmados como JWT, junto con un refresh token opaco de vida larga almacenado en base de datos, que sí se puede revocar. Cuando el access token expira, el cliente lo renueva contra un endpoint de refresh sin pedir credenciales de nuevo. Esto acota el riesgo de un access token robado: como mucho es válido durante los minutos que le queden antes de expirar.
Dónde guardar el JWT en el navegador
Guardar el JWT en localStorage lo expone a cualquier script que consiga inyectarse vía XSS, porque JavaScript tiene acceso de lectura completo. Una cookie con HttpOnly, Secure y SameSite=Strict no es legible desde JavaScript y reduce ese vector, a cambio de necesitar protección CSRF explícita si el backend acepta peticiones mutadoras basadas en cookie. Para SPA que consumen una API propia bajo el mismo dominio, la combinación de cookie HttpOnly para el refresh token y el access token en memoria (una variable de JavaScript, no persistida) es el equilibrio más razonable entre seguridad y experiencia de usuario.
Validar la firma sin exponer la clave: JWKS
Cuando se usa RS256 o EdDSA, el emisor publica sus claves públicas en un endpoint JWKS (JSON Web Key Set), normalmente en /.well-known/jwks.json, y cada kid (key ID) del header del token identifica cuál de las claves publicadas usar para verificar. Esto permite rotar la clave de firma sin coordinación manual: el emisor publica la clave nueva junto a la antigua durante un periodo de solape, firma los tokens nuevos con la nueva y los verificadores la descubren automáticamente a través del JWKS. Proveedores de identidad como los que implementan OpenID Connect exponen siempre este endpoint junto al issuer en su documento de descubrimiento.
Salidas reales de ejemplo
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c3JfOGYyMWEiLCJuYW1lIjoiTGF1cmEgR29tZXoiLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3MTcwMDAwMDAsImV4cCI6MTcxNzAwMzYwMH0.mxmoQPxXFdvX5naVSDSB0G_xP1WWXNyyOw2JMYr4jfcToken de ejemplo generado con la clave `clave-secreta-super-larga-y-aleatoria-2024`; cualquier decodificador Base64URL revela el payload sin necesidad de conocer la clave.
{
"sub": "usr_8f21a",
"name": "Laura Gomez",
"role": "admin",
"iat": 1717000000,
"exp": 1717003600
}El decodificador sólo aplica Base64URL inverso; no verifica la firma a menos que se le proporcione la clave.
$ echo -n 'header.payload' | openssl dgst -sha256 -hmac 'clave-secreta-super-larga-y-aleatoria-2024'
HMAC-SHA256(stdin)= 9c9e0b1f0a5d7f2c8b0e1a4d7f9c2b5e8a1d4f7c0b3e6a9d2f5c8b1e4a7d0f3c
$ echo -n 'header.payload' | openssl dgst -sha256 -hmac 'clave-incorrecta'
HMAC-SHA256(stdin)= 3a7f2c9e1b5d8a0f4c7e2b9d5a8f1c4e7a0d3f6c9b2e5a8d1f4c7b0e3a6d9f2cCon la clave correcta la firma calculada coincide byte a byte; con una clave distinta, incluso de un solo carácter, el resultado es completamente diferente.
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"error": "invalid_token",
"error_description": "JWT expired at 2024-05-29T21:00:00Z"
}Respuesta habitual de una API cuando el claim exp ya ha pasado respecto al reloj del servidor.
Algoritmos de firma para JWT
| Algoritmo | Tipo de clave | Rendimiento | Cuándo usarlo |
|---|---|---|---|
| HS256 | Simétrica (secreto compartido) | Muy rápido | Backend único que emite y verifica |
| RS256 | Asimétrica RSA (2048+ bits) | Más lento en firma | Microservicios; terceros deben verificar sin poder firmar |
| EdDSA (Ed25519) | Asimétrica de curva elíptica | Rápido en firma y verificación | Alternativa moderna a RS256 con claves más pequeñas |
Elección según si emisor y verificador son el mismo servicio o entidades distintas.
Casos de uso comunes
- Autenticación stateless entre un frontend SPA y una API REST propia
- Emisión de tokens de acceso de corta duración en un flujo OAuth2 / OpenID Connect
- Comunicación entre microservicios donde cada servicio verifica el token con una clave pública compartida
- Depurar por qué una API rechaza un token de un tercero, comparando claims esperados y recibidos
- Firmar enlaces de un solo uso (invitaciones, verificación de email) con expiración incorporada
Buenas prácticas
- Fija siempre el algoritmo esperado en el verificador; nunca confíes en el campo `alg` del propio token
- Usa expiraciones cortas para access tokens y un refresh token opaco y revocable para renovarlos
- No metas datos sensibles en el payload: es legible por cualquiera, sólo la firma está protegida
- Para RS256/EdDSA, rota las claves con solape usando `kid` y un endpoint JWKS versionado
- Valida `aud` además de `exp` cuando el mismo emisor sirve tokens a varias APIs distintas
- Evita `localStorage` para tokens sensibles; prefiere cookies `HttpOnly` con `SameSite` adecuado