JWT vs cookies de sesión: en qué se diferencian y cuándo usar cada uno

Una cookie de sesión es la llave de un estado que se guarda en el servidor; un JWT lleva ese estado firmado hasta el servidor. De esa única diferencia salen todas las ventajas e inconvenientes.

La comparación suele plantearse como JWT contra cookies, pero no son el mismo tipo de cosa. Una cookie es una forma de enviar datos con cada petición. Un JWT es un formato de datos. La elección real está entre una sesión en el servidor, en la que el navegador guarda un ID aleatorio y el servidor consulta a quién pertenece, y un token autocontenido, en el que el cliente guarda afirmaciones (claims) firmadas que el servidor puede comprobar sin consultar nada. Con eso claro, las dudas sobre revocación, almacenamiento y CSRF encajan por sí solas.

Cómo funciona una sesión en el servidor

Tras un inicio de sesión correcto, el servidor crea un registro de sesión (ID de usuario, roles, caducidad) en memoria, en una base de datos o en un almacén como Redis, y envía al navegador un ID imposible de adivinar dentro de una cookie. El navegador adjunta la cookie a todas las peticiones posteriores a ese sitio, y el servidor carga el registro en cada una. El ID no significa nada por sí mismo; 32 bytes aleatorios se codifican en 43 caracteres.

Set-Cookie: __Host-session=p-sLYX7J3ziMKxq-dadVMuQJNSVBHM5hrw7MJPihGQ0; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=86400

El prefijo __Host- hace que los navegadores rechacen la cookie salvo que sea Secure, tenga Path=/ y no fije ningún Domain, de modo que un subdominio no puede sobrescribirla.

Cómo funciona un JWT

Un JSON Web Token (RFC 7519) son tres partes codificadas en Base64URL y unidas por puntos: una cabecera que indica el algoritmo, un payload con claims como sub y exp, y una firma sobre las dos primeras partes. El servidor comprueba la firma y la caducidad con una clave y confía en los claims sin preguntar a una base de datos. Normalmente se envía en una cabecera Authorization: Bearer.

El payload solo está codificado, no cifrado. Cualquiera que tenga el token puede leerlo, algo que puede comprobar pegando uno en el decodificador JWT. No meta nunca en un JWT secretos ni datos personales que no le mostraría al propio usuario. Existen tokens cifrados (JWE, RFC 7516), pero la mayoría de los JWT en uso solo van firmados. Para entender por qué las partes usan - y _ en lugar de + y /, consulte Base64 vs Base64URL.

Este es un token de ejemplo y el código que lee su payload en el navegador. Como se ve, no hace falta ninguna clave:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzg0MTIiLCJpYXQiOjE3NjAwMDAwMDAsImV4cCI6MTc2MDAwMDkwMH0.08BFm85KQIp34VSetcibqwBVLpHKR3IZhI_IPNeTif4

// Reading the payload needs no key at all (browser):
const part = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
const bytes = Uint8Array.from(atob(part), c => c.charCodeAt(0));
JSON.parse(new TextDecoder().decode(bytes));
// { sub: 'user_8412', iat: 1760000000, exp: 1760000900 }

Ventajas e inconvenientes, uno al lado del otro

Sesión en el servidorJWT
El cliente guardaUn ID aleatorio y sin significadoClaims firmados y legibles
El servidor necesitaUn almacén de sesiones compartido por todos los servidoresSolo la clave para verificar firmas
Cerrar la sesión de un usuario en todas partesBorrar la sesión: inmediatoImposible antes de exp sin estado adicional
Cambio de rol o de permisosSe aplica en la siguiente peticiónSe aplica cuando se emite un token nuevo
Transporte habitualCookie, enviada automáticamenteCabecera Authorization, o una cookie
Tamaño por peticiónID de 43 caracteres en nuestro ejemploDe 152 a 540 caracteres en nuestros ejemplos

Revocación, caducidad y refresh tokens

No guardar estado (statelessness) es el principal argumento a favor de los JWT y también su principal debilidad. Un token válido sigue siéndolo hasta que caduca, aunque el usuario cierre sesión, cambie la contraseña o sea bloqueado. La respuesta habitual es que los access tokens duren poco, a menudo minutos en lugar de horas, y acompañarlos de un refresh token de mayor duración que el cliente canjea por nuevos access tokens. El refresh token se comprueba contra un estado guardado en el servidor, así que al revocarlo se deja de emitir access tokens nuevos.

La OAuth 2.0 Security Best Current Practice (RFC 9700) describe la rotación de refresh tokens: cada renovación devuelve un refresh token nuevo e invalida el anterior, de modo que se puede detectar la reutilización de un token robado. Si además necesita revocar al instante los access tokens, acabará manteniendo una lista de denegación de IDs de token (jti) o una versión de token por usuario, lo que vuelve a ser estado en el servidor. Llegado ese punto, valore si una sesión normal no sería más sencilla.

Dónde guardar el token: cookies, localStorage y XSS

Todo lo que esté en localStorage o sessionStorage lo puede leer cualquier script que se ejecute en su página. Basta un fallo de cross-site scripting (XSS) o un script de terceros comprometido para copiar el token y usarlo desde cualquier lugar hasta que caduque.

Una cookie marcada como HttpOnly no es accesible desde JavaScript, así que un XSS no puede robarla. Aun así, un XSS puede enviar peticiones desde la página abierta de la víctima, y el navegador adjuntará la cookie, de modo que HttpOnly limita el daño en lugar de evitarlo. Prevenir el XSS sigue siendo imprescindible en cualquier caso; las cookies HttpOnly simplemente hacen que un ataque logrado le sirva de menos al atacante.

CSRF y SameSite

La comodidad de la cookie es también su riesgo: el navegador la adjunta automáticamente, incluso en peticiones provocadas por otro sitio web. Eso es la falsificación de peticiones entre sitios (CSRF). Los tokens enviados en una cabecera Authorization no están expuestos al CSRF clásico, porque el navegador no adjunta un bearer token por su cuenta; tiene que añadirlo su propio código.

  • SameSite=Strict: la cookie no se envía nunca en peticiones iniciadas desde otro sitio, ni siquiera al seguir un enlace hacia el suyo.
  • SameSite=Lax: se envía cuando el usuario llega a su sitio con un enlace normal (un GET de nivel superior), pero no en envíos de formularios entre sitios, peticiones fetch ni recursos incrustados. Chrome aplica Lax a las cookies que no fijan el atributo; indíquelo de forma explícita en lugar de depender de eso.
  • SameSite=None: se envía en todos los casos y debe combinarse con Secure (si no, Chrome la rechaza). Solo hace falta para usos realmente entre sitios, como widgets incrustados.

«Mismo sitio» significa el mismo dominio registrable, así que app.example.com y forum.example.com cuentan como el mismo sitio. SameSite no le protege de un subdominio comprometido. Para las peticiones que cambian estado, mantenga también un token CSRF o compruebe la cabecera Origin.

Tamaño del token

Un ID de sesión sigue siendo diminuto por mucho que sepa del usuario. Un JWT crece con cada claim y según el algoritmo de firma. Los tokens que generamos para esta guía ocupaban 152 caracteres con HS256 y tres claims, 241 caracteres al añadir nombre, correo electrónico y rol, y 540 caracteres con esos mismos claims firmados con RS256 y una clave de 2048 bits. El RFC 6265 solo obliga a los navegadores a admitir cookies de al menos 4096 bytes, contando nombre y atributos, así que un JWT cargado de permisos puede no caber en una cookie. Y ese tamaño viaja en todas y cada una de las peticiones.

Errores que rompen la seguridad de un JWT

  • Confiar en la cabecera alg. Fije los algoritmos que acepta su servidor. Aceptar none, o dejar que un token pase de RS256 a HS256 para que la clave pública se use como secreto HMAC, permite a los atacantes falsificar tokens.
  • Decodificar en lugar de verificar. Leer el payload no demuestra nada; verifique antes la firma con una biblioteca mantenida.
  • Saltarse la comprobación de claims. Valide exp, nbf, iss y aud, para que un token emitido para un servicio no lo acepte otro.

El RFC 8725, las JWT Best Current Practices, trata estos errores y algunos más.

Cuándo encaja cada uno

SituaciónLo que suele encajar mejor
Aplicación web renderizada en el servidor, en un solo dominioCookie de sesión
Aplicación de una sola página servida desde el mismo sitio que su APICookie de sesión, o un backend-for-frontend que guarde los tokens en el servidor
Aplicación móvil nativaAccess token de corta duración más refresh token en el almacenamiento seguro de la plataforma
Microservicios que verifican peticiones sin llamar a un servicio de autenticaciónAccess tokens JWT firmados con una clave asimétrica, como RS256 o ES256
Acceso de terceros a su APIAccess tokens de OAuth 2.0, que a menudo son JWT

El modelo híbrido: un JWT dentro de una cookie HttpOnly

Muchas aplicaciones guardan el propio JWT en una cookie HttpOnly; Secure; SameSite. Los scripts no pueden leerlo y los servidores pueden seguir verificándolo sin consultar una sesión. Pero los inconvenientes también vienen incluidos: la cookie se envía automáticamente, así que vuelven a hacer falta defensas contra CSRF; se aplica el límite de 4 KB de las cookies; y revocarlo es exactamente igual de difícil que con cualquier otro JWT. Es un diseño sólido para una aplicación de navegador apoyada en APIs sin estado, siempre que lo trate como una cookie en lo que respecta al CSRF y como un JWT en lo que respecta a la revocación.

Preguntas Frecuentes

¿JWT es más seguro que las cookies de sesión?

No, ninguno es más seguro por naturaleza. Las sesiones son más fáciles de revocar y no revelan nada al cliente. Los JWT evitan consultar la sesión, pero son difíciles de revocar antes de que caduquen. Importa más dónde se guarda la credencial y cómo se gestionan el CSRF y el XSS que el formato.

¿Debo guardar un JWT en localStorage?

Es sencillo, pero expone el token a cualquier script de la página, así que un solo fallo XSS puede filtrarlo. Una cookie HttpOnly y Secure con un atributo SameSite explícito lo deja fuera del alcance de JavaScript, a cambio de necesitar protección contra CSRF.

¿Se puede invalidar un JWT?

No por sí solo. Un token firmado sigue siendo válido hasta su hora exp. Para cortar el acceso antes, haga que los access tokens duren poco, revoque el refresh token o compruebe los IDs de token contra una lista de denegación, lo que añade estado en el servidor.

¿Los datos de un JWT van cifrados?

Normalmente no. La cabecera y el payload son JSON codificado en Base64URL que cualquiera puede decodificar; solo la firma impide modificarlos. Use JWE si el contenido debe ser confidencial, o deje los datos sensibles fuera del token.