Implementación de Rotación de Refresh Tokens y Cookies HttpOnly para JWT

Almacenar tokens de autenticación en localStorage expone tu aplicación a ataques de Cross-Site Scripting (XSS), permitiendo que cualquier script malicioso robe la sesión del usuario. La persistencia de datos en el cliente requiere un enfoque defensivo que combine el aislamiento del token y mecanismos de detección de anomalías.

Implementarás un sistema de autenticación robusto que utiliza cookies seguras para el transporte y una estrategia de rotación que invalida automáticamente sesiones comprometidas.

En resumen — La seguridad JWT óptima se logra moviendo el Refresh Token a una cookie HttpOnly con el atributo SameSite y aplicando Rotación de Refresh Tokens (RTR) para detectar y bloquear intentos de reutilización de tokens robados.

1. Qué es la Rotación de Refresh Tokens (RTR)

💡 Analogía: Imagina un pase VIP de un solo uso. Cada vez que lo entregas en la puerta, el guardia te da uno nuevo y quema el anterior. Si alguien te roba un pase viejo e intenta usarlo, el guardia notará que ese número ya fue usado y prohibirá la entrada a cualquier persona que lleve pases relacionados con ese código.

La rotación de Refresh Tokens es una técnica donde el servidor emite un nuevo Refresh Token cada vez que se solicita un nuevo Access Token. El estándar actual para aplicaciones que no pueden mantener un secreto de cliente de forma segura es seguir las recomendaciones de OAuth 2.1, que prioriza el uso de RTR para mitigar el secuestro de sesiones.

El mecanismo se basa en el encadenamiento de tokens. Cada vez que el cliente intercambia un Refresh Token (RT1) por un Access Token (AT1), el servidor también devuelve un nuevo Refresh Token (RT2). Si RT1 se utiliza de nuevo, el sistema asume que ha habido una brecha de seguridad e invalida inmediatamente RT2 y toda la familia de tokens asociada a esa sesión.

2. Escenarios de riesgo en SPAs

Cuando utilizas una Single Page Application (SPA), el mayor vector de ataque es el robo de tokens mediante XSS. Si un atacante inyecta un script a través de una dependencia de terceros o un input no saneado, puede ejecutar localStorage.getItem('token') y enviarlo a su servidor. Una vez que el atacante tiene un Refresh Token persistente, puede mantener el acceso de forma indefinida.

Otro escenario crítico ocurre cuando el usuario accede desde una red pública. Aunque HTTPS protege el tránsito, un ataque de Cross-Site Request Forgery (CSRF) podría intentar realizar acciones en nombre del usuario si las cookies no están configuradas correctamente con el atributo SameSite. La combinación de RTR y cookies seguras aborda ambos frentes de ataque simultáneamente.

3. Guía de implementación técnica

El proceso requiere cambios coordinados tanto en el manejo de cabeceras HTTP como en la lógica de persistencia de la base de datos.

Paso 1. Configuración de Cookies Seguras

El servidor debe emitir el Refresh Token en una cookie que sea inaccesible para JavaScript. Esto neutraliza el robo por XSS directo.

res.cookie('refreshToken', newRefreshToken, {
  httpOnly: true,
  secure: true, // Solo sobre HTTPS
  sameSite: 'Strict', // Protege contra CSRF
  path: '/api/auth/refresh', // Limita el alcance de la cookie
  maxAge: 7 * 24 * 60 * 60 * 1000 // 7 días
});

Paso 2. Lógica de Rotación y Detección de Reúso

En el backend, debes almacenar los Refresh Tokens activos y verificar si un token entrante ya ha sido utilizado anteriormente.

async function refreshSession(oldToken) {
  const tokenRecord = await db.tokens.find(oldToken);

  if (!tokenRecord) {
    // ¡Alerta! Token inexistente o ya rotado.
    // Posible ataque: invalidar todos los tokens del usuario.
    await db.tokens.revokeAllForUser(tokenRecord.userId);
    throw new Error('Detected reuse of refresh token');
  }

  const newAccessToken = generateAT(tokenRecord.userId);
  const newRefreshToken = generateRT(tokenRecord.userId);

  // Reemplazar el antiguo por el nuevo (Rotación)
  await db.tokens.replace(oldToken, newRefreshToken);

  return { newAccessToken, newRefreshToken };
}

Paso 3. Validación de Flujo

Verifica que al solicitar el refresh, el navegador incluya automáticamente la cookie y que la respuesta actualice la cookie con el nuevo valor. Puedes probar esto con curl o un cliente de API observando las cabeceras Set-Cookie.

curl -i -X POST https://api.tuapp.com/v1/auth/refresh \
     -H "Content-Type: application/json" \
     -b "refreshToken=RT1_VALOR"

4. Cookies HttpOnly vs. LocalStorage

La elección entre almacenamiento web y cookies determina la superficie de ataque de tu aplicación.

CriterioLocalStorageCookies HttpOnly
Protección XSSVulnerableInmune (JS no accede)
Protección CSRFInmune (manual)Vulnerable (requiere SameSite)
SimplicidadAltaMedia
EscalabilidadAlta (Stateless)Alta (con SameSite=Strict)

Si tu aplicación maneja datos sensibles o financieros, el uso de cookies HttpOnly con SameSite=Strict es obligatorio para cumplir con los estándares de seguridad modernos.

5. Errores críticos y Race Conditions

⚠️ Error frecuente: No manejar solicitudes concurrentes de refresco de token provoca que los usuarios legítimos sean expulsados del sistema.

En aplicaciones complejas, es común que múltiples componentes disparen solicitudes de refresh casi simultáneamente al expirar el Access Token. Si el primer proceso invalida el RT original para dar uno nuevo, el segundo proceso fallará y activará el mecanismo de detección de fraude, cerrando la sesión del usuario.

Solución por mensaje de error

// Problema: "Detected reuse of refresh token" por concurrencia
// Solución: Implementar un "grace period" (periodo de gracia) de 10-30s
// donde el token antiguo sigue siendo válido solo para intercambio.

if (tokenRecord.isUsed && tokenRecord.usedAt < Date.now() - 30000) {
    await revokeAllSessions(userId);
    throw new Error('Token reused beyond grace period');
}

6. Consejos para producción

Mantén el tiempo de vida (TTL) de los Access Tokens corto, idealmente entre 5 y 15 minutos. Esto reduce la ventana de oportunidad si un token es interceptado a pesar de todas las protecciones.

Implementa "Sliding Sessions" donde la expiración del Refresh Token se extiende con el uso activo, pero establece un límite absoluto (por ejemplo, 30 días) para obligar a un re-login completo por seguridad.

📌 Puntos clave

  • Usa cookies HttpOnly para evitar que XSS robe tus Refresh Tokens.
  • La rotación de tokens detecta robos mediante el seguimiento del historial de uso.
  • Configura SameSite=Lax o Strict para mitigar ataques CSRF de forma nativa en el navegador.

Preguntas frecuentes

Q. ¿Es segura la rotación de tokens sin cookies?

A. No, porque XSS aún puede robar el token activo antes de que rote.

Q. ¿Cómo afecta RTR a la experiencia de usuario móvil?

A. Es transparente, solo requiere manejar correctamente el almacenamiento de cookies.

Q. ¿Qué pasa si el servidor es stateless?

A. Requiere una base de datos rápida (Redis) para rastrear tokens usados.

Post a Comment