Cookies vs localStorage para Autenticación con JWT
Ya construiste tu flujo de autenticación, los JWT están funcionando y ahora te enfrentas a la misma pregunta que todo desarrollador frontend termina haciéndose: ¿dónde pongo realmente este token? La respuesta importa más de lo que la mayoría de los tutoriales reconocen, y el consejo habitual —“simplemente usa cookies HttpOnly”— omite compromisos reales que necesitas entender.
A continuación encontrarás un desglose claro de ambas opciones, contra qué protege cada una y cómo las aplicaciones modernas abordan esto en la práctica.
Puntos Clave
localStoragees completamente accesible para cualquier JavaScript que se ejecute en tu página, lo que lo hace vulnerable al robo de tokens mediante XSS.- Las cookies
HttpOnlybloquean el acceso de JavaScript por completo, pero introducen riesgo de CSRF, que los atributosSameSiteySecuremitigan. - El patrón moderno almacena tokens de acceso de corta duración en memoria y tokens de refresco en cookies
HttpOnly,SecureySameSite. - Las guías de OWASP y de aplicaciones basadas en navegador para OAuth desaconsejan colocar tokens de larga duración en
localStorage. - La elección correcta depende de tu modelo de amenazas, del control que tengas sobre tu backend y de si tu API requiere encabezados
Authorization.
Qué Está Realmente en Juego con el Almacenamiento de JWT
El lugar donde almacenas un JWT determina qué vectores de ataque aplican a tu aplicación. Las dos amenazas principales son:
- XSS (Cross-Site Scripting): JavaScript malicioso que se ejecuta en el contexto de tu aplicación.
- CSRF (Cross-Site Request Forgery): Engañar al navegador del usuario para que realice solicitudes autenticadas no deseadas.
Ninguna opción de almacenamiento elimina ambos riesgos simultáneamente. El objetivo es entender qué riesgo estás aceptando y cómo mitigarlo.
localStorage: Conveniente, pero Accesible desde JavaScript
Almacenar un JWT en localStorage es sencillo. Lo escribes, lo lees y lo adjuntas manualmente a los encabezados Authorization: Bearer. Funciona bien con APIs que esperan ese formato de encabezado.
El problema es que localStorage es completamente accesible para cualquier JavaScript que se ejecute en tu página. Si un atacante logra inyectar un script —a través de una vulnerabilidad en una dependencia, un CDN comprometido o una falla XSS en tu propio código— puede leer el token directamente y exfiltrarlo. OWASP desaconseja explícitamente almacenar identificadores de sesión en localStorage por esta razón.
Esto no es teórico. Las aplicaciones web modernas incorporan decenas de scripts de terceros, y cada uno representa una superficie de ataque potencial.
Cookies HttpOnly: Mejor Resistencia a XSS, Nuevas Consideraciones
Una cookie HttpOnly no puede ser leída por JavaScript en absoluto. Incluso si un atacante ejecuta código en tu página, no puede extraer el valor del token. Eso representa una mejora significativa.
Sin embargo, las cookies introducen exposición a CSRF. Los navegadores adjuntan automáticamente las cookies a las solicitudes coincidentes, incluidas las provocadas por sitios maliciosos de terceros.
Tres atributos de cookies trabajan en conjunto para cerrar esa brecha:
HttpOnly— bloquea completamente el acceso de JavaScript.Secure— transmite la cookie únicamente a través de HTTPS.SameSite— controla cuándo se envían las cookies en solicitudes entre sitios.
En cuanto a SameSite, los navegadores modernos aplican Lax por defecto cuando el atributo no está definido, lo que bloquea las cookies en sub-solicitudes entre sitios (como los POST desde otro origen) pero las permite en navegaciones de nivel superior. Strict es más conservador e impide que la cookie se envíe en cualquier solicitud entre sitios, incluidas las navegaciones de nivel superior. Establece siempre este atributo de forma explícita en lugar de depender de los valores predeterminados del navegador. La compatibilidad de SameSite en navegadores modernos es excelente y puede verificarse en Can I Use.
Con SameSite=Strict o Lax correctamente configurado, el riesgo de CSRF se reduce sustancialmente para la mayoría de los esquemas de autenticación en el mismo sitio. Para endpoints sensibles que modifican estado, combina esto con un token anti-CSRF para lograr defensa en profundidad.
Discover how at OpenReplay.com.
El Patrón que Usan la Mayoría de las Aplicaciones Modernas
Muchas aplicaciones en producción dividen el problema:
- Tokens de acceso de corta duración almacenados en memoria JavaScript (una variable a nivel de módulo o estado de React).
- Tokens de refresco almacenados en una cookie
HttpOnly,SecureySameSite.
El token de acceso desaparece al cerrar la pestaña o al recargar la página, pero una llamada silenciosa a tu endpoint /refresh recupera uno nuevo utilizando la cookie. El token de acceso nunca toca el almacenamiento persistente, y el token de refresco nunca es legible por JavaScript.
Este enfoque se alinea con las guías actuales para aplicaciones basadas en navegador que usan OAuth 2.0 con PKCE (flujo de código de autorización con PKCE), que es lo que recomienda la guía OAuth 2.0 for Browser-Based Apps. Si trabajas con OpenID Connect (OIDC), el mismo patrón aplica: mantén los ID tokens y los tokens de refresco fuera de localStorage.
Lista de Verificación de Auditoría de Seguridad
Antes de desplegar a producción, verifica:
- Flag
HttpOnlyestablecido en cualquier cookie que contenga tokens. - Flag
Securehabilitado (HTTPS obligatorio). SameSiteestablecido explícitamente enStrictoLax.- Los tokens de acceso son de corta duración, típicamente medidos en minutos en lugar de horas.
- Encabezados de Content Security Policy configurados.
- Ningún JWT de larga duración almacenado en
localStorage.
Cómo Elegir el Enfoque Correcto para tu Aplicación
No existe una respuesta universal. Si controlas tu backend y sirves tu aplicación desde el mismo dominio, las cookies HttpOnly con una configuración adecuada de SameSite son la opción predeterminada más sólida. Si estás integrando con una API de terceros que requiere encabezados Authorization y no puedes establecer cookies del lado del servidor, el almacenamiento en memoria con expiración corta es una alternativa razonable —simplemente nunca persistas tokens de larga duración en localStorage.
Conclusión
Los JWT de larga duración en localStorage son lo que las guías de seguridad actuales desaconsejan de manera consistente. Las cookies HttpOnly con los atributos Secure y SameSite ofrecen la opción predeterminada más sólida para la mayoría de los esquemas en el mismo dominio, mientras que el almacenamiento en memoria combinado con una cookie de token de refresco cubre los casos más complejos. Una vez que comprendes el modelo de amenazas —XSS por un lado, CSRF por el otro—, la elección correcta para tu aplicación se convierte en un compromiso sobre el que puedes razonar con claridad, en lugar de una suposición.
Preguntas Frecuentes
sessionStorage comparte la misma debilidad que localStorage: cualquier JavaScript que se ejecute en la página puede leerlo. La única diferencia es que sessionStorage se borra cuando se cierra la pestaña. Eso reduce la ventana de exposición, pero no protege contra XSS. Para el almacenamiento de tokens, trata sessionStorage con la misma precaución que localStorage y evita colocar tokens de larga duración allí.
SameSite=Strict impide que las cookies se envíen en solicitudes entre sitios, lo que bloquea la mayoría de los patrones de ataque CSRF. Sin embargo, para endpoints de alto valor que modifican estado, agregar un token anti-CSRF te proporciona defensa en profundidad. SameSite es aplicado por el navegador, por lo que clientes más antiguos o casos extremos inusuales pueden no respetarlo. El patrón de doble envío de token sigue siendo una salvaguarda sensata.
Un rango habitual es de 5 a 15 minutos. Lo suficientemente corto como para que un token robado tenga un valor limitado, pero lo suficientemente largo como para no saturar tu endpoint de refresco. Combina esto con un token de refresco de mayor duración (horas o días) en una cookie HttpOnly. Si tu aplicación maneja operaciones sensibles como pagos, inclínate por una expiración más corta y exige reautenticación para acciones críticas.
Almacena el token de acceso en memoria JavaScript —una variable de módulo, estado de React o un closure— en lugar de en localStorage. Mantenlo de corta duración y refrésalo a través de un endpoint de backend cuando sea posible. Si debes persistir algo entre recargas, enruta el flujo de refresco a través de tu propio backend, que mantendrá la credencial de larga duración del lado del servidor, y nunca expongas tokens de larga duración al almacenamiento del cliente.
Gain control over your UX
See how users are using your site as if you were sitting next to them, learn and iterate faster with OpenReplay. — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.