Un JWT tiene aspecto de cosa cifrada: tres bloques de letras y números sin sentido separados por puntos. Ese aspecto es la raíz de la mitad de los fallos. Hay aplicaciones que meten datos personales dentro creyendo que nadie los lee, servidores que aceptan un token sin firma porque la cabecera dice que no hace falta, y código que «comprueba» el token leyéndolo sin verificarlo.

Ninguno de esos fallos rompe la criptografía. Todos son fallos de confianza: el servidor cree algo que le ha dicho el cliente. El laboratorio «Autenticación y JWT» los pone en cinco salas, con tokens que se firman y se verifican de verdad dentro de la página, para que el fallo se vea en lugar de contarse.

Entrar al laboratorio →

A quién va dirigido

Formaciónmaterial del Curso de especialización en Ciberseguridad en entornos de las TI; encaja en la parte de seguridad web y autenticación segura
Requisitossaber qué es una petición HTTP y una cabecera, y leer un poco de JavaScript
Duraciónunos 20 minutos de juego; con la puesta en común, una sesión
Formatoen solitario, sin reloj de cuenta atrás, cinco salas y 1300 puntos posibles
IdiomasES · CA · EN

La teoría

Qué es un JWT

Un JSON Web Token (RFC 7519) es una forma compacta de transportar un conjunto de afirmaciones —claims— entre dos partes. En su forma habitual, firmada, son tres partes separadas por puntos:

cabecera.carga.firma
ParteQué llevaEjemplo
Cabecerael tipo y el algoritmo de firma{"alg":"HS256","typ":"JWT"}
Carga (payload)los claims: quién es, qué puede, hasta cuándo{"sub":"42","role":"user","exp":1767225600}
Firmael resultado de firmar cabecera.carga con una clave32 bytes en HS256

Cabecera y carga son JSON codificado en Base64url (RFC 4648, sección 5): el Base64 de siempre con - y _ en lugar de + y /, y sin el relleno =. Es una codificación, no un cifrado: convierte bytes en texto que se puede poner en una URL o una cabecera, y se deshace sin ninguna clave.

Los claims registrados que hay que conocer:

ClaimSignificado
issquién emitió el token
subde quién habla el token
audpara quién está pensado
expa partir de cuándo deja de valer
nbfantes de cuándo todavía no vale
iatcuándo se emitió
jtiidentificador único, útil para revocar

Las fechas son NumericDate: segundos desde el 1 de enero de 1970 en UTC, no milisegundos.

Firmar no es cifrar: JWS y JWE

El JWT es el formato de la carga; lo que la protege es otra especificación:

  • JWS (RFC 7515) firma. Garantiza integridad y autenticidad: nadie sin la clave puede cambiar la carga sin que la firma deje de cuadrar. No garantiza confidencialidad: cualquiera que tenga el token lo lee.
  • JWE (RFC 7516) cifra. Es un formato distinto, de cinco partes, y se usa mucho menos.

Los algoritmos están en la RFC 7518 (JWA). Los dos grandes grupos:

FamiliaEjemplosClave para firmarClave para verificar
HMAC (simétrico)HS256, HS384, HS512un secreto compartidoel mismo secreto
Firma asimétricaRS256, PS256, ES256, EdDSAla clave privadala clave pública

Consecuencia práctica: en la carga de un JWT firmado no va nada que el usuario no pueda ver. Si hace falta confidencialidad, JWE o, mucho más sencillo, un token opaco que sea solo una referencia a datos guardados en el servidor.

alg: none y la confusión de algoritmos

La RFC 7518 define el valor none para un JWS sin protección, pensado para contextos donde la integridad ya está garantizada por otro medio. Una librería que lee el campo alg de la cabecera y obedece puede aceptar un token sin firma como si fuera válido. El problema de fondo es que la cabecera la escribe el cliente: dejar que decida cómo se verifica es preguntarle al sospechoso si hay que registrarlo.

La misma familia incluye la confusión de algoritmos. Un servidor que espera RS256 verifica con la clave pública. Si acepta también HS256 y usa «la clave que tenga configurada», el atacante puede firmar con HMAC usando como secreto la clave pública, que por definición conoce.

La defensa es la misma para las dos: el algoritmo lo fija el servidor, con una lista blanca, y nunca se deduce del token. Si se usa kid para elegir clave, se valida contra un conjunto cerrado; nunca como ruta de fichero ni dentro de una consulta. La RFC 8725 (JSON Web Token Best Current Practices) recoge estas reglas y es la lectura obligada después del laboratorio.

Secretos débiles y entropía

Un token HS256 se puede atacar sin conexión: con un token válido en la mano, el atacante prueba secretos candidatos hasta que la firma cuadra, sin hacer ni una petición al servidor. Por eso la RFC 7518 exige para HMAC una clave al menos tan larga como la salida del hash: 256 bits para HS256. Una palabra, una frase o el nombre del proyecto no lo son.

Lo mismo vale para los tokens opacos, los identificadores de sesión que el servidor busca en su tabla: valen exactamente lo que vale su entropía, es decir, cuántos valores igual de probables podrían haber salido.

Cómo se generaEntropía real
contador, autoincrementocasi cero: se predice el siguiente
marca de tiempo, Date.now()unos pocos bits si se conoce la hora aproximada
hash del correo o del id de usuariola del dato de origen, que suele ser público
Math.random()no es un generador criptográfico: su estado se puede reconstruir
CSPRNG, 16 bytes128 bits
CSPRNG, 32 bytes256 bits

La guía de OWASP sobre gestión de sesiones pide al menos 64 bits de entropía; 128 bits o más es el margen cómodo y el que pide el laboratorio. Para hacerse una idea: 2¹²⁸ son unos 3,4 × 10³⁸ valores. A un billón de intentos por segundo, recorrer la mitad llevaría del orden de 5 × 10¹⁸ años. En Node se generan con crypto.randomBytes(32); en el navegador, con crypto.getRandomValues().

Y una regla de implementación: los tokens y las firmas se comparan en tiempo constante (crypto.timingSafeEqual en Node), para que el tiempo de respuesta no cuente cuántos caracteres coincidían.

Autenticar en cada ruta, autorizar en cada acción

Autenticar es saber quién eres; autorizar es decidir qué puedes hacer. Las dos tienen que pasar en el servidor y en todas las rutas. El fallo más común no es criptográfico: es una ruta a la que nadie le puso el guardián. Rutas de depuración, /v1/ olvidada al publicar /v2/, métricas, webhooks, paneles de administración «que nadie conoce».

La causa es el modelo por defecto. Si el router permite y la protección se añade ruta a ruta, cada ruta nueva es una ocasión de olvido. La alternativa es denegar por defecto: un middleware global y una lista blanca explícita de rutas públicas, con un test que recorra el árbol de rutas y falle si alguna no está ni protegida ni en la lista.

En el OWASP API Security Top 10 (2023) esto aparece repartido en varias entradas: API2 (autenticación rota), API5 (autorización a nivel de función rota), API8 (configuración de seguridad incorrecta) e API9 (inventario de API mal gestionado). En el OWASP Top 10 de aplicaciones web de 2021, la primera posición es precisamente el control de acceso roto (A01).

decode() frente a verify()

En la librería jsonwebtoken de Node, jwt.decode() solo lee: parte el token, deshace el Base64url y devuelve la carga. No comprueba la firma, ni la caducidad, ni nada. jwt.verify() recalcula la firma con la clave, comprueba el algoritmo y los claims temporales, y solo entonces devuelve la carga.

// mal: la carga es texto que manda el cliente
const u = jwt.decode(token);

// bien: firma, algoritmo y claims comprobados
const u = jwt.verify(token, CLAVE, {
  algorithms: ['HS256'], issuer: 'mi-api', audience: 'mi-app'
});

Ojo con los nombres, que no son universales: en PyJWT la función se llama jwt.decode() y sí verifica cuando se le pasan la clave y los algoritmos, salvo que se desactive explícitamente. Lo que hay que mirar no es el nombre, sino si la firma se comprueba.

Y ninguna validación en el navegador cuenta como control de seguridad: el cliente está en manos de quien lo usa. Es experiencia de usuario, nada más.

Caducidad, revocación y claims

Un JWT firmado es autocontenido: el servidor no necesita consultar nada para aceptarlo. Eso es su ventaja y su problema, porque tampoco hay dónde revocarlo. La práctica habitual:

  • Caducidad corta en el token de acceso (exp), de minutos.
  • Un token de refresco guardado y revocable en el servidor, que se rota en cada uso; si aparece uno ya usado, se invalida toda la familia.
  • Si hace falta revocar antes de tiempo, una lista de denegación por jti, que devuelve algo de estado al servidor.
  • Verificar también iss y aud: una firma válida de otro emisor, o un token emitido para otra aplicación, sigue siendo un token ajeno.
  • Una tolerancia pequeña de reloj (unos segundos) para exp y nbf, no minutos.

Dónde se guarda el token en el navegador

Cookie HttpOnly; Secure; SameSitelocalStorage
¿Lo lee JavaScript?nosí, cualquier script de la página
Ante un XSSel atacante puede hacer peticiones, pero no llevarse el tokense lo lleva
Ante un CSRFel navegador la envía sola: hace falta SameSite (y token anti-CSRF si hay None)no se envía sola
Se envíaautomáticamente al dominioa mano, en Authorization: Bearer

Para una aplicación web con su propio backend, la opción por defecto razonable es una cookie HttpOnly, Secure y SameSite=Lax o Strict, con el prefijo __Host- si es posible. Ninguna opción arregla un XSS: solo cambia lo que el atacante se puede llevar.

Referencias

  • RFC 7519 (JWT), 7515 (JWS), 7516 (JWE), 7517 (JWK), 7518 (JWA) y 8725 (buenas prácticas).
  • OWASP ASVS, capítulos de autenticación, gestión de sesiones y control de acceso; la versión 5.0 dedica uno propio a los tokens autocontenidos.
  • OWASP Cheat Sheet Series: Session Management, JSON Web Token y Authorization.
  • OWASP API Security Top 10 (2023).

Cómo funciona el laboratorio

Entras en un edificio abandonado como Modo Dios: puedes ver el interior de los sistemas. Cada sala tiene un reto y, al resolverlo, una revelación con la explicación y una ficha de defensa con qué hacer en tu código.

  1. El Cofre Transparente. Firmado no es cifrado: qué hay realmente dentro de un JWT y quién puede leerlo.
  2. La Firma Rota. Una forja de tokens y dos servidores, uno antiguo y uno bien configurado: qué pasa cuando el servidor obedece a la cabecera.
  3. La Llave de Guardarropa. Tokens opacos y entropía: un generador débil frente a uno criptográfico, y por qué la diferencia es de categoría.
  4. La Puerta sin Guardián. Un mapa de rutas y sus respuestas HTTP: la ruta que se quedó sin middleware.
  5. La Sala del Espejo. Dos servidores casi idénticos y un token manipulado: decode() frente a verify().

Cada sala vale 200 puntos, con bonus por rapidez (+60 o +30). Cada intento fallido resta 15, la pista escrita 50, los Rayos X —el código interno de la sala— 25 la primera vez, y el decodificador de emergencia de la primera sala 75. Ninguna sala baja de cero, y lo que deduzcas por tu cuenta no cuesta nada: puedes usar la consola del navegador o las herramientas de hash, HMAC e inspector de JWT de Recursos.

Errores habituales

  • «Está en Base64, así que está protegido.» Base64 es un formato, no un candado.
  • «Si la firma no cuadra, la librería lo rechazará.» Solo si se le pide verificar, y con qué algoritmos.
  • Guardar en la carga lo que no debería ver el usuario: datos personales, claves internas, permisos que no le corresponde conocer.
  • Usar el nombre del proyecto como secreto HMAC, o el mismo en desarrollo y en producción, o dejarlo en el repositorio.
  • Fabricar identificadores con Math.random(), la hora o un contador.
  • Proteger rutas una a una y confiar en que nadie olvide ninguna.
  • Confundir autenticación con autorización: estar identificado no significa poder hacerlo todo.
  • Validar en el cliente y dar el trabajo por hecho.
  • Tokens que no caducan, o que caducan en días.

Notas para el profesorado

  • Es individual y dura unos 20 minutos. Funciona bien como entrada a una sesión de seguridad web: se juega primero y se explica después, con la experiencia de haber forjado un token todavía fresca.
  • No hay red: es una sola página HTML con su propia implementación de SHA-256 y HMAC-SHA256. Se puede abrir desde un pendrive en un aula sin conexión, y no envía nada a ningún sitio.
  • El proyector sirve para la puesta en común: abrir las fichas de defensa de cada sala y leerlas contra un fragmento de código real del propio alumnado o de un proyecto de clase.
  • La puntuación premia no pedir ayuda, pero la ayuda no es trampa. Si alguien acaba con mucha penalización, que repita intentando pagar menos.
  • Para alumnado de desarrollo, una ampliación natural es buscar en un proyecto propio dónde se emite y dónde se verifica el token, con qué algoritmo y con qué secreto.

Preguntas para la puesta en común:

  1. ¿Qué parte de un JWT puede leer cualquiera, y qué parte solo puede producir quien tiene la clave?
  2. ¿Por qué el fallo de alg: none no es un fallo de criptografía?
  3. ¿Qué tiene que pasar en un proyecto para que una ruta nueva nazca protegida sin que nadie se acuerde?
  4. Si tu aplicación guarda el token en localStorage, ¿qué consecuencias tiene un XSS? ¿Y si lo guarda en una cookie HttpOnly?

Entrar al laboratorio →