De localStorage a Cookies HttpOnly
Introducción
Cuando comencé a desarrollar Angotest - una plataforma de tests online con comparativas entre usuarios - mi enfoque inicial fue la funcionalidad. Usé localStorage para guardar tokens JWT porque era rápido y sencillo. Pero cuando decidí lanzarla al público para academias e institutos, una pregunta me perseguía: “¿Está realmente segura?”
La respuesta era clara: no lo suficiente. Hoy comparto cómo migré de localStorage a cookies HttpOnly y por qué esta decisión es esencial para cualquier aplicación que maneje datos sensibles.
El problema: localStorage y sus riesgos
Lo que hacía antes:
// auth.service.ts (versión antigua)
private readonly TOKEN_KEY = 'angotest_token';
private setAuthState(data: AuthResponse): void {
// ❌ Token accesible por JavaScript
localStorage.setItem(this.TOKEN_KEY, data.token);
this.tokenSignal.set(data.token);
}
Los riesgos:
- Vulnerable a XSS: Cualquier script malicioso podía leer el token
- Visible en DevTools: Cualquier usuario podía ver el token
- No se envía automáticamente: Cada petición requería configurar headers manualmente
- Sin protección CSRF: Fácil de explotar en ciertos escenarios
Para una aplicación educativa que maneja resultados de tests y datos personales de estudiantes, estos riesgos eran inaceptables.
La solución: Cookies HttpOnly
¿Por qué cookies HttpOnly?
- Inaccesibles a JavaScript: El navegador las maneja automáticamente
- Protección contra XSS: Aunque hay vulnerabilidades, el token no es legible
- Envío automático: Se incluyen en cada petición sin código adicional
- SameSite atributo: Protege contra CSRF
- Más seguro en general: Menos superficie de ataque
Implementación en el Backend (Go):
// controllers/auth.go - Versión mejorada
func Login(c *gin.Context) {
// ... validación de usuario ...
// Configurar cookie HttpOnly
c.SetSameSite(http.SameSiteStrictMode)
c.SetCookie(
"auth_token", // nombre
signed, // valor del token
24*60*60, // expiración (24h)
"/", // path
"", // dominio
isProduction, // secure (true en prod)
true, // httpOnly (CRUCIAL)
)
// Ya no enviamos el token en la respuesta JSON
c.JSON(http.StatusOK, gin.H{"user": userResponse})
}
Cambios en el Frontend (Angular):
// auth.service.ts - Versión mejorada
login(email: string, password: string): Observable<any> {
return this.http.post<{ user: User }>(
`${this.API_URL}/login`,
{ email, password },
{ withCredentials: true } // ¡Magia! Envía/recibe cookies automáticamente
)
}
Las mejoras clave implementadas
1. Eliminación del token del localStorage
- ❌ Antes:
localStorage.setItem('token', jwt) - ✅ Ahora: Cookie HttpOnly gestionada por el navegador
2. Middleware actualizado
// Lee de cookie primero, header como fallback
tokenStr, err := c.Cookie("auth_token")
if err != nil {
// Fallback a Authorization header
authHeader := c.GetHeader("Authorization")
// ... procesar header
}
3. Verificación continua de autenticación
// Verificación periódica con el backend
private async checkAuthStatus(): Promise<void> {
const response = await this.http.get(
`${this.API_URL}/check-auth`,
{ withCredentials: true }
)
// Actualiza estado según respuesta del servidor
}
4. Gestión automática de sesiones
- Las cookies se envían automáticamente
- El logout limpia la cookie en el servidor
- SameSite protege contra CSRF
¿Por qué esto es crucial para Angotest?
Contexto de la aplicación:
- Tests educativos: Resultados sensibles
- Comparativas entre usuarios: Datos personales involucrados
- Gestión por administradores: Acceso privilegiado
- Uso en instituciones: Responsabilidad legal
Riesgos mitigados:
- Robo de sesiones: XSS ya no puede extraer tokens
- Acceso no autorizado: Las cookies HttpOnly son más difíciles de explotar
- Ataques CSRF: SameSite cookies proporcionan protección
- Exposición accidental: No más tokens visibles en DevTools
Lo que aprendí en el proceso
1. La simplicidad inicial no siempre es mejor
- localStorage parece más simple, pero es menos seguro
- Cookies HttpOnly requieren más configuración inicial, pero menos código de mantenimiento
2. La compatibilidad es importante
- Mantener soporte para Authorization header como fallback
- No romper clientes existentes durante la migración
3. La seguridad debe evolucionar con la aplicación
- Lo aceptable para un prototipo no lo es para producción
- Cada nueva característica requiere reevaluar la seguridad
4. Documentar los cambios es crucial
- Para el equipo
- Para futuros mantenimientos
- Para casos de auditoría
Recomendaciones para otros desarrolladores
Si estás empezando:
- Usa cookies HttpOnly desde el principio
- Configura SameSite adecuadamente
- Considera HTTPS desde el desarrollo
Si migras una app existente:
- Implementa gradualmente: Soporta ambos métodos temporalmente
- Comunica los cambios: A usuarios y administradores
- Monitorea: Busca errores o comportamientos inesperados
- Documenta: Explica por qué y cómo se hizo el cambio
Para aplicaciones educativas como Angotest:
- Prioriza seguridad sobre conveniencia
- Considera regulaciones: GDPR, protección de datos estudiantiles
- Implementa logging: Para auditoría y detección de anomalías
- Educa a los usuarios: Sobre buenas prácticas de seguridad
Conclusión
Migrar de localStorage a cookies HttpOnly en Angotest no fue solo un cambio técnico. Fue un compromiso con la seguridad de los datos de estudiantes, profesores y administradores.
La lección más importante: La seguridad no es un feature que se añade al final. Es una mentalidad que debe guiar cada decisión técnica, especialmente cuando tu aplicación crece de un prototipo privado a una plataforma pública.
En el mundo de las aplicaciones educativas, donde manejamos datos sensibles y confiamos en la integridad de los resultados, invertir en seguridad no es opcional. Es nuestra responsabilidad como desarrolladores.