Gestión de sesiones en aplicaciones web: cookies, JWTs y refresh tokens
Comparativa de gestión de sesiones para apps web: sesiones por cookie, JWTs y refresh tokens, con modelos de amenaza concretos y requisitos realistas de cierre de sesión.

Qué hace realmente la gestión de sesiones
Una sesión es cómo tu app responde a una pregunta después de que alguien inicia sesión: "¿Quién eres ahora?". Una vez que esa respuesta es fiable, la app puede decidir qué puede ver el usuario, qué puede cambiar y qué acciones deben bloquearse.
"Mantener la sesión iniciada" también es una elección de seguridad. Estás decidiendo cuánto tiempo debe seguir siendo válida una identidad de usuario, dónde vive la prueba de identidad y qué ocurre si esa prueba se copia.
La mayoría de las configuraciones de aplicaciones web se basan en tres piezas fundamentales:
- Sesiones en servidor basadas en cookies: el navegador almacena una cookie y el servidor consulta la sesión en cada petición.
- JWTs (tokens de acceso): el cliente envía un token firmado que el servidor puede verificar sin buscar en la base de datos.
- Refresh tokens: una credencial de vida más larga que se usa para obtener nuevos tokens de acceso de corta duración.
No son tanto "estilos" en competencia como distintas formas de manejar los mismos trade-offs: velocidad vs control, simplicidad vs flexibilidad, y "¿podemos invalidar esto ahora mismo?" vs "¿expira por sí solo?".
Una forma útil de evaluar cualquier diseño: si un atacante roba lo que tu app usa como comprobante (una cookie o un token), ¿qué puede hacer y por cuánto tiempo? Las sesiones por cookie suelen ganar cuando necesitas un control fuerte en el servidor, como forzar cierre de sesión o bloqueo instantáneo. Los JWTs pueden encajar bien para comprobaciones sin estado entre servicios, pero se vuelven problemáticos cuando necesitas revocación inmediata.
Ninguna opción gana en todos los casos. El enfoque correcto depende de tu modelo de amenazas, cuán estrictos son tus requisitos de cierre de sesión y cuánta complejidad puede mantener tu equipo.
Modelos de amenaza que cambian la respuesta correcta
Un buen diseño de sesiones depende menos del "mejor" tipo de token y más de qué ataques necesitas resistir realmente.
Si un atacante roba datos del almacenamiento del navegador (como localStorage), los tokens de acceso JWT son fáciles de capturar porque el JavaScript de la página puede leerlos. Una cookie robada es diferente: si está marcada como HttpOnly, el código normal de la página no puede leerla, así que los ataques de "robar token" se vuelven más difíciles. Pero si el atacante tiene el dispositivo (portátil perdido, malware, equipo compartido), las cookies aún pueden copiarse desde el perfil del navegador.
XSS (código atacante ejecutándose en tu página) lo cambia todo. Con XSS, el atacante puede no necesitar robar nada: puede usar la sesión ya iniciada de la víctima para realizar acciones. Las cookies HttpOnly ayudan a evitar la lectura de secretos de sesión, pero no impiden que un atacante realice peticiones desde la página.
CSRF (otro sitio que desencadena acciones no deseadas) amenaza principalmente a las sesiones basadas en cookies, porque los navegadores adjuntan cookies automáticamente. Si dependes de cookies, necesitas defensas claras contra CSRF: ajustes intencionales de SameSite, tokens anti-CSRF y manejo cuidadoso de peticiones que cambian estado. Los JWTs enviados en un encabezado Authorization están menos expuestos al CSRF clásico, pero siguen siendo vulnerables a XSS si se almacenan donde JavaScript pueda leerlos.
Los ataques de replay (reutilizar una credencial robada) son donde las sesiones en servidor sobresalen: puedes invalidar un ID de sesión inmediatamente. Los JWTs de corta duración reducen el tiempo de replay, pero no impiden el replay mientras el token sea válido.
Los dispositivos compartidos y los teléfonos perdidos convierten el "cerrar sesión" en un modelo de amenaza real. Las decisiones suelen reducirse a preguntas como: ¿puede un usuario forzar el cierre de sesión en otros dispositivos?, ¿qué tan rápido debe surtir efecto?, ¿qué pasa si se roban un refresh token?, y ¿permites sesiones tipo "recordarme"? Muchos equipos también aplican estándares más estrictos al acceso del personal que al de clientes, lo que cambia los tiempos de expiración y las expectativas de revocación.
Sesiones por cookie: cómo funcionan y qué protegen
Las sesiones basadas en cookies son la configuración clásica. Tras el inicio de sesión, el servidor crea un registro de sesión (a menudo un ID más campos como user ID, tiempo de creación y expiración). El navegador almacena solo el ID de sesión en una cookie. En cada petición, el navegador envía esa cookie y el servidor consulta la sesión para decidir quién es el usuario.
La gran ventaja de seguridad es el control. La sesión se valida en el servidor cada vez. Si necesitas expulsar a alguien, borras o deshabilitas el registro de sesión en servidor y deja de funcionar de inmediato, aunque el usuario todavía tenga la cookie.
Mucho de la protección viene de los ajustes de la cookie:
- HttpOnly: evita que JavaScript lea la cookie.
- Secure: envía la cookie solo por HTTPS.
- SameSite: limita cuándo el navegador envía la cookie en peticiones cross-site.
Dónde almacenas el estado de sesión afecta al escalado. Mantener sesiones en la memoria de la app es simple, pero falla cuando ejecutas múltiples servidores o reinicias con frecuencia. Una base de datos funciona bien para durabilidad. Redis es común cuando quieres búsquedas rápidas y muchas sesiones activas. El punto clave es el mismo: el servidor debe poder encontrar y validar la sesión en cada petición.
Las sesiones por cookie encajan bien cuando necesitas comportamiento de cierre de sesión estricto, como paneles de administración o portales de clientes donde un admin debe poder forzar el cierre tras un cambio de rol. Si un empleado se va, deshabilitar sus sesiones del servidor termina el acceso de inmediato, sin esperar a que expiren los tokens.
Tokens de acceso JWT: fortalezas y aristas
Un JWT (JSON Web Token) es una cadena firmada que contiene algunas claims sobre el usuario (como user ID, rol, tenant) más un tiempo de expiración. Tu API verifica la firma y la expiración localmente, sin llamar a una base de datos, y luego autoriza la petición.
Por eso los JWTs son populares en productos API-first, apps móviles y sistemas donde múltiples servicios deben validar la misma identidad. Si tienes múltiples instancias backend, cada una puede verificar el mismo token y obtener la misma respuesta.
Fortalezas
Los tokens de acceso JWT se verifican rápido y son fáciles de pasar entre llamadas de API. Si tu frontend llama a muchos endpoints, un token de acceso de corta duración puede mantener el flujo simple: verificar firma, leer user ID y continuar.
Ejemplo: un portal de clientes llama a "List invoices" y "Update profile" en servicios separados. Un JWT puede llevar el customer ID y un rol como customer, de modo que cada servicio pueda autorizar la petición sin consultar la sesión cada vez.
Aristas
La mayor compensación es la revocación. Si un token es válido por una hora, normalmente es válido en todas partes durante esa hora, incluso si el usuario pulsa "log out" o un admin deshabilita la cuenta, a menos que añadas comprobaciones adicionales en el servidor.
Los JWTs también se filtran de maneras ordinarias. Puntos de fallo comunes incluyen localStorage (XSS puede leerlo), la memoria del navegador (extensiones maliciosas), logs y reportes de errores, proxies y herramientas de analítica que capturan encabezados, y tokens copiados en chats de soporte o capturas de pantalla.
Por esto, los tokens de acceso JWT funcionan mejor para acceso de corta duración, no para "logeos para siempre". Mantenlos mínimos (sin datos personales sensibles dentro), ponles expiración corta y asume que un token robado será usable hasta que expire.
Refresh tokens: hacer que los JWT funcionen en la práctica
Los tokens de acceso JWT están pensados para ser de corta duración. Eso es bueno para seguridad, pero crea un problema práctico: los usuarios no deberían tener que iniciar sesión cada pocos minutos. Los refresh tokens solucionan eso permitiendo que la app obtenga silenciosamente un nuevo token de acceso cuando el anterior expira.
Dónde almacenas el refresh token importa aún más que dónde almacenas el access token. En una app web basada en navegador, el predeterminado más seguro es una cookie HttpOnly y Secure para que JavaScript no pueda leerla. El almacenamiento local es más fácil de implementar, pero también más fácil de robar si tienes un bug XSS. Si tu modelo de amenazas incluye XSS, evita poner secretos de larga duración en almacenamiento accesible por JavaScript.
La rotación es lo que hace que los refresh tokens funcionen en sistemas reales. En vez de usar el mismo refresh token durante semanas, lo intercambias cada vez que se usa: el cliente presenta el refresh token A, el servidor emite un nuevo token de acceso más el refresh token B, y el refresh token A queda invalidado.
Una configuración simple de rotación suele seguir algunas reglas:
- Mantén los access tokens cortos (minutos, no horas).
- Almacena los refresh tokens en servidor con estado y tiempo de último uso.
- Rota en cada refresh e invalida el token anterior.
- Ata los refresh tokens a un dispositivo o navegador cuando sea posible.
- Registra eventos de refresh para investigar abusos.
La detección de reuso es la alarma clave. Si el refresh token A ya fue intercambiado pero lo ves de nuevo más tarde, asume que se copió. Una respuesta común es revocar toda la sesión (y a menudo todas las sesiones del usuario) y requerir un nuevo inicio de sesión, porque no puedes saber cuál copia es la real.
Para el cierre de sesión necesitas algo que el servidor pueda hacer cumplir. Eso suele significar una tabla de sesiones (o una lista de revocación) que marque los refresh tokens como revocados. Los access tokens pueden seguir funcionando hasta que expiren, pero puedes mantener esa ventana pequeña manteniendo access tokens de corta duración.
Requisitos de cierre de sesión y lo que realmente puede aplicarse
Cerrar sesión suena simple hasta que lo defines. Normalmente hay dos peticiones distintas: "cerrar sesión en este dispositivo" (un navegador o un teléfono) y "cerrar sesión en todas partes" (todas las sesiones activas en todos los dispositivos).
También hay una pregunta de tiempo. "Cierre inmediato" significa que la app deja de aceptar la credencial ahora mismo. "Cerrar sesión tras expiración" significa que la app deja de aceptar la credencial cuando la sesión o el token expira de forma natural.
Con sesiones basadas en cookies, el cierre inmediato es sencillo porque el servidor controla la sesión. Borras la cookie en el cliente e invalidas el registro de sesión en el servidor. Si alguien copió el valor de la cookie antes, es la negativa del servidor la que aplica realmente el cierre.
Con autenticación solo por JWT (tokens de acceso sin estado y sin consulta al servidor), no puedes garantizar verdaderamente un cierre inmediato. Un JWT robado sigue siendo válido hasta que expire, porque el servidor no tiene dónde consultar "¿está este token revocado?". Puedes añadir una denylist, pero entonces estás manteniendo estado y revisándolo, lo que elimina gran parte de la simplicidad original.
Un patrón práctico es tratar los access tokens como de corta duración y aplicar el cierre mediante refresh tokens. El access token puede seguir siendo válido unos minutos, pero el refresh token es lo que mantiene viva la sesión. Si se roba un portátil, revocar la familia de refresh tokens corta el acceso futuro rápidamente.
Lo que puedes prometer razonablemente a los usuarios:
- Cerrar sesión en este dispositivo: revocar esa sesión o refresh token y borrar cookies o almacenamiento local.
- Cerrar sesión en todas partes: revocar todas las sesiones o todas las familias de refresh tokens de la cuenta.
- Efecto "inmediato": garantizado con sesiones en servidor; con access tokens, es mejor esfuerzo hasta que expiren.
- Eventos de cierre forzado: cambio de contraseña, cuenta deshabilitada, degradación de rol.
Para cambios de contraseña y deshabilitaciones, no confíes en "el usuario cerrará la sesión". Almacena una versión de sesión a nivel de cuenta (o un timestamp de "tokens válidos después de"). En cada refresh (y a veces en cada petición), compárala. Si cambió, deniega y exige volver a iniciar sesión.
Paso a paso: elegir un enfoque de sesiones para tu app
Si quieres que el diseño de sesiones sea simple, decide tus reglas primero y luego elige la mecánica. La mayoría de los problemas empiezan cuando los equipos eligen JWTs o cookies porque son populares, no porque coincidan con los riesgos y requisitos de cierre de sesión.
Empieza listando cada lugar donde un usuario inicia sesión. Una app web se comporta diferente a una app nativa móvil, una herramienta interna de administración o una integración de un tercero. Cada uno cambia qué puede almacenarse de forma segura, cómo se renuevan los inicios y qué debe significar "cerrar sesión".
Un orden práctico que funciona para la mayoría de equipos:
- Enumera tus clientes: web, iOS/Android, herramientas internas, acceso de terceros.
- Elige un modelo de amenazas por defecto: XSS, CSRF, dispositivo robado.
- Decide qué debe garantizar el cierre de sesión: este dispositivo, todos los dispositivos, cierre forzado por admin.
- Elige un patrón base: sesiones por cookie (el servidor recuerda) o access token + refresh token.
- Define timeouts y reglas de respuesta: expiración por inactividad vs absoluta, y qué haces al detectar reuso sospechoso.
Luego documenta las promesas exactas que hace tu sistema. Ejemplo: "Las sesiones web expiran tras 30 minutos de inactividad o 7 días absolutos. El admin puede forzar el cierre en 60 segundos. Un teléfono perdido puede deshabilitarse de forma remota." Esas frases importan más que la librería que uses.
Finalmente, añade monitorización que coincida con tu patrón. Para configuraciones con tokens, una señal fuerte es el reuso de refresh tokens (el mismo refresh token usado dos veces). Trátalo como robo probable, revoca la familia de sesiones y alerta al usuario.
Errores comunes que conducen a toma de control de cuentas
La mayoría de las tomas de cuenta no son "hackeos inteligentes". Son victorias fáciles causadas por errores previsibles en las sesiones. Manejar sesiones bien es, en gran parte, no dar a los atacantes una forma sencilla de robar o reproducir credenciales.
Una trampa común es poner access tokens en localStorage y confiar en que nunca tendrás XSS. Si cualquier script se ejecuta en tu página (una dependencia maligna, un widget inyectado, un comentario almacenado), puede leer localStorage y enviar el token. Las cookies con la bandera HttpOnly reducen ese riesgo porque JavaScript no puede leerlas.
Otra trampa es hacer JWTs de larga duración para evitar refresh tokens. Un token de acceso de 7 días es una ventana de reuso de 7 días si se filtra. Un access token corto más un refresh token bien gestionado es más difícil de abusar, especialmente cuando puedes cortar el refresh.
Las cookies traen su propia trampa: olvidar las defensas CSRF. Si tu app usa sesiones por cookie y aceptas peticiones que cambian estado sin protección CSRF, un sitio malicioso puede engañar a un navegador logueado para enviar peticiones válidas.
Otros errores que aparecen a menudo en revisiones de incidentes:
- Refresh tokens que nunca rotan, o que rotan pero no se detecta el reuso.
- Soportar múltiples métodos de login (sesión por cookie y bearer token) pero la regla del servidor de "cuál prevalece" no está clara.
- Tokens que terminan en logs (consola del navegador, eventos de analítica, logs de servidor), donde se copian y retienen.
Un ejemplo concreto: un agente de soporte pega un "log de depuración" en un ticket. El log incluye un encabezado Authorization. Cualquiera con acceso al ticket puede reproducir ese token y actuar como el agente. Trata los tokens como contraseñas: no los imprimas, no los almacenes y mantenlos de corta duración.
Comprobaciones rápidas antes de lanzar
La mayoría de los fallos de sesiones no van de criptografía sofisticada. Van de una bandera faltante, un token que vive demasiado o un endpoint que debería haber requerido re-autenticación.
Antes de lanzar, haz una pasada corta centrada en qué puede hacer un atacante con una cookie o token robado. Es una de las formas más rápidas de mejorar la seguridad sin reescribir todo tu sistema de autenticación.
Lista de comprobación previa al lanzamiento
Revisa esto en staging y luego otra vez en producción:
- Mantén los access tokens con vida corta (minutos) y confirma que la API realmente los rechaza tras expirar.
- Trata los refresh tokens como contraseñas: almacénalos donde JavaScript no pueda leerlos si es posible, envíalos solo al endpoint de refresh y rótalos después de cada uso.
- Si usas cookies para auth, verifica las banderas: HttpOnly activada, Secure activada y SameSite configurado intencionalmente. También confirma que el scope de la cookie (dominio y path) no sea más amplio del necesario.
- Si las cookies autentican peticiones, añade defensas CSRF y confirma que los endpoints que cambian estado fallan sin la señal CSRF.
- Haz que la revocación sea real: después de un restablecimiento de contraseña o deshabilitar una cuenta, las sesiones existentes deberían dejar de funcionar rápidamente (borrado de sesión en servidor, invalidación de refresh token o una comprobación de "versión de sesión").
Después de eso, prueba tus promesas de cierre de sesión. "Cerrar sesión" suele significar "eliminar la sesión local", pero los usuarios esperan más.
Una prueba práctica: inicia sesión en un portátil y en un teléfono, luego cambia la contraseña. El portátil debe salir forzado en su siguiente petición, no horas después. Si ofreces "cerrar sesión en todas partes" y una lista de dispositivos, confirma que cada dispositivo se mapea a un registro de sesión o refresh token distinto que puedas revocar.
Ejemplo: un portal de clientes con cuentas de personal y cierre forzado
Imagina una pequeña empresa con un portal web de clientes (los clientes consultan facturas, abren tickets) y una app móvil para el personal de campo (trabajos, notas, fotos). El personal a veces trabaja en sótanos sin señal, así que la app debe seguir funcionando sin conexión por un rato. Los admins además quieren un gran botón rojo: si se pierde una tablet o un contratista se va, puedan forzar un cierre de sesión.
Ahora añade tres amenazas comunes: tablets compartidas en furgonetas (alguien olvida cerrar sesión), phishing (un empleado escribe credenciales en una página falsa) y un bug ocasional de XSS en el portal (un script se ejecuta en el navegador y trata de robar lo que pueda).
Una configuración práctica aquí son access tokens de corta duración más refresh tokens rotados, con revocación del lado servidor. Te da llamadas de API rápidas y tolerancia offline, y a la vez permite a los admins cortar sesiones.
Así puede verse:
- Vida del access token: 5 a 15 minutos.
- Rotación de refresh tokens: en cada refresh se devuelve un nuevo refresh token y el anterior queda invalidado.
- Almacena los refresh tokens de forma segura: en web, guarda el refresh token en una cookie HttpOnly y Secure; en móvil, guárdalo en el almacén seguro del SO.
- Rastrea los refresh tokens en servidor: guarda un registro de token (usuario, dispositivo, hora de emisión, último uso, bandera de revocado). Si un token rotado se reutiliza, trátalo como robo y revoca toda la cadena.
El cierre forzado se vuelve aplicable: el admin revoca el registro de refresh token de ese dispositivo (o todos los dispositivos del usuario). El dispositivo robado puede seguir usando el access token actual hasta que expire, pero no puede obtener uno nuevo. Así que el tiempo máximo para cortar completamente el acceso es la vida de tu access token.
Para un dispositivo perdido, define la regla en lenguaje claro: "En 10 minutos, la app dejará de sincronizar y requerirá volver a iniciar sesión." El trabajo offline puede permanecer en el dispositivo, pero el siguiente sync online fallará hasta que el usuario inicie sesión.
Siguientes pasos: implementar, probar y mantenerlo manejable
Escribe en palabras llanas qué significa "cerrar sesión". Por ejemplo: "Cerrar sesión elimina el acceso en este dispositivo", "Cerrar sesión en todas partes expulsa todos los dispositivos en 1 minuto" o "Cambiar la contraseña cierra otras sesiones". Esas promesas deciden si necesitas estado de sesión en servidor, listas de revocación o tokens de corta duración.
Convierte las promesas en un pequeño plan de pruebas. Los bugs de tokens y sesiones suelen parecer correctos en demos de flujo feliz y fallan en la vida real (modo suspensión, redes inestables, múltiples dispositivos).
Lista práctica de pruebas
Realiza pruebas que cubran los casos complicados:
- Expiración: el acceso debe detenerse cuando el access token o la sesión expira, incluso si el navegador sigue abierto.
- Revocación: tras "cerrar sesión en todas partes", la credencial antigua debe fallar en la siguiente petición.
- Rotación: la rotación de refresh debe emitir un nuevo refresh token e invalidar el anterior.
- Detección de reuso: reproducir un refresh token antiguo debe disparar una respuesta de bloqueo.
- Multi-dispositivo: las reglas de "solo este dispositivo" vs "todos los dispositivos" se aplican y la UI lo refleja.
Después de las pruebas, haz un simulacro de ataque simple con tu equipo. Elige tres historias y recórrelas end-to-end: un bug XSS que lee tokens, un intento CSRF contra sesiones por cookie y un teléfono robado con una sesión activa. Estás comprobando si el diseño coincide con las promesas.
Si necesitas moverte rápido, reduce el código glue personalizado. AppMaster (appmaster.io) es una opción cuando quieres un backend generado listo para producción más apps web y nativas, de modo que puedas mantener reglas como expiración, rotación y cierre forzado consistentes entre clientes.
Programa una revisión posterior al lanzamiento. Usa tickets de soporte reales e incidentes para ajustar timeouts, límites de sesión y comportamiento de "cerrar sesión en todas partes", y luego vuelve a ejecutar la misma lista para que las correcciones no regresen por regresiones.


