JWT a fondo:
cómo se firma un token y cómo se rompe una implementación descuidada
Seguridad de aplicaciones web | unfantasmaenelsistema.com
El JSON Web Token se ha convertido en el mecanismo por defecto para mantener la sesión de un usuario en APIs, aplicaciones de una sola página y arquitecturas de microservicios. Su atractivo es evidente: el servidor firma un pequeño paquete de datos, se lo entrega al cliente, y a partir de ahí puede verificar cada petición sin consultar una base de datos de sesiones. Esa misma comodidad es la que lleva a muchos equipos a tratarlo como una caja negra: se copia un fragmento de código de un tutorial, funciona a la primera, y nadie vuelve a mirarlo.
El problema es que la seguridad de un JWT no vive en el token, sino en cómo lo valida quien lo recibe. Una firma es tan buena como la comprobación que se hace de ella, y la especificación deja margen para decisiones desafortunadas. En este artículo desmontamos la estructura de un token, vemos cómo se firma de verdad, y repasamos los fallos clásicos de implementación —todos ellos reproducibles en una API de pruebas propia— junto con la forma correcta de cerrarlos.
Qué aprenderás en este artículo
Cómo se compone un JWT: header, payload y signature en base64url · La diferencia real entre firmar con HS256 y con RS256/ES256 · Los fallos clásicos: alg=none, confusión de algoritmo, secretos débiles y claims sin validar · El problema de la revocación y por qué un JWT no es una sesión · Dónde guardar el token en el cliente y qué ataque habilita cada opción
1. Qué es exactamente un JWT
Un JWT es una cadena de texto formada por tres partes separadas por puntos: header.payload.signature. Las dos primeras son objetos JSON codificados en base64url (una variante de base64 que sustituye los caracteres problemáticos en URLs). La tercera es la firma criptográfica que ata el conjunto.
Conviene tener claro desde el principio que base64url no es cifrado: cualquiera que intercepte el token puede decodificar el header y el payload y leer su contenido en claro. Un JWT firmado (un JWS) garantiza integridad y autenticidad, no confidencialidad. Nunca se debe meter en el payload una contraseña, un número de tarjeta o cualquier dato sensible que no quieras que vea el cliente.
1.1 El header
El header declara el tipo de token y, sobre todo, el algoritmo de firma en el campo alg. Este campo es la fuente de la mitad de los problemas de seguridad de JWT, porque le dice a quien valida qué operación debe hacer para comprobar la firma.
{
"alg": "HS256",
"typ": "JWT"
}1.2 El payload y los claims registrados
El payload contiene los claims: pares clave-valor con la información de la sesión. Algunos nombres están reservados por el estándar (RFC 7519) y tienen un significado fijo que las librerías saben interpretar.
| Claim | Significado | Para qué sirve al validar |
|---|---|---|
| iss | Issuer (emisor) | Comprobar que el token lo emitió tu servidor de autenticación y no otro |
| sub | Subject (sujeto) | Identificador del usuario o entidad a la que pertenece el token |
| aud | Audience (destinatario) | Comprobar que el token estaba destinado a esta API concreta |
| exp | Expiration time | Rechazar el token pasada su fecha de caducidad (timestamp Unix) |
| nbf | Not before | No aceptar el token antes de un instante dado |
| iat | Issued at | Momento de emisión; útil para invalidar tokens anteriores a un cambio |
| jti | JWT ID | Identificador único del token; base para listas de denegación |
1.3 La firma
La firma se calcula sobre la cadena base64url(header) + «.» + base64url(payload). El resultado depende del algoritmo declarado y de una clave. Si alguien cambia un solo carácter del payload —por ejemplo, para ascender su rol de «user» a «admin»— la firma deja de cuadrar y un servidor que valide correctamente rechaza el token.
# Un JWT real, troceado por sus puntos (token de laboratorio)
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 <- header {"alg":"HS256","typ":"JWT"}
eyJzdWIiOiIxMDAxIiwicm9sZSI6InVzZXIiLCJleHAiOjE3ODUwMDAwMDB9 <- payload
S1Xg9m0m9r0k2p4c8n6b3v5z7x1w4q2e8t6y0u9i3o <- signature (HMAC-SHA256)2. HS256 frente a RS256: firma simétrica o asimétrica
La familia HS (HS256, HS384, HS512) usa HMAC con SHA-2 y una única clave secreta compartida. La misma cadena que firma el token es la que lo verifica. Es simple y rápido, pero implica que todo servicio que necesite validar tokens debe conocer el secreto —y quien conoce el secreto puede también emitir tokens válidos.
La familia RS (RSA) y ES (ECDSA) usa criptografía asimétrica: el servidor de autenticación firma con una clave privada que no sale de él, y cualquier otro servicio verifica con la clave pública correspondiente, que se puede distribuir sin riesgo. Es la opción correcta cuando varios servicios independientes consumen los mismos tokens, o cuando el verificador no debería poder emitir.
| Aspecto | HS256 (simétrico) | RS256 / ES256 (asimétrico) |
|---|---|---|
| Clave | Un secreto compartido | Par privada/pública |
| Quién puede emitir | Cualquiera con el secreto | Solo quien tiene la clave privada |
| Quién puede verificar | Cualquiera con el secreto | Cualquiera con la clave pública |
| Distribución de clave | Delicada: hay que proteger el secreto en cada servicio | Sencilla: la pública se publica (p. ej. en un JWKS) |
| Caso de uso ideal | Monolito o un único servicio de confianza | Varios servicios, terceros, OpenID Connect |
| Riesgo específico | Secreto débil o filtrado | Confusión de algoritmo si la validación es laxa |
3. Los fallos clásicos de implementación
Ninguno de los siguientes problemas es un fallo del formato JWT en sí. Todos nacen de validar el token de forma incompleta o de confiar en datos que el cliente controla. Se describen aquí para poder reconocerlos y corregirlos en tu propio código, probándolos contra una API de pruebas que tú controles.
3.1 alg: none
El estándar contempla un algoritmo none, pensado para tokens que ya viajan por un canal de confianza. Si una librería lo acepta sin más, un atacante puede coger un token legítimo, cambiar el header a {«alg»:»none»}, borrar la firma y modificar el payload a voluntad. La defensa es explícita: al validar, pásale a la librería la lista de algoritmos permitidos y asegúrate de que «none» nunca está en ella. Las librerías modernas ya lo rechazan por defecto, pero conviene fijarlo.
3.2 Confusión de algoritmo (RS256 a HS256)
Este es más sutil. Un servicio configurado para RS256 tiene la clave pública a mano para verificar. Si el código de validación decide el algoritmo a partir del header del token en lugar de fijarlo, un atacante puede enviar un token con el header cambiado a HS256 y firmarlo con HMAC usando la clave pública como si fuera el secreto. Como esa clave pública es conocida, la firma cuadra y el servidor la da por buena. La causa raíz es siempre la misma: dejar que el token diga cómo debe validarse.
La regla que evita 3.1 y 3.2 de golpe
El algoritmo de verificación lo decide el servidor por configuración, nunca el campo alg del token que recibe · En la práctica: jwt.verify(token, clave, { algorithms: [‘RS256’] }) y equivalentes · Si el header trae un alg distinto al esperado, el token se rechaza antes de mirar la firma
3.3 Secretos débiles en HS256
Cuando la firma es HMAC, la seguridad del sistema entero se reduce a la entropía del secreto. Un secreto corto o basado en una palabra de diccionario («secret», «changeme», el nombre del proyecto) puede recuperarse offline probando candidatos contra un token capturado, porque verificar un intento no requiere hablar con el servidor. Un secreto HS256 debe ser una cadena aleatoria de al menos 256 bits generada por un CSPRNG y almacenada como cualquier otra credencial: fuera del repositorio, en un gestor de secretos.
3.4 No validar exp, aud e iss
Comprobar la firma solo dice que el token lo emitió alguien con la clave. No dice que siga vigente ni que fuera para ti:
- Sin validar exp, un token robado hace meses sigue funcionando indefinidamente
- Sin validar aud, un token emitido para el servicio A es aceptado por el servicio B, que quizá otorga más privilegios con el mismo identificador
- Sin validar iss, cualquier emisor en el que el sistema confíe para otra cosa puede colar tokens
- Sin margen de reloj (leeway) razonable, un pequeño desfase de hora entre servidores genera falsos rechazos y tienta a desactivar la comprobación
3.5 kid manipulable
El header puede incluir un kid (key ID) que indica qué clave usar de un conjunto. Si el servidor toma ese valor y lo usa para construir una ruta de fichero o una consulta SQL sin sanear, se abren problemas de traversal de directorios o de inyección: el atacante apunta el kid a un fichero cuyo contenido conoce y firma el token con él. El kid es un dato no confiable: trátalo como cualquier entrada de usuario y resuélvelo contra una lista blanca de claves conocidas.
| Fallo | Causa | Mitigación |
|---|---|---|
| alg=none aceptado | La librería no restringe algoritmos | Lista explícita de algoritmos permitidos; nunca «none» |
| Confusión RS256->HS256 | El algoritmo se toma del header del token | Fijar el algoritmo por configuración del servidor |
| Secreto HMAC recuperable | Secreto corto o de diccionario | Cadena aleatoria >=256 bits de un CSPRNG, en gestor de secretos |
| Token caducado aceptado | No se valida exp | Validar exp con un leeway pequeño y controlado |
| Token de otra API aceptado | No se valida aud/iss | Exigir aud e iss concretos en cada servicio |
| Traversal / inyección por kid | kid usado sin sanear | Resolver kid contra una lista blanca de claves |
| Firma no verificada | Se decodifica el payload sin llamar a verify | Usar siempre verify; decode sin verificar es solo para depurar |
4. El problema de la revocación
La ventaja de un JWT —que el servidor no necesita estado para validarlo— es también su mayor inconveniente. Una sesión clásica se borra de la base de datos y deja de valer al instante. Un JWT firmado es válido hasta que caduca, aunque el usuario cierre sesión, cambie la contraseña o su cuenta sea suspendida. No hay forma de «apagar» un token que ya está en circulación sin reintroducir estado.
4.1 Estrategias habituales
- Caducidad corta: tokens de acceso de 5 a 15 minutos. Reducen la ventana de abuso de un token robado sin necesidad de consultar nada
- Token de refresco: un segundo token, de vida larga y guardado de forma más protegida, que solo sirve para pedir nuevos tokens de acceso. Este sí se puede revocar en servidor porque se consulta en cada refresco
- Lista de denegación (denylist): almacenar el jti de los tokens revocados hasta su exp. Vuelve a meter una consulta por petición, pero solo sobre los revocados, no sobre todos
- Rotación de token de refresco: cada uso del token de refresco lo invalida y emite uno nuevo; si un refresco ya usado reaparece, es señal de robo y se corta toda la familia
Un JWT no es una sesión
Si tu aplicación necesita cerrar sesión de verdad, bloquear cuentas al instante o ver quién tiene sesiones abiertas, necesitas estado en el servidor · En ese caso, plantéate si una sesión clásica con cookie no te da menos complejidad y más control que un JWT con toda la maquinaria de refresco y denylist
5. Dónde guardar el token en el cliente
Una vez emitido, el token vive en el navegador, y cada sitio donde puede guardarse tiene un perfil de riesgo distinto. No hay una opción perfecta; hay que elegir contra qué ataque te proteges.
| Ubicación | Riesgo de XSS | Riesgo de CSRF | Notas |
|---|---|---|---|
| localStorage | Alto: cualquier script de la página lee el token | Bajo: no se envía solo | Cómodo para SPAs, pero un XSS roba la sesión entera |
| Cookie HttpOnly + Secure | Bajo: el script no puede leerla | Presente: se envía en cada petición | Necesita SameSite y/o token anti-CSRF |
| Memoria de la aplicación (variable JS) | Medio: se pierde al recargar | Bajo | Se combina con un refresco en cookie HttpOnly |
La combinación que hoy se considera más equilibrada para una SPA: el token de acceso en memoria (se pierde al recargar, ventana de robo pequeña) y el token de refresco en una cookie HttpOnly, Secure y SameSite con su protección anti-CSRF. Así un XSS no puede leer directamente ninguna credencial persistente, y el CSRF queda cubierto por SameSite y el token dedicado.
El XSS lo gana casi todo
Si un atacante consigue ejecutar JavaScript en tu dominio, ninguna ubicación de token te salva del todo: aunque no pueda leer una cookie HttpOnly, puede hacer peticiones autenticadas desde la propia página mientras el usuario está conectado · Cerrar el XSS (CSP, escape de salida, sanitización) es prioritario sobre dónde se guarda el token
6. Descomponer un token en local, sin herramientas online
Para inspeccionar un token de tu propia aplicación no hace falta pegarlo en una web de terceros —algo que, con un token real, equivale a entregar una credencial. La librería estándar de tu lenguaje basta.
# Python: leer header y payload de un JWT de tu API de pruebas
# sin verificar la firma (solo para inspeccion).
import base64, json
def _b64url(seg):
seg += "=" * (-len(seg) % 4) # re-anadir padding
return base64.urlsafe_b64decode(seg)
token = "eyJhbGciOi..." # token de laboratorio propio
header_b64, payload_b64, sig_b64 = token.split(".")
print(json.loads(_b64url(header_b64))) # {"alg": "HS256", "typ": "JWT"}
print(json.loads(_b64url(payload_b64))) # claims: sub, role, exp, aud...
# Para VALIDAR de verdad, usa la libreria y fija el algoritmo:
# import jwt (PyJWT)
# jwt.decode(token, clave_publica, algorithms=["RS256"],
# audience="mi-api", issuer="https://auth.miempresa.local")Marco legal y de uso responsable
Todo lo descrito aquí es para auditar tu propio código y tus propios tokens, o los de un sistema para el que tengas autorización escrita · Manipular tokens contra servicios de terceros para acceder a datos o funciones ajenas encaja en los delitos de acceso ilícito a sistemas y de descubrimiento y revelación de secretos (arts. 197 bis, 197 ter y 264 del Código Penal español) · Practica solo contra una API de pruebas montada por ti en un entorno aislado
Conclusión
Un JWT bien implementado es un mecanismo sólido: firma criptográfica estándar, formato interoperable y validación sin estado cuando eso encaja con el problema. Casi todos los fallos que se le atribuyen no están en el token, sino en el código que lo recibe: aceptar el algoritmo que propone el atacante, usar un secreto adivinable, o dar por buena una firma sin comprobar que el token sigue vigente y estaba destinado a ti.
La lista de verificación es corta y cabe en la cabeza: fija el algoritmo por configuración, usa secretos o claves de entropía real, valida exp, aud e iss en cada servicio, trata el header como dato no confiable, y decide de antemano cómo vas a revocar. Si tu aplicación necesita revocación instantánea y visibilidad de sesiones, admite que quizá una sesión clásica te dé más por menos.
Referencias
RFC 7519 (JSON Web Token) y RFC 8725 (JWT Best Current Practices) — ietf.org · OWASP JWT for Java Cheat Sheet y OWASP API Security Top 10 — owasp.org · Auth0: «Critical vulnerabilities in JSON Web Token libraries» — análisis histórico de alg=none y confusión de algoritmo · Documentación de tu librería (PyJWT, jose, jsonwebtoken): busca siempre la sección de algoritmos permitidos · Nuestros artículos sobre OWASP Top 10:2025 y cabeceras de seguridad HTTP, para el contexto de XSS y CSRF
unfantasmaenelsistema.com | Seguridad, sistemas y todo lo que no debería funcionar (pero funciona)

