Dónde se guarda la sesión: el debate del JWT está mal planteado
La discusión sobre autenticación en español lleva años atascada en dónde guardar el token y esa es la segunda pregunta. La primera es quién decide cuándo se acaba una sesión: si la respuesta es «el propio token», no puedes echar a nadie hasta que caduque y eso cambia el diseño entero.
El debate que se repite
Buscas cómo autenticar en tu producto y encuentras siempre la misma discusión: localStorage
frente a cookie HttpOnly. Sobre eso hay consenso desde hace años y conviene despacharlo rápido:
localStoragees legible por cualquier JavaScript de la página. Si entra un script por una dependencia comprometida o por un XSS, el token se lee y se lleva. OWASP desaconseja explícitamente guardar identificadores de sesión ahí.- Una cookie
HttpOnlyno la puede leer el JavaScript, ni el tuyo ni el del atacante. - La práctica corriente hoy es token de acceso corto en memoria y token de refresco en
cookie
HttpOnly,SecureySameSite.
Hasta aquí, lo que dice cualquier guía. El problema es que resolver eso deja intacta la pregunta importante.
La pregunta que casi nadie hace
Un JWT es un dato firmado: el servidor lo valida comprobando la firma, sin mirar en ninguna parte. Esa es toda su gracia —no hace falta consultar nada— y es exactamente de donde viene su problema.
Si no consultas nada, no puedes saber que ese token ya no vale.
Cuatro situaciones normales, de las que pasan cada semana en un producto con clientes:
| Situación | Con sesión en servidor | Con JWT autocontenido |
|---|---|---|
| Un empleado deja la empresa y le quitan el acceso | Deja de entrar en el acto | Sigue entrando hasta que caduque |
| A alguien le bajan el rol de administrador | Efecto inmediato | Conserva permisos de admin hasta que caduque |
| Un cliente cierra sesión en un ordenador ajeno | La sesión se destruye | El token sigue siendo válido |
| Sospechas de una cuenta comprometida | La cierras y ya está | No hay forma de cerrarla |
La columna de la derecha describe un JWT usado exactamente como sugiere su documentación. No hay nada mal implementado en ella.
La caja del medio es toda la diferencia: si no se consulta nada, no hay forma de saber que ese acceso ya no existe.
Los apaños y lo que cuesta cada uno
Todo el mundo llega al mismo sitio y resuelve de alguna de estas tres maneras. Cada una tiene su precio.
Caducidad muy corta. Tokens de cinco minutos y refresco continuo. La ventana de exposición se acorta sin llegar a desaparecer y multiplicas las peticiones de refresco. Además el token de refresco sí hay que poder revocarlo, así que vuelves a necesitar estado en el servidor: acabas de reinventar la sesión, solo que con dos piezas en vez de una.
Lista de revocados. Guardas los identificadores de los tokens invalidados y los consultas en cada petición. Funciona, pero acabas de renunciar a la única ventaja del JWT: ya estás consultando un almacén en cada petición, igual que con una sesión, pero con más complejidad.
Versión de credenciales. Un contador en el usuario que se incrementa al cambiar contraseña o permisos y que viaja dentro del token. Es el apaño más limpio, pero también hay que leerlo, así que otra vez estás consultando.
El patrón se repite: cada arreglo del problema de revocación te devuelve al estado en servidor, que es lo que el JWT venía a evitar.
Entonces, ¿qué se usa?
La regla que aplica Nimboo, que no es original ni pretende serlo:
Sesión en servidor para tus propios usuarios. JWT para lo que atraviesa una frontera.
Sesión en servidor —un identificador opaco en cookie HttpOnly y los datos en tu almacén— es
lo correcto para la aplicación web y móvil de tu producto. Se revoca en el acto, se puede listar
«dispositivos conectados», se cierra sesión de verdad y el coste es una lectura por petición a un
almacén que ya tienes.
JWT encaja donde el que valida no puede consultar a quien emitió: comunicación entre servicios distintos, integraciones con terceros, enlaces firmados de un solo uso o federación de identidad. Ahí la firma resuelve un problema real que la sesión no resuelve.
La confusión viene de que el JWT se popularizó como «la forma moderna de hacer login», cuando el problema que resuelve bien es otro.
Lo que sigue estando mal aunque uses sesiones
Cambiar de token a sesión no arregla lo que se rompe de verdad en las auditorías:
- Cerrar sesión no invalida en servidor. Borrar la cookie en el navegador deja el identificador válido para quien lo tuviera.
- La sesión no se renueva al elevar privilegios. Al iniciar sesión o cambiar de rol hay que emitir un identificador nuevo, o un atacante que fijó el anterior lo mantiene.
- Sin caducidad por inactividad. Una sesión que dura para siempre es un problema en cuanto alguien deja un portátil abierto.
- El segundo factor no llega a la sesión. Si el segundo factor solo se comprueba al entrar y luego la sesión vale días, la protección es menor de lo que parece.
SameSitemal puesto. Sin él, una petición desde otro sitio arrastra la cookie y ahí entra el CSRF.
Cuándo esto no importa
Si tu producto es una herramienta interna con usuarios que no cambian y sin datos de terceros, un JWT de una hora sin revocación es perfectamente razonable y te ahorra trabajo. El riesgo real es bajo y la simplicidad vale.
El umbral está en una pregunta concreta: ¿alguien puede necesitar que un acceso se corte antes de que caduque el token? En cuanto vendes a empresas, gestionas empleados de tu cliente o tocas datos personales, la respuesta es sí y ahí el JWT autocontenido deja de valer.
Y una recomendación que ahorra discusiones: empieza por sesión en servidor. Es más fácil de razonar y se revoca. Si algún día necesitas tokens firmados para hablar con otro sistema, se añaden encima sin rehacer nada. El camino contrario —de JWT a sesión— obliga a tocar el cliente, el servidor y el modelo de datos a la vez.
Vecinly gestiona comunidades con roles finos, donde alguien deja de ser presidente y su acceso tiene que cortarse ese mismo día. Está en su ficha de caso. Si tienes un prototipo con autenticación montada de cualquier manera, SaaS a medida empieza por revisar exactamente esto.