erp-core/docs/03-requerimientos/RF-auth/RF-AUTH-004.md

12 KiB

RF-AUTH-004: Logout y Revocacion de Sesion

Identificacion

Campo Valor
ID RF-AUTH-004
Modulo MGN-001
Nombre Modulo Auth - Autenticacion
Prioridad P0
Complejidad Baja
Estado Aprobado
Autor System
Fecha 2025-12-05

Descripcion

El sistema debe permitir a los usuarios cerrar su sesion de forma segura, revocando todos los tokens asociados (access token y refresh token). Esto garantiza que los tokens no puedan ser reutilizados despues del logout, incluso si no han expirado.

Contexto de Negocio

El logout seguro es esencial para:

  • Proteger cuentas en dispositivos compartidos
  • Cumplir con politicas de seguridad corporativas
  • Permitir al usuario revocar acceso si sospecha compromiso
  • Terminar sesiones en dispositivos perdidos o robados

Criterios de Aceptacion

  • CA-001: El sistema debe aceptar el refresh token para identificar la sesion a cerrar
  • CA-002: El sistema debe invalidar el refresh token en la base de datos
  • CA-003: El sistema debe agregar el access token actual a la blacklist
  • CA-004: El sistema debe eliminar la cookie httpOnly del refresh token
  • CA-005: El sistema debe responder con 200 OK en logout exitoso
  • CA-006: El sistema debe registrar el logout en el historial de sesiones
  • CA-007: El sistema debe permitir logout de todas las sesiones (logout global)
  • CA-008: El frontend debe limpiar tokens de memoria/storage

Ejemplos de Verificacion

Scenario: Logout exitoso
  Given un usuario autenticado con sesion activa
  When el usuario hace POST /api/v1/auth/logout
  Then el sistema revoca el refresh token actual
  And agrega el access token a la blacklist
  And elimina la cookie del refresh token
  And responde con status 200
  And registra el evento en session_history

Scenario: Logout de todas las sesiones
  Given un usuario con multiples sesiones activas en diferentes dispositivos
  When el usuario hace POST /api/v1/auth/logout-all
  Then el sistema revoca TODOS los refresh tokens del usuario
  And invalida toda la familia de tokens
  And responde con status 200
  And el usuario es forzado a re-autenticarse en todos los dispositivos

Scenario: Logout con token ya expirado
  Given un usuario con access token expirado pero refresh token valido
  When el usuario intenta hacer logout
  Then el sistema permite el logout usando solo el refresh token
  And responde con status 200

Reglas de Negocio

ID Regla Validacion
RN-001 El logout revoca la sesion actual unicamente Por defecto solo sesion actual
RN-002 Logout-all revoca todas las sesiones del usuario Parametro all=true
RN-003 Los tokens revocados se almacenan hasta su expiracion natural Cleanup job posterior
RN-004 El access token se blacklistea en Redis/memoria TTL = tiempo restante de exp
RN-005 El logout debe funcionar aunque el access token este expirado Usar refresh token
RN-006 El evento de logout se registra para auditoria session_history.action = 'logout'

Blacklist de Tokens

Para invalidacion inmediata de access tokens (que son stateless), se usa una blacklist:

Token activo + logout → jti agregado a blacklist
Validacion de token → verificar si jti esta en blacklist
Blacklist TTL → igual al tiempo restante del token

Impacto en Capas

Database

Elemento Accion Descripcion
Tabla usar refresh_tokens - marcar como revocado
Tabla usar session_history - registrar logout
Columna agregar refresh_tokens.revoked_at TIMESTAMPTZ
Columna agregar refresh_tokens.revoked_reason VARCHAR(50)

Backend

Elemento Accion Descripcion
Controller crear AuthController.logout()
Controller crear AuthController.logoutAll()
Method crear TokenService.revokeRefreshToken()
Method crear TokenService.revokeAllUserTokens()
Method crear TokenService.blacklistAccessToken()
Service crear BlacklistService (Redis)
Endpoint crear POST /api/v1/auth/logout
Endpoint crear POST /api/v1/auth/logout-all

Frontend

Elemento Accion Descripcion
Service modificar authService.logout()
Store modificar authStore.clearSession()
Method crear tokenService.clearAllTokens()
Interceptor modificar Redirect a login en 401 post-logout

Dependencias

Depende de (Bloqueantes)

ID Requerimiento Estado
RF-AUTH-001 Login Crea la sesion a cerrar
RF-AUTH-002 JWT Tokens Tokens a revocar
RF-AUTH-003 Refresh Token Token a invalidar

Dependencias Relacionadas

ID Requerimiento Relacion
RF-AUTH-005 Password Recovery Puede forzar logout-all

Especificaciones Tecnicas

Flujo de Logout

┌─────────────────────────────────────────────────────────────────┐
│ 1. Frontend llama POST /api/v1/auth/logout                      │
│    - Envia refresh token en cookie httpOnly                     │
│    - Envia access token en header Authorization                 │
└───────────────────────────┬─────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────────┐
│ 2. Backend extrae tokens                                         │
│    - Decodifica refresh token para obtener jti                  │
│    - Decodifica access token para obtener jti                   │
└───────────────────────────┬─────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────────┐
│ 3. Revoca refresh token en BD                                    │
│    - UPDATE refresh_tokens SET                                   │
│        revoked_at = NOW(),                                       │
│        revoked_reason = 'user_logout'                           │
│      WHERE jti = :refresh_jti                                    │
└───────────────────────────┬─────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────────┐
│ 4. Blacklistea access token en Redis                             │
│    - SET blacklist:{access_jti} = 1                             │
│    - EXPIRE blacklist:{access_jti} {remaining_ttl}              │
└───────────────────────────┬─────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────────┐
│ 5. Elimina cookie de refresh token                               │
│    - Set-Cookie: refresh_token=; Max-Age=0; HttpOnly            │
└───────────────────────────┬─────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────────┐
│ 6. Registra evento en session_history                            │
│    - INSERT INTO session_history (user_id, action, ...)         │
└───────────────────────────┬─────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────────┐
│ 7. Responde al frontend                                          │
│    - Status 200 OK                                               │
│    - Body: { message: "Sesion cerrada exitosamente" }           │
└─────────────────────────────────────────────────────────────────┘

Blacklist Service (Redis)

@Injectable()
export class BlacklistService {
  constructor(private redis: RedisService) {}

  async blacklistToken(jti: string, expiresIn: number): Promise<void> {
    const key = `blacklist:${jti}`;
    await this.redis.set(key, '1', 'EX', expiresIn);
  }

  async isBlacklisted(jti: string): Promise<boolean> {
    const key = `blacklist:${jti}`;
    const result = await this.redis.get(key);
    return result !== null;
  }
}

Logout All (Logout Global)

async logoutAll(userId: string): Promise<void> {
  // Revocar todos los refresh tokens del usuario
  await this.refreshTokenRepo.update(
    { userId, revokedAt: IsNull() },
    { revokedAt: new Date(), revokedReason: 'logout_all' }
  );

  // Blacklistear todos los access tokens activos
  // (requiere tracking de tokens activos o usar family_id)
  await this.blacklistUserTokens(userId);

  // Registrar evento
  await this.sessionHistoryService.record({
    userId,
    action: 'logout_all',
    metadata: { reason: 'user_requested' }
  });
}

Datos de Prueba

Escenario Entrada Resultado
Logout exitoso Token valido 200, sesion cerrada
Logout sin token Sin Authorization 401, "Token requerido"
Logout token expirado Access expirado, refresh valido 200, permite logout
Logout-all all=true 200, todas sesiones cerradas
Logout token ya revocado Refresh ya revocado 200, idempotente

Estimacion

Capa Story Points Notas
Database 1 Columnas adicionales
Backend 3 Controller, Services, Redis
Frontend 2 Limpieza de estado
Total 6

Notas Adicionales

  • Implementar logout como operacion idempotente (no falla si ya esta logged out)
  • Considerar endpoint para logout de sesion especifica por device_id
  • El blacklist en Redis debe tener alta disponibilidad
  • Cleanup job para eliminar tokens revocados expirados de BD
  • Notificar al usuario via email si se hace logout-all (seguridad)

Historial de Cambios

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

Aprobaciones

Rol Nombre Fecha Firma
Analista System 2025-12-05 [x]
Tech Lead - - [ ]
Product Owner - - [ ]