# 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 | - | - | [ ] |
