Tokens y formatos: JWT, opacos, JWE y PASETO
Las piezas de las que dependen los protocolos: la estructura del JWT (cabecera, contenido, firma), los tokens autocontenidos frente a los opacos o de referencia, JWE para contenidos confidenciales, PASETO como alternativa más segura que evita la confusión de algoritmos, y cómo validar un token: firma, expiración, destinatario, emisor y revocación.
Una cadena en un ticket de soporte
David trabaja en el equipo de APIs. Un cliente abre un ticket de soporte y, con toda la buena intención, pega la cadena larga que su app envió en la petición que falla: tres bloques de letras y números separados por puntos. David la mete en un decodificador por costumbre y ahí está todo, en JSON legible: el email del cliente, su tenant, su rol, la hora de expiración. Sin clave, sin contraseña, sin nada. No ha roto nada; el token funciona así. El ticket es ahora una copia de una credencial viva guardada en el sistema de soporte hasta que caduque.
Esa cadena era un token, el artefacto que no deja de aparecer a lo largo de este módulo. OIDC emite un ID token, OAuth emite un access token, SAML emite una aserción. Todos son paquetes de claims de confianza que una parte fabrica y otra verifica. Este artículo abre el token por dentro: cómo se construye, qué formatos te vas a encontrar y, sobre todo, cómo validarlo sin llevarte un susto.
La mayoría de los tokens modernos son tokens al portador (bearer): quien lo tiene lo puede usar, como el dinero en efectivo. Por eso su formato y su validación son cuestiones de seguridad. Un token en el que tu servicio confía tras una validación descuidada es un billete falso que aceptaste por no mirar la marca de agua. Para cualquier token que diseñes o aceptes, dos preguntas explican casi todo lo que viene: quién puede leerlo, y cómo se valida, en local o preguntando al emisor.
JWT: el caballo de batalla autocontenido
El JSON Web Token (RFC 7519) es el formato de token dominante en la web actual. Es autocontenido: todo lo que el verificador necesita viaja dentro, así que se puede comprobar en local sin llamar al emisor. Un JWT son tres partes en base64url unidas por puntos, cabecera.contenido.firma.
Cada parte tiene un trabajo. La cabecera (header) describe el propio token: alg, el algoritmo de firma (RS256, ES256, etc.), casi siempre kid, el identificador de clave que le dice al verificador qué clave pública usar, y typ. El contenido (payload) lleva los claims, afirmaciones como iss (emisor), sub (sujeto), aud (destinatario), exp (expiración) e iat (emisión), que son lo que el verificador usa de verdad. La firma es la del emisor sobre la cabecera y el contenido codificados; el verificador la recalcula y la compara, y si coincide, el token viene del emisor y nadie lo ha cambiado.
Esto es lo que sorprendió a David. Base64url es una codificación, no un cifrado. La cabecera y el contenido de un JWT firmado normal los puede leer cualquiera que tenga el token: el navegador, los logs, un ticket de soporte, un atacante. La firma protege la integridad y la autenticidad, no la confidencialidad. No metas nunca un secreto, una contraseña o datos personales sensibles en el contenido de un JWT salvo que uses JWE, del que hablamos enseguida. Trata el contenido como público.
JWS: firmado, lo normal
Lo que hemos descrito, un JWT firmado, es técnicamente un JWS (JSON Web Signature, RFC 7515). La firma puede ser asimétrica o simétrica. Con algoritmos asimétricos (RS256, ES256), el emisor firma con una clave privada y los verificadores comprueban con la pública, que descargan del endpoint JWKS del emisor. Es lo habitual en federación: muchos pueden verificar y solo uno puede fabricar. Con algoritmos simétricos (HS256), un mismo secreto compartido firma y verifica. Es más sencillo, pero cualquier verificador puede también falsificar, así que solo encaja dentro de un único dominio de confianza.
Claims registrados, públicos y privados
Los claims son de tres tipos, y distinguirlos evita choques. Los registrados son los nombres estándar reservados por el RFC 7519: iss, sub, aud, exp, nbf (no antes de), iat (emitido en) y jti, un identificador único del token muy útil para listas de denegación y para detectar reutilizaciones. Los públicos son nombres registrados en IANA o con espacio de nombres por URI para no chocar, como email y name de OIDC. Los privados son los que acuerdan las dos partes entre sí (tenant_id, role). Valen dentro de tus sistemas, pero ponles un prefijo para que no choquen con nombres que se registren más adelante.
Los algoritmos de firma que te vas a encontrar
La cabecera alg dice cómo se firmó el token, y unos pocos valores lo cubren casi todo. RS256, firmas RSA, está en todas partes y es el valor seguro por defecto en federación. ES256, ECDSA sobre la curva P-256, da claves y firmas más pequeñas que RSA con la misma fuerza y se prefiere cada vez más. EdDSA con Ed25519 es moderno, rápido y difícil de usar mal, la mejor opción allí donde esté soportado. HS256, HMAC con secreto compartido, solo vale para un único dominio de confianza, porque el verificador también puede fabricar.
El único valor que no debes aceptar jamás es none, un JWT que dice no necesitar firma. Fija los algoritmos que esperas y rechaza todo lo demás. Dejar que el token elija es la raíz de los ataques que veremos con PASETO y con la validación.
JWE: cuando el contenido tiene que ser secreto
A veces los propios claims son sensibles y ni el cliente ni nada de lo que haya en medio debería poder leerlos. JWE (JSON Web Encryption, RFC 7516) es un JWT cuyo contenido va cifrado, no solo firmado, de modo que solo el destinatario previsto, con la clave adecuada, puede descifrarlo. Un JWE tiene cinco partes en lugar de tres, porque también lleva la clave cifrada y el vector de inicialización.
La diferencia es sencilla. Un token firmado responde a si puedes fiarte de él; un JWE además controla quién más puede leerlo. Usa JWE cuando un token tenga que pasar por una parte que no debe ver su contenido, por ejemplo un access token que viaja por el navegador con atributos que el cliente no debe inspeccionar. En la práctica JWE es mucho menos común que JWS, porque la mayoría de los tokens no llevan nada secreto y un token firmado y legible es más fácil de manejar. Si el token del cliente de David hubiera llevado algo de verdad sensible, ese habría sido el argumento para usarlo.
Autocontenido frente a opaco
Por debajo de la cuestión del formato hay una decisión de diseño más profunda: ¿el token es autocontenido, un JWT que el verificador lee en local, u opaco, una referencia que el verificador tiene que consultar?
flowchart TB
accTitle: Validación de tokens autocontenidos frente a opacos
accDescr: Un resource server recibe un token y actúa según su tipo. Si es un JWT autocontenido, lo valida en local con la clave pública del emisor y los claims que lleva dentro, sin llamadas de red; es rápido y sin estado, pero sigue siendo válido hasta que caduca. Si es un token opaco de referencia, el servidor tiene que llamar por red al endpoint de introspección del emisor para preguntar si el token está activo y qué scopes lleva; eso añade latencia y dependencia, pero permite al emisor revocarlo al instante.
T[Llega un token al resource server] --> Q{¿Autocontenido u opaco?}
Q -->|JWT autocontenido| L[Valida en local<br/>firma + claims, sin red]
L --> L2[Rápido y sin estado<br/>pero válido hasta exp]
Q -->|Referencia opaca| I[Llama a la introspección del emisor]
I --> I2[Más latencia y dependencia<br/>pero revocación inmediata]Con un JWT autocontenido, el verificador comprueba la firma y los claims en local. Es rápido, no guarda estado y no depende del emisor en el momento de la petición, lo que encaja con APIs de mucho tráfico y con microservicios. El precio es la revocación: como la verificación es sin conexión, el token vale para todos hasta exp y no hay forma fácil de retirarlo. Con un token opaco de referencia, el token es solo un identificador aleatorio, y el verificador llama al endpoint de introspección del emisor (RFC 7662) para saber si está activo y qué concede. El emisor es la fuente de verdad en vivo, así que la revocación es inmediata. El precio es una llamada de red por cada validación y una dependencia fuerte de que el emisor esté disponible.
Muchos sistemas acaban mezclando: JWT de vida corta para las llamadas de todos los días, aceptando un pequeño hueco de revocación a cambio de velocidad, y tokens opacos con introspección para los recursos más sensibles, pagando la latencia a cambio de control inmediato. Es la misma tensión entre velocidad y control que vimos entre JIT y SCIM, ahora en la capa del token.
Dónde aparecen estos tokens de verdad
Ayuda situar los formatos en los protocolos que ya conoces, porque cada uno tomó una decisión deliberada. El ID token de OIDC es siempre un JWT firmado; está pensado para que lo lea el cliente, así que autocontenido y legible es justo lo que toca. El access token de OAuth puede ser un JWT u opaco, a criterio del proveedor. Unos emiten JWT para que los resource servers validen rápido, otros emiten tokens opacos y esperan introspección, y el cliente debe tratarlo como opaco en cualquier caso. El refresh token de OAuth es casi siempre opaco y de larga duración, y solo viaja entre el cliente y el servidor de autorización, la única parte que necesita consultarlo. Y la aserción SAML también es un token firmado, solo que en XML en lugar de JSON: la misma idea de claims más firma, de una época anterior.
Así que “token” no es una sola cosa. Los mismos reflejos sirven para todos, sean JSON o XML, autocontenidos o de referencia: verifica la firma, comprueba para quién es y comprueba cuándo es válido.
PASETO: una alternativa más segura a JWT
La flexibilidad de JWT es también su debilidad. Como el token declara su propio algoritmo en la cabecera, un verificador descuidado se puede dejar engañar. Está el famoso ataque alg:none, en el que el servidor acepta un token que dice no necesitar firma, y están los ataques de confusión de algoritmos, en los que se engaña a un servidor para que verifique un token RS256 usando la clave pública como si fuera un secreto HS256. Los dos son fallos de implementación, pero el formato los pone fáciles.
PASETO (Platform-Agnostic Security Tokens) es una respuesta moderna que parte de una idea: no dejes que el token negocie su propio algoritmo. Cada versión de PASETO fija un único conjunto de primitivas criptográficas revisadas, y cada token tiene uno de dos propósitos. Los local son simétricos, cifrados y autenticados con una clave compartida, así que el contenido es confidencial. Los public van firmados de forma asimétrica, como un JWS, así que el contenido se puede leer pero también verificar.
Como la versión fija el algoritmo, alg:none y la confusión de algoritmos sencillamente no existen como superficie de ataque. PASETO cambia la amplia configurabilidad de JWT por valores seguros por defecto. JWT sigue dominando gracias a su ecosistema, con librerías y soporte de proveedores en todas partes, y eso es un motivo real para elegirlo. Pero PASETO merece la pena como el diseño que aprendió de las cicatrices de JWT, y como recordatorio de que un formato puede hacer imposible toda una clase de errores en vez de solo desaconsejarla. Cuando eliges JWT, te quedas con una flexibilidad que luego tienes que asegurar con una validación cuidadosa.
Validar el token es la seguridad
Un token es tan fiable como las comprobaciones que hay detrás, y la mayoría de las vulnerabilidades con tokens vienen de una validación que se saltó o se hizo mal. Valida cada token en este orden:
flowchart TB
accTitle: Lista de comprobación para validar un token
accDescr: Un token recibido pasa por una secuencia de comprobaciones, y si falla cualquiera se rechaza. Primero se verifica la firma con la clave pública del emisor, rechazando explícitamente alg:none y los algoritmos inesperados. Después se comprueba la expiración exp, junto con nbf e iat, con un margen de reloj mínimo. Después se confirma que el destinatario aud es este servicio. Después se confirma que el emisor iss es un proveedor de confianza. Por último se comprueba que no esté revocado mediante vidas cortas, una lista de denegación o introspección. Solo se acepta el token que supera todas las comprobaciones.
S[Token recibido] --> SIG{¿Firma válida?<br/>rechaza alg:none}
SIG -->|no| R[Rechazar]
SIG -->|sí| EXP{¿No caducado?<br/>exp / nbf / iat}
EXP -->|no| R
EXP -->|sí| AUD{¿Soy el destinatario?<br/>aud}
AUD -->|no| R
AUD -->|sí| ISS{¿Emisor de confianza?<br/>iss}
ISS -->|no| R
ISS -->|sí| REV{¿No revocado?}
REV -->|no| R
REV -->|sí| OK[Aceptar]Empieza por la firma: verifícala con la clave del emisor, elegida por kid en el JWKS, rechaza alg:none y fija el algoritmo esperado para que el token no pueda rebajarse a sí mismo. Luego la expiración: exp tiene que estar en el futuro, nbf e iat tienen que cuadrar, y el margen para desajustes de reloj debe ser de un minuto o dos, no de horas. Luego el destinatario: aud tiene que nombrar a tu servicio. Saltarse esto permite reutilizar contra un servicio un token emitido para otro, un error que está detrás de brechas reales. Luego el emisor: iss tiene que estar en una lista de proveedores de confianza, no ser lo que el token diga de sí mismo. Y por último la revocación, la difícil, porque una firma válida no demuestra que el token no se haya revocado. Cúbrela con vidas cortas, una lista de denegación de ID revocados o introspección para los recursos sensibles.
Dos fallos explican buena parte de las brechas con tokens: aceptar un token cuya cabecera dice alg:none, con lo que cualquiera puede fabricar uno, y leer los claims de un token sin verificar la firma en absoluto. Decodificar en base64 no es validar. Las buenas prácticas de JWT (RFC 8725) existen precisamente porque estos errores son muy habituales. Verifica primero y fíate de los claims solo después.
Higiene operativa
Más allá de cada petición, algunos hábitos mantienen sano un sistema de tokens.
Cuida dónde viven los tokens. Un token es una credencial al portador. En apps de navegador, mejor una cookie HttpOnly, Secure y SameSite, o guardarlo en memoria, que localStorage, que cualquier script puede leer y que es un objetivo favorito del cross-site scripting. Y mantén los tokens fuera de sitios donde no pintan nada, como los logs, la analítica y los tickets de soporte; el equipo de David acabó poniendo un aviso en el formulario de soporte y un filtro que enmascara cualquier cosa con forma de JWT.
Mantén el contenido escueto y apto para ser leído. Pon solo lo que los verificadores necesitan, nunca secretos ni datos personales sensibles en un JWT firmado y legible. Cada claim de más es algo en lo que un verificador podría apoyarse por error.
Rota las claves de firma. Publícalas en el JWKS con su kid para poder pasar a una clave nueva sin cortes, y retira las antiguas según un calendario. Rotar la clave es además tu palanca bruta de revocación: cambias la clave y todos los tokens firmados con ella mueren.
Mantén vidas cortas y rota los refresh tokens. Los access tokens de vida corta reducen el hueco de revocación, y los refresh tokens rotatorios de OAuth hacen que un token robado pierda su valor rápido.
Plantéate atar los tokens a quien los envía. Los tokens al portador se pueden robar por definición. Vincular un token a su emisor legítimo, para que una copia robada no sirva en ningún otro sitio, es lo que hacen DPoP y mTLS, y los veremos entre los temas de frontera.
Tokens frente a sesiones en el servidor
Debajo de todo esto hay una pregunta razonable: ¿por qué usar tokens en lugar del patrón web clásico, una cookie de sesión que apunta a un estado guardado en el servidor? Es el mismo dilema con otra ropa. Una sesión en el servidor es el token opaco por excelencia: la cookie es solo un ID, todo el estado vive en el servidor y revocarla es trivial, borras la fila. Es sencillo y se revoca al instante, pero cada petición tiene que llegar a un almacén de sesiones compartido, y eso se complica con muchos servicios y regiones.
Un token autocontenido le da la vuelta. El estado viaja dentro del token, así que cualquier servicio puede verificarlo solo con una clave pública, sin almacén de sesiones compartido ni consulta central. Por eso los tokens encajan de forma natural en sistemas distribuidos, APIs y microservicios, y el precio, otra vez, es la revocación. El debate entre tokens y sesiones es el mismo eje que autocontenido frente a opaco. Muchos sistemas combinan los dos: una sesión con estado para la propia app del usuario y tokens de vida corta para las llamadas a APIs y servicios.
Los formatos lado a lado
| Formato | ¿Legible? | Validación | Úsalo para |
|---|---|---|---|
| JWT / JWS | Sí (firmado, no secreto) | En local, sin conexión | Tokens autocontenidos; ID tokens, muchos access tokens |
| JWE | No (cifrado) | En local, con la clave de descifrado | Tokens cuyos claims deben ser confidenciales |
| Opaco / de referencia | No hay nada que leer | Llamada a la introspección del emisor | Revocación inmediata; refresh tokens, recursos sensibles |
| PASETO | Según el propósito (public legible, local cifrado) | En local, algoritmo fijo | Sistemas nuevos que quieren valores seguros por defecto |
No hay un formato mejor que todos. La elección vuelve a las dos preguntas del principio, quién puede leerlo y cómo se valida. Una API de mucho tráfico tira hacia JWT autocontenidos, un sistema que tiene que cortar sesiones al instante tira hacia tokens opacos, un contenido con secretos necesita JWE y un desarrollo nuevo que quiere evitar las trampas clásicas debería mirar PASETO.
Resumen
Los tokens son los paquetes de confianza que se pasan los protocolos.
- JWT es el caballo de batalla autocontenido:
cabecera.contenido.firma, en base64url, validado en local. Firmado (JWS), cualquiera lo puede leer, así que nunca metas secretos. - JWE cifra el contenido cuando los intermediarios no deben leer los claims; JWS solo garantiza autenticidad e integridad.
- Autocontenido frente a opaco es el dilema de fondo. Los JWT son rápidos y sin estado pero difíciles de revocar; los opacos necesitan una llamada de introspección pero se revocan al instante. Es el mismo eje que tokens sin estado frente a sesiones en el servidor.
- PASETO elimina la negociación de algoritmos de JWT fijándolos por versión, así que no hay
alg:noneni confusión de algoritmos. - La validación es la seguridad: firma (rechazando
alg:none), expiración, destinatario, emisor y revocación, en ese orden y siempre. - La higiene reduce el daño cuando un token se filtra: guárdalos con cuidado, mantén el contenido escueto, rota las claves, usa vidas cortas y plantéate atar el token a quien lo envía.
Ejercicios prácticos
Están pensados para hacerse, no solo para leerse.
- Decodifica un JWT. Coge un JWT real, de un flujo “Iniciar sesión con” o de la sesión de tu navegador, y pégalo en un decodificador o decodifica a mano cada segmento en base64url. Localiza
algykiden la cabecera eiss,audyexpen el contenido, y comprueba que has podido leer todos los claims sin ninguna clave. - Detecta el riesgo de almacenamiento. Una aplicación de una sola página guarda su access token en
localStorage. Explica el ataque concreto que eso permite y propone dos alternativas más seguras con sus pros y contras. - Elige el modelo de validación. Para cada caso, escoge un JWT autocontenido o un token opaco con introspección y justifícalo: una API pública con mucho tráfico; una app bancaria en la que una sesión robada tiene que poder cortarse en segundos; un conjunto de microservicios detrás de un único gateway.
- Rompe un validador. Un servicio valida JWT pero se fía del campo
algde la cabecera y no compruebaaud. Describe dos falsificaciones distintas que esto permite y los arreglos exactos. - Defiende JWT o PASETO. Un equipo está eligiendo formato de token para un servicio interno nuevo. Escribe dos frases a favor de PASETO y dos a favor de quedarse con JWT, y di cuál elegirías y por qué.
- Elige el formato. Escoge JWT, JWE u opaco para cada caso y justifícalo: un access token que tiene que llevar el nivel de habilitación de un usuario que ningún cliente debe leer; un token de sesión para un banco que tiene que poder revocarse en segundos; el ID token de una app web normal con “Iniciar sesión con”.
Tres preguntas para ponerte a prueba
- ¿Por qué cualquiera que tenga un JWT firmado puede leerlo, y qué formato deberías usar cuando los claims tienen que ser confidenciales?
- Explica el dilema de la revocación entre tokens autocontenidos y opacos, y describe dos formas prácticas de suavizar el lado de los JWT.
- ¿En qué consiste el ataque
alg:none, qué hace vulnerable a JWT y cómo lo descarta el diseño de PASETO?
Preguntas frecuentes
¿Qué es un JWT (JSON Web Token)?
Un JWT es un token compacto y autocontenido formado por tres partes en base64url separadas por puntos: cabecera, contenido y firma. La cabecera indica el algoritmo de firma, el contenido lleva los claims (de quién habla el token, para quién es, cuándo caduca) y la firma permite al receptor comprobar que nadie lo ha alterado. Como es autocontenido, un servicio puede validarlo en local sin llamar al emisor.
¿Qué diferencia hay entre un JWT y un token opaco?
Un JWT es autocontenido: el receptor lo valida en local comprobando la firma y los claims, sin llamar al emisor. Un token opaco o de referencia es solo un identificador aleatorio, y el receptor tiene que llamar al endpoint de introspección del emisor para saber si es válido y qué concede. Los JWT son rápidos y sin estado, pero difíciles de revocar antes de que caduquen; los opacos añaden una consulta, pero se pueden revocar al instante.
¿Qué diferencia hay entre JWS y JWE?
JWS (JSON Web Signature) es un token firmado: cualquiera puede leer el contenido, pero la firma demuestra que es auténtico y que no se ha alterado. Es lo que la gente suele llamar 'un JWT'. JWE (JSON Web Encryption) es un token cifrado: el contenido es confidencial y solo el destinatario previsto puede leerlo. Usa JWS para integridad y autenticidad, y JWE cuando el propio contenido tenga que ser secreto.
¿Qué es PASETO y por qué usarlo en lugar de JWT?
PASETO (Platform-Agnostic Security Tokens) es un formato moderno pensado para evitar las trampas de JWT, sobre todo la confusión de algoritmos y el famoso ataque alg:none. En vez de dejar que el token declare su propio algoritmo, PASETO fija un pequeño conjunto de algoritmos revisados por cada versión del protocolo, así que no se puede engañar al receptor para que acepte un token falsificado o sin firma. Renuncia a la flexibilidad de JWT a cambio de valores seguros por defecto.
¿Cómo se valida un token?
Comprueba, en este orden: la firma (con la clave pública del emisor, rechazando alg:none y cualquier algoritmo inesperado), la expiración (exp, además de nbf e iat con un margen de reloj mínimo), el destinatario (aud tiene que ser tu servicio) y el emisor (iss tiene que ser un proveedor de confianza). La revocación es el caso difícil: apóyate en vidas cortas, en una llamada de introspección o en una lista de denegación, porque una firma válida no demuestra que el token no se haya revocado.
¿Se puede revocar un JWT antes de que caduque?
No con facilidad, y ese es el gran inconveniente de los tokens autocontenidos. Un JWT es válido para cualquier verificador hasta que pasa su exp, porque la verificación es local y sin conexión. Las salidas prácticas son vidas cortas para los access tokens (minutos), una lista de denegación en el servidor con los ID de token revocados, tokens opacos con introspección para los recursos sensibles, o rotar las claves de firma para invalidar muchos tokens de golpe.