erp-core/docs/01-fase-foundation/MGN-001-auth/historias-usuario/US-MGN001-003.md

9.1 KiB

US-MGN001-003: Renovacion Automatica de Tokens

Identificacion

Campo Valor
ID US-MGN001-003
Modulo MGN-001 Auth
Sprint Sprint 1
Prioridad P0 - Critica
Story Points 8
Estado Ready
Autor System
Fecha 2025-12-05

Historia de Usuario

Como usuario autenticado del sistema ERP Quiero que mi sesion se renueve automaticamente mientras estoy usando el sistema Para no tener que re-autenticarme constantemente y tener una experiencia de usuario fluida


Descripcion

El sistema debe renovar automaticamente los tokens de acceso antes de que expiren, utilizando el refresh token. Esto permite mantener sesiones de larga duracion (hasta 7 dias) mientras los access tokens mantienen una vida corta (15 minutos) por seguridad.

Contexto

  • Access tokens expiran en 15 minutos por seguridad
  • Los usuarios no deben ser interrumpidos mientras trabajan
  • El proceso debe ser transparente para el usuario

Criterios de Aceptacion

Escenario 1: Refresh automatico exitoso

Given un usuario autenticado trabajando en el sistema
  And su access token expira en menos de 1 minuto
  And su refresh token es valido
When el frontend detecta que el token esta por expirar
Then el frontend envia automaticamente POST /api/v1/auth/refresh
  And recibe nuevos access y refresh tokens
  And actualiza los tokens en memoria
  And la cookie httpOnly es actualizada
  And el usuario NO es interrumpido

Escenario 2: Refresh con token valido (manual)

Given un usuario con refresh token valido
When hace POST /api/v1/auth/refresh
Then el sistema valida el refresh token
  And genera un nuevo par de tokens
  And el refresh token anterior es marcado como usado
  And el nuevo refresh token pertenece a la misma familia
  And responde con status 200

Escenario 3: Refresh token expirado

Given un usuario con refresh token expirado (>7 dias)
When intenta renovar tokens
Then el sistema responde con status 401
  And el mensaje es "Refresh token expirado"
  And el usuario es redirigido al login

Escenario 4: Deteccion de token replay

Given un refresh token que ya fue usado para renovar
  And el sistema genero un nuevo token despues de usarlo
When alguien intenta usar el refresh token viejo
Then el sistema detecta el reuso
  And invalida TODA la familia de tokens
  And responde con status 401
  And el mensaje es "Sesion comprometida. Por favor inicia sesion."
  And todas las sesiones del usuario son cerradas

Escenario 5: Refresh sin token

Given un request sin refresh token
When intenta renovar tokens
Then el sistema responde con status 400
  And el mensaje es "Refresh token requerido"

Escenario 6: Multiples requests simultaneos

Given un usuario con token por expirar
  And el frontend hace 3 requests simultaneos de refresh
When los requests llegan al servidor
Then solo el primero obtiene nuevos tokens
  And los siguientes reciben los nuevos tokens (idempotente)
  OR los siguientes reciben error y deben reintentar con el nuevo token

Mockup / Wireframe

El proceso es INVISIBLE para el usuario. No hay UI directa.

┌──────────────────────────────────────────────────────────────────┐
│ Indicador de sesion (opcional en header)                         │
├──────────────────────────────────────────────────────────────────┤
│                                                                   │
│  [🔒 Sesion activa]  ← Verde: sesion renovada                    │
│                                                                   │
│  [⚠️ Renovando...]   ← Amarillo: refresh en progreso             │
│                                                                   │
│  [❌ Sesion expirada] ← Rojo: necesita login                     │
│                                                                   │
└──────────────────────────────────────────────────────────────────┘

Flujo de sesion expirada:
┌──────────────────────────────────────────────────────────────────┐
│                     SESION EXPIRADA                              │
├──────────────────────────────────────────────────────────────────┤
│                                                                   │
│   Tu sesion ha expirado. Por favor inicia sesion nuevamente.    │
│                                                                   │
│   [  Ir a Login  ]                                               │
│                                                                   │
└──────────────────────────────────────────────────────────────────┘

Notas Tecnicas

API

// Request
POST /api/v1/auth/refresh
Cookie: refresh_token=eyJhbGciOiJSUzI1NiIs...

// O en body (fallback)
{
  "refreshToken": "eyJhbGciOiJSUzI1NiIs..."
}

// Response 200
{
  "accessToken": "eyJhbGciOiJSUzI1NiIs...",
  "refreshToken": "eyJhbGciOiJSUzI1NiIs...",
  "tokenType": "Bearer",
  "expiresIn": 900
}
// Set-Cookie: refresh_token=nuevo...; HttpOnly; Secure; SameSite=Strict

Logica de Refresh en Frontend

// Token refresh interceptor (pseudo-code)
class TokenRefreshService {
  private refreshing = false;
  private refreshPromise: Promise<void> | null = null;

  async checkAndRefresh(): Promise<void> {
    const accessToken = this.getAccessToken();
    const expiresAt = this.decodeExpiration(accessToken);
    const now = Date.now();
    const oneMinute = 60 * 1000;

    // Renovar si expira en menos de 1 minuto
    if (expiresAt - now < oneMinute) {
      await this.refresh();
    }
  }

  async refresh(): Promise<void> {
    // Evitar multiples refreshes simultaneos
    if (this.refreshing) {
      return this.refreshPromise;
    }

    this.refreshing = true;
    this.refreshPromise = this.doRefresh();

    try {
      await this.refreshPromise;
    } finally {
      this.refreshing = false;
      this.refreshPromise = null;
    }
  }

  private async doRefresh(): Promise<void> {
    try {
      const response = await api.post('/auth/refresh');
      this.setTokens(response.data);
    } catch (error) {
      // Refresh failed, redirect to login
      this.clearTokens();
      router.push('/login');
      throw error;
    }
  }
}

Rotacion de Tokens (Token Family)

Login exitoso:
  └── RT1 (family: ABC) ← Activo

Refresh 1:
  ├── RT1 (family: ABC) → isUsed: true, replacedBy: RT2
  └── RT2 (family: ABC) ← Activo

Refresh 2:
  ├── RT1 (family: ABC) → isUsed: true, replacedBy: RT2
  ├── RT2 (family: ABC) → isUsed: true, replacedBy: RT3
  └── RT3 (family: ABC) ← Activo

Token Replay (RT1 reutilizado):
  ├── RT1 (family: ABC) → ALERTA! ya esta usado
  ├── RT2 (family: ABC) → REVOCADO por seguridad
  └── RT3 (family: ABC) → REVOCADO por seguridad

Definicion de Done

  • Endpoint POST /api/v1/auth/refresh implementado
  • Rotacion de tokens funcionando
  • Deteccion de token replay
  • Revocacion de familia en caso de reuso
  • Cookie actualizada en cada refresh
  • Frontend: interceptor de refresh automatico
  • Frontend: manejo de sesion expirada
  • Tests unitarios (>80% coverage)
  • Tests e2e pasando
  • Code review aprobado

Dependencias

Requiere

Item Descripcion
US-MGN001-001 Login (obtener tokens iniciales)
Tabla refresh_tokens Con campos family_id, is_used, replaced_by
Redis Para rate limiting

Bloquea

Item Descripcion
Todas las features Dependen de sesion activa

Estimacion

Tarea Horas
Backend: refreshTokens() 4h
Backend: Token rotation logic 3h
Backend: Token replay detection 2h
Backend: Tests 3h
Frontend: Refresh interceptor 3h
Frontend: Token storage 2h
Frontend: Tests 2h
Total 19h

Referencias


Historial

Version Fecha Autor Cambios
1.0 2025-12-05 System Creacion inicial