"¿Uso JWT o sesiones?" es la pregunta de autenticación más repetida del desarrollo moderno, y casi siempre se plantea mal: como si JWT fuera lo nuevo y las sesiones lo viejo. La realidad es que son dos modelos con compromisos opuestos, y elegir mal produce los bugs clásicos —el usuario que borra su cuenta y sigue logueado, el token robado imposible de revocar—. Aquí va la comparación honesta.
El modelo sesión: el servidor recuerda
Flujo clásico, décadas funcionando:
- El usuario envía credenciales.
- El servidor crea un registro de sesión (en memoria, Redis o base de datos) con ID aleatorio seguro.
- Ese ID viaja al cliente en una cookie
HttpOnlyy vuelve automáticamente en cada petición. - El servidor consulta el almacén: si la sesión existe y no expiró, la petición es válida.
Set-Cookie: sid=a8f5f167...; HttpOnly; Secure; SameSite=Lax; Path=/
La propiedad clave: el estado vive en el servidor. Logout = borrar el registro. Cambiar permisos = actualizar el registro. Banear = eliminarlo. Todo surte efecto inmediato porque cada petición valida contra la verdad actual.
El modelo JWT: el cliente lleva sus papeles
Un JSON Web Token empaqueta las afirmaciones (sub, role, exp...) firmadas por el servidor:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ← header (algoritmo)
.eyJzdWIiOiIxMjM0NSIsInJvbGUiOiJhZG0i...} ← payload (Base64url, legible)
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJVadQssw5c ← firma HMAC
El servidor no guarda nada: verifica la firma con su clave secreta (o pública, en RS256/EdDSA), comprueba exp, y confía en el payload. Stateless puro: cualquier instancia del backend valida sin consultar almacén compartido.
El matiz que sorprende a todos: el payload va solo codificado en Base64url. Cualquiera puede leerlo — el decodificador de JWT lo muestra en un segundo. La firma protege contra modificación, jamás contra lectura. Jamás metas datos sensibles dentro.
La tabla de compromisos real
| Aspecto | Sesiones + cookie | JWT |
|---|---|---|
| Revocación inmediata | Trivial (borrar registro) | Difícil (ver abajo) |
| Escalado horizontal | Requiere almacén compartido | Natural (stateless) |
| Almacenamiento cliente | Cookie HttpOnly (inaccesible a JS) | localStorage (XSS lo roba) o cookie |
| Tamaño por petición | ~40 bytes de ID | Creciente con el payload |
| Riesgo CSRF | Presente → mitigar con SameSite | Inexistente si va en header |
| Datos extra | Solo en servidor | Dentro del token (legible) |
| Microservicios / APIs móviles | Acoplado al almacén central | Encaja naturalmente |
Los dos problemas estructurales de JWT
1. No hay logout real. Si el token vale hasta exp, borrarlo del cliente no invalida nada: quien lo copió sigue usándolo horas. Las soluciones parciales tienen todas un coste:
- Vidas cortas (5-15 min): reduce ventana, no la elimina; multiplica refrescos.
- Lista negra de tokens revocados: acabas de reconstruir el estado server-side que JWT prometía evitar.
- Rotar claves: revoca TODO el mundo, no a uno.
2. El refresh token traslada el problema, no lo resuelve. El patrón habitual —access token corto + refresh token largo para renovarlo— mueve el activo valioso al refresh. Y ese sí suele almacenarse server-side o cifrado, volviendo al mundo con estado por la puerta de atrás. Es un buen patrón igualmente, pero seco: no es "JWT mágicamente sin estado", es sesiones disfrazadas con pasos extra.
El problema estructural de localStorage
Si guardas JWT en localStorage para enviarlos en headers Authorization, una única vulnerabilidad XSS entrega todos los tokens de todos los usuarios activos al atacante — sin rastro en el servidor. Las cookies HttpOnly existen precisamente para eso: JavaScript no puede leerlas aunque el XSS esté ahí. Con cookies vuelves a necesitar defensa CSRF (SameSite=Lax cubre la mayoría de casos hoy), que es un problema mucho menor y mejor resuelto que XSS.
Qué elegir, decidido sin ambigüedad
- Web app tradicional monolito (Next.js SSR, Django, Rails): sesiones con cookie HttpOnly. Revocación, simplicidad y madurez ganan. No necesitas JWT para esto.
- API consumida por móvil nativo + web + terceros: JWT de acceso corto + refresh token, con rotación de refresh detectable (reuso = robo → revocar familia).
- Microservicios: JWT firmado (idealmente asimétrico, RS256/EdDSA) propagado entre servicios internos; el gateway valida una vez y cada servicio verifica localmente sin llamar al auth center por petición.
- Híbrido sensato (el que usan muchos productos serios): sesión de navegador gestionada con cookie + intercambio por JWT efímero cuando hace falta llamar a otros dominios.
Y siempre, con ambos modelos: HTTPS obligatorio, contraseñas hasheadas con Argon2/bcrypt, rate limiting en el login y expiraciones cortas donde puedas. Para inspeccionar qué contiene exactamente un token —header, payload y expiración— pégalo en nuestro decodificador JWT; y si quieres entender la capa criptográfica debajo, la diferencia entre cifrado, hash y codificación aclara qué garantiza una firma HMAC.
Preguntas frecuentes
¿Puedo poner JWT en cookie? Sí, y es el mejor de ambos mundos para webs: revocación vía lista de jti en Redis (barata porque solo consulta en login/refresh) y protección XSS del HttpOnly.
¿RS256 o HS256? HS256 (simétrico) basta si solo un servicio valida. En cuanto varios servicios o terceros deben verificar sin poder firmar, asimétrico (RS256/ES256/EdDSA): pública para verificar, privada custodiada solo por el emisor.
¿Los JWT sustituyen a OAuth? No: OAuth 2.0 es el framework de delegación de autorización; JWT suele ser simplemente el formato de los access tokens que OAuth emite. Son capas distintas del mismo sistema.
Decodifica e inspecciona cualquier JWT con nuestro decodificador online, gratis y sin que el token salga de tu navegador.