289 lines
12 KiB
Markdown
289 lines
12 KiB
Markdown
# 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
|
|
|
|
- [x] **CA-001:** El sistema debe aceptar el refresh token para identificar la sesion a cerrar
|
|
- [x] **CA-002:** El sistema debe invalidar el refresh token en la base de datos
|
|
- [x] **CA-003:** El sistema debe agregar el access token actual a la blacklist
|
|
- [x] **CA-004:** El sistema debe eliminar la cookie httpOnly del refresh token
|
|
- [x] **CA-005:** El sistema debe responder con 200 OK en logout exitoso
|
|
- [x] **CA-006:** El sistema debe registrar el logout en el historial de sesiones
|
|
- [x] **CA-007:** El sistema debe permitir logout de todas las sesiones (logout global)
|
|
- [x] **CA-008:** El frontend debe limpiar tokens de memoria/storage
|
|
|
|
### Ejemplos de Verificacion
|
|
|
|
```gherkin
|
|
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)
|
|
|
|
```typescript
|
|
@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)
|
|
|
|
```typescript
|
|
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 | - | - | [ ] |
|