Un JWT (JSON Web Token, RFC 7519) es una cadena de texto con tres partes separadas por puntos, cabecera.payload.firma, que transporta afirmaciones (claims) sobre un usuario o una sesión de forma que el receptor pueda comprobar que nadie las ha modificado. Las dos primeras partes son JSON codificado en Base64URL: cualquiera puede leerlas. La tercera es una firma que solo puede generar quien tiene la clave. Por eso decodificar un JWT no es validarlo: leer el contenido es trivial; confiar en él exige verificar la firma, la caducidad y el emisor.
Este artículo recorre la anatomía de un token, los claims habituales, cómo inspeccionarlo, los errores de seguridad clásicos y dónde guardarlo en una aplicación web.
Anatomía de un token
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0IiwibmFtZSI6IkFpbXJhbiIsImlhdCI6MTc5MTI4MDAwMCwiZXhwIjoxNzkxMjgzNjAwfQ.<firma>
- Cabecera (
{"alg":"HS256","typ":"JWT"}): indica el algoritmo de firma y el tipo de token. - Payload (
{"sub":"1234","name":"Aimran","iat":1791280000,"exp":1791283600}): los claims. - Firma: el resultado de firmar
base64url(cabecera) + "." + base64url(payload)con el algoritmo de la cabecera. ConHS256es un HMAC-SHA256 con una clave secreta compartida; conRS256oES256es una firma asimétrica con clave privada que se verifica con la clave pública.
La codificación es Base64URL (variante de Base64 con - y _ en lugar de + y /, sin relleno =), de la que hablamos en qué es Base64. No es cifrado: el payload se lee con una línea de código.
Los claims habituales
| Claim | Nombre | Para qué sirve |
|---|---|---|
iss |
Issuer | Quién emitió el token (tu servidor de autenticación). |
sub |
Subject | A quién se refiere (el identificador del usuario). |
aud |
Audience | Para quién es (la API que debe aceptarlo). |
exp |
Expiration | Instante Unix a partir del cual no es válido. |
nbf |
Not before | Instante Unix antes del cual aún no es válido. |
iat |
Issued at | Cuándo se emitió. |
jti |
JWT ID | Identificador único, útil para revocar. |
Todos son opcionales según el RFC, pero exp debería estar siempre: un token sin caducidad vale para siempre si se filtra. Puedes añadir claims propios (role, plan), con una regla: nunca datos sensibles, porque el payload es público para quien tenga el token.
Cómo inspeccionar un JWT
Para ver qué lleva un token basta con decodificar las dos primeras partes. El decodificador de JWT de AIMRAN Tools muestra cabecera y payload, traduce iat, exp y nbf a fechas legibles e indica si el token está caducado o aún no es válido, todo en el navegador, sin enviar el token a ningún servidor. Si pegas también el secreto, verifica la firma HMAC. En Node.js, sin librerías:
const [header, payload] = token.split('.');
const decode = (part) => JSON.parse(Buffer.from(part, 'base64url').toString('utf8'));
console.log(decode(header), decode(payload));
Ese código lee, no valida. Para validar, usa una librería consolidada (jose, jsonwebtoken en Node; PyJWT en Python) que compruebe la firma con el algoritmo que tú esperas, la caducidad y el emisor.
Errores de seguridad que se repiten
RFC 8725, las buenas prácticas oficiales, recoge la mayoría:
- Confiar en el payload sin verificar la firma. Cualquiera puede fabricar un token con
"role":"admin". Si tu API no verifica la firma, acaba de regalar permisos. - Aceptar el algoritmo que dice la cabecera. El ataque clásico
alg: noneconsiste en enviar un token sin firma y confiar en que el servidor lo acepte. La librería debe verificar solo los algoritmos que tú configures. - Confusión HS256/RS256. Si el servidor usa RS256 (clave pública) y acepta también HS256, un atacante puede firmar con la clave pública como si fuera el secreto HMAC. Misma solución: lista cerrada de algoritmos.
- Secretos débiles en HMAC. Un secreto corto se rompe por fuerza bruta sin conectar con el servidor, porque el atacante tiene el token. Usa secretos aleatorios de al menos 256 bits.
- Tokens eternos. Sin
exp, o con caducidades de meses. Lo habitual: access tokens de minutos y un refresh token para renovarlos. - Datos sensibles en el payload. Correos, DNI, direcciones: el payload no está cifrado. Si necesitas confidencialidad, existe JWE (tokens cifrados), pero rara vez hace falta.
Dónde guardar el token en el navegador
Dos opciones, con compromisos distintos:
- Cookie
HttpOnly; Secure; SameSite: JavaScript no puede leerla, así que un XSS no la roba; el navegador la envía solo; hay que protegerse del CSRF (SameSite=LaxoStrictcubre la mayoría de casos). Es la recomendación habitual para aplicaciones web clásicas. localStorageo memoria: cualquier script de la página puede leerlo, por lo que un XSS lo compromete por completo. Tiene sentido solo para clientes que no pueden usar cookies (apps nativas) o si la API está en otro dominio y el token vive solo en memoria mientras dura la pestaña.
En ambos casos, servir el sitio con una Content-Security-Policy estricta reduce el riesgo de XSS, que es la amenaza real detrás de este debate.
JWT frente a sesiones clásicas
Un JWT permite que la API valide la identidad sin consultar una base de datos en cada petición: toda la información va en el token. Esa ventaja tiene un coste: no se puede revocar un token antes de que caduque salvo que mantengas una lista de revocados (lo que devuelve la consulta a la base de datos). Para una aplicación con un único servidor, una sesión tradicional (identificador opaco en cookie, datos en el servidor) es más simple y más fácil de invalidar. Los JWT brillan cuando hay varios servicios que deben aceptar la misma identidad sin compartir una base de datos de sesiones.
Conclusión
Un JWT son tres bloques Base64URL: cabecera, payload y firma. Cualquiera puede leer los dos primeros; solo la firma, verificada con el algoritmo correcto y la clave correcta, te permite confiar en ellos. Añade siempre exp, limita los algoritmos aceptados, no metas datos sensibles en el payload y guárdalo en una cookie HttpOnly cuando puedas. Y cuando quieras saber qué lleva un token, decodifícalo sin miedo: eso sí es gratis.