Entrada

JWT: anatomía, ventajas y las trampas que nadie te cuenta

Una disección técnica del formato JWT, sus garantías criptográficas, el problema de la revocación y las vulnerabilidades históricas que aún aparecen en producción.

JWT: anatomía, ventajas y las trampas que nadie te cuenta

JSON Web Token se ha convertido en el formato de credencial más popular de la última década. Su ubicuidad es tal que muchos equipos lo adoptan por inercia, sin entender del todo sus garantías ni sus costes. Este artículo forma parte de la serie La evolución de la seguridad en aplicaciones modernas y pretende ofrecer la lectura que uno necesita antes de decidir si un JWT es la herramienta correcta para un caso concreto.

De dónde viene y qué problema resuelve

El estándar se formaliza en RFC 7519 (mayo de 2015), dentro de la familia JOSE (JSON Object Signing and Encryption) que incluye también JWS (RFC 7515, firmas), JWE (RFC 7516, cifrado), JWK (RFC 7517, representación de claves) y JWA (RFC 7518, algoritmos). La motivación es directa: trasladar claims verificables entre partes que no comparten infraestructura de sesión común, con un formato compacto, seguro para URL y manejable por cualquier stack.

La solución que JWT populariza es la verificación sin estado. En lugar de mantener una tabla de sesiones en base de datos y consultarla en cada petición, el servidor emite una credencial firmada que contiene los atributos relevantes. Cualquier verificador con la clave pública (o la clave compartida) puede validar la credencial sin red adicional, sin coordinación, sin acoplamiento. Este desacoplamiento es el que ha hecho de JWT el compañero natural de arquitecturas distribuidas y OAuth 2.0.

Anatomía: tres partes y un malentendido recurrente

Un JWT es una cadena con tres segmentos separados por puntos: header.payload.signature. Cada segmento es una codificación base64url (sin padding) de un objeto JSON, excepto la firma que es la codificación de los bytes de la firma criptográfica.

El header declara metadatos del propio token: alg (algoritmo de firma), typ (tipo, normalmente JWT) y opcionalmente kid (identificador de clave, fundamental cuando el emisor rota claves). Un header típico es {"alg":"RS256","typ":"JWT","kid":"2024-q1"}.

El payload contiene los claims: afirmaciones sobre el sujeto y el contexto. RFC 7519 define un conjunto de claims registrados: iss (issuer), sub (subject), aud (audience), exp (expiration), nbf (not before), iat (issued at) y jti (JWT ID, único por token). A partir de ahí, el emisor puede incluir claims públicos (registrados en IANA) o privados.

La signature se calcula sobre los dos primeros segmentos ya codificados. Es lo que garantiza integridad y autenticidad del emisor; no confidencialidad.

Aquí aparece el malentendido más extendido: base64url no es cifrado. Cualquier persona con el token en la mano puede decodificar header y payload. Colocar datos sensibles —números de tarjeta, tokens de otros sistemas, información médica— en un JWT firmado es filtrarlos a cualquiera que intercepte el token. Si la confidencialidad del contenido importa, se usa JWE (el token cifrado), no JWS.

Firmas: simétricas, asimétricas y consecuencias operativas

El campo alg admite varias familias. HS256, HS384 y HS512 son HMAC con SHA-2 y clave compartida: emisor y verificador conocen el mismo secreto. Simple pero rígido, porque cualquier componente capaz de verificar también es capaz de emitir. Adecuado para entornos pequeños con un único emisor y verificadores bajo el mismo dominio de confianza.

RS256 y su familia (RS384, RS512) son RSA con SHA-2. ES256, ES384, ES512 son ECDSA sobre curvas NIST. EdDSA (con Ed25519) es la opción moderna más limpia. En todas ellas, el emisor firma con clave privada y los verificadores validan con clave pública. Este modelo es el que habilita arquitecturas federadas: un IdP emite tokens que cientos de APIs pueden verificar sin compartir secretos.

La distribución de claves públicas se resuelve con un endpoint JWKS (JSON Web Key Set), habitualmente publicado en /.well-known/jwks.json. Los verificadores lo consultan, cachean con respeto al TTL indicado y seleccionan la clave correcta a partir del kid del header. Cualquier arquitectura seria debería soportar rotación de claves sin downtime, y eso pasa por implementar kid desde el primer día.

El problema de la revocación

La ventaja de la verificación sin estado se paga en el momento en que hace falta revocar. Un JWT firmado es válido hasta su exp, sin más. No hay botón de apagado. Si un usuario cambia la contraseña, cierra sesión, pierde el dispositivo o detectas compromiso de cuenta, los tokens ya emitidos siguen funcionando hasta que caducan.

Existen tres estrategias habituales y todas tienen su coste.

La primera es TTL cortos con rotación de refresh tokens: access tokens de pocos minutos, refresh tokens rotatorios con detección de reutilización. Limita la ventana de abuso sin reintroducir estado en la ruta caliente de validación. Es la opción por defecto recomendable.

La segunda es una blocklist de jti consultada en cada verificación, típicamente en Redis o una cache distribuida. Resuelve revocación inmediata a costa de una lectura externa por petición, lo que reintroduce parte del acoplamiento que JWT venía a evitar.

La tercera es renunciar al JWT y usar tokens opacos con introspección (RFC 7662): una referencia opaca que el Resource Server intercambia con el Authorization Server para obtener el estado actual. Estado centralizado, revocación inmediata, pero red en cada validación. Para APIs internas con latencia baja, es una opción perfectamente razonable.

Vulnerabilidades clásicas

La superficie de ataque de JWT está bien documentada, y sin embargo sigue apareciendo en auditorías. Vale la pena conocer las tres más recurrentes.

alg: none

El estándar incluye el algoritmo none para tokens “sin firma”. Librerías antiguas validaban el alg del header y, si decía none, aceptaban el token sin verificar nada. Un atacante forjaba un token con {"alg":"none"}, payload arbitrario y firma vacía, y entraba. El mitigante es sencillo y no negociable: el verificador debe configurar qué algoritmos acepta y rechazar todo lo demás, nunca confiar en el alg que declare el propio token.

Confusión RS256 / HS256

Variante más refinada. El servidor usa RS256 y publica su clave pública. Un atacante toma esa clave pública, genera un token firmado con HS256 usando la clave pública como si fuera un secreto compartido, y lo envía. Si el verificador elige el algoritmo basándose en el header, trata la clave pública como secreto HMAC y la verificación pasa. Misma mitigación: lista blanca explícita de algoritmos, vinculada al tipo de clave.

kid injection

El header kid es una cadena controlada por el emisor. Si el verificador lo usa sin saneamiento como parámetro en una consulta a base de datos, lectura de fichero o path dinámico, abre la puerta a SQL injection, path traversal o equivalentes. Un kid debe tratarse como input no confiable y validarse contra un conjunto conocido.

A esto se suman las claves débiles en HS256 (secretos cortos rompibles por fuerza bruta offline), tokens con exp desmesuradamente largos y la omisión de validación de aud e iss, que permite reutilizar tokens emitidos para otro servicio.

JWT frente a cookies de sesión

No todo token debería ser un JWT. Para autenticación en una aplicación web tradicional con un único dominio, backend propio y frontend servido por ese mismo backend, una cookie de sesión httpOnly + Secure + SameSite es más simple, revocable de inmediato y menos vulnerable a XSS que cualquier JWT almacenado en localStorage. Sesiones en Redis con TTL corto resuelven el problema sin introducir criptografía ni gestión de claves.

JWT brilla cuando hay múltiples verificadores independientes, federación entre dominios, arquitecturas multi-tenant o microservicios que no pueden permitirse una dependencia síncrona con el IdP en cada petición. Allí su coste se amortiza.

Access Token frente a ID Token

Conviene separar dos usos distintos que comparten formato. En OAuth 2.0, el access token representa autorización: “el portador puede hacer X”. Su audiencia es el Resource Server. En OpenID Connect, el ID Token representa autenticación: “este usuario se autenticó en tal momento”. Su audiencia es el cliente. Mezclarlos es el origen de una categoría entera de vulnerabilidades: usar el access token para validar identidad, o enviar el ID Token a una API como si fuera credencial de acceso.

Tamaño, Bearer y vinculación al cliente

Los JWT viajan en cada petición. Un payload inflado con grupos, permisos y atributos arbitrarios pronto supera los límites prácticos de headers HTTP. La regla conservadora es incluir lo imprescindible para decisiones de autorización rápidas y dejar el resto en sistemas consultables bajo demanda.

La semántica bearer —poseer el token es usar el token— es el talón de Aquiles. RFC 8705 introduce mutual-TLS-bound access tokens: el token lleva el hash del certificado cliente y solo es válido cuando se presenta sobre la misma conexión mTLS. DPoP (RFC 9449) ofrece una vinculación equivalente sin requerir infraestructura PKI en el cliente. Son las defensas modernas para contextos donde el robo de token es un riesgo tangible.

Si el servicio maneja datos regulados o ejecuta operaciones de alto impacto, los access tokens bearer puros han dejado de ser aceptables. Tokens vinculados al canal (mTLS) o al cliente (DPoP) son la línea base razonable.

Artículos relacionados en esta serie

Referencias

  • RFC 7515 (mayo 2015). JSON Web Signature (JWS).
  • RFC 7516 (mayo 2015). JSON Web Encryption (JWE).
  • RFC 7517 (mayo 2015). JSON Web Key (JWK).
  • RFC 7518 (mayo 2015). JSON Web Algorithms (JWA).
  • RFC 7519 (mayo 2015). JSON Web Token (JWT).
  • RFC 7662 (octubre 2015). OAuth 2.0 Token Introspection.
  • RFC 8725 (febrero 2020). JSON Web Token Best Current Practices.
  • RFC 8705 (febrero 2020). OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens.
  • RFC 9449 (septiembre 2023). OAuth 2.0 Demonstrating Proof of Possession (DPoP).
Esta entrada está licenciada bajo CC BY 4.0 por el autor.