262 lines
10 KiB
Markdown
262 lines
10 KiB
Markdown
# RF-AUTH-003: Refresh Token y Renovacion de Sesion
|
|
|
|
## Identificacion
|
|
|
|
| Campo | Valor |
|
|
|-------|-------|
|
|
| **ID** | RF-AUTH-003 |
|
|
| **Modulo** | MGN-001 |
|
|
| **Nombre Modulo** | Auth - Autenticacion |
|
|
| **Prioridad** | P0 |
|
|
| **Complejidad** | Media |
|
|
| **Estado** | Aprobado |
|
|
| **Autor** | System |
|
|
| **Fecha** | 2025-12-05 |
|
|
|
|
---
|
|
|
|
## Descripcion
|
|
|
|
El sistema debe permitir renovar el access token utilizando un refresh token valido, sin requerir que el usuario vuelva a ingresar sus credenciales. Esto permite mantener sesiones de larga duracion de forma segura, mientras los access tokens tienen vida corta.
|
|
|
|
### Contexto de Negocio
|
|
|
|
Los access tokens tienen vida corta (15 minutos) por seguridad. Sin un mecanismo de refresh, los usuarios tendrian que re-autenticarse constantemente, lo cual afecta negativamente la experiencia de usuario. El refresh token permite mantener la sesion activa hasta 7 dias sin comprometer la seguridad.
|
|
|
|
---
|
|
|
|
## Criterios de Aceptacion
|
|
|
|
- [x] **CA-001:** El sistema debe aceptar un refresh token valido y generar nuevos tokens
|
|
- [x] **CA-002:** El nuevo access token debe tener los mismos claims que el original
|
|
- [x] **CA-003:** El refresh token usado debe invalidarse (rotacion de tokens)
|
|
- [x] **CA-004:** Se debe generar un nuevo refresh token con cada renovacion
|
|
- [x] **CA-005:** El sistema debe rechazar refresh tokens expirados con error 401
|
|
- [x] **CA-006:** El sistema debe rechazar refresh tokens revocados con error 401
|
|
- [x] **CA-007:** El sistema debe detectar y prevenir reuso de refresh tokens (token replay)
|
|
- [x] **CA-008:** El frontend debe renovar automaticamente antes de que expire el access token
|
|
|
|
### Ejemplos de Verificacion
|
|
|
|
```gherkin
|
|
Scenario: Renovacion exitosa de tokens
|
|
Given un usuario con refresh token valido
|
|
When el usuario envia el refresh token a /api/v1/auth/refresh
|
|
Then el sistema invalida el refresh token anterior
|
|
And genera un nuevo par de tokens (access + refresh)
|
|
And responde con status 200 y los nuevos tokens
|
|
|
|
Scenario: Refresh token expirado
|
|
Given un usuario con refresh token expirado
|
|
When el usuario intenta renovar tokens
|
|
Then el sistema responde con status 401
|
|
And el mensaje es "Refresh token expirado"
|
|
And el usuario debe hacer login nuevamente
|
|
|
|
Scenario: Deteccion de token replay (reuso)
|
|
Given un refresh token que ya fue usado para renovar
|
|
When alguien intenta usar ese mismo refresh token
|
|
Then el sistema detecta el reuso
|
|
And invalida TODOS los tokens del usuario (seguridad)
|
|
And responde con status 401
|
|
And el mensaje es "Sesion comprometida, por favor inicie sesion"
|
|
```
|
|
|
|
---
|
|
|
|
## Reglas de Negocio
|
|
|
|
| ID | Regla | Validacion |
|
|
|----|-------|------------|
|
|
| RN-001 | Refresh token valido por 7 dias | JWT exp claim |
|
|
| RN-002 | Cada refresh genera nuevo refresh token (rotacion) | Token replacement |
|
|
| RN-003 | Refresh token usado se invalida inmediatamente | Marcar como usado en BD |
|
|
| RN-004 | Reuso de refresh token invalida toda la familia | Revocar todos tokens del usuario |
|
|
| RN-005 | Maximo 5 sesiones activas por usuario | Contador de sesiones |
|
|
| RN-006 | El refresh se hace 1 minuto antes de expiracion | Frontend timer |
|
|
|
|
### Token Family (Familia de Tokens)
|
|
|
|
Cada refresh token pertenece a una "familia" que se origina en un login. Si se detecta reuso de un token de esa familia, toda la familia se invalida.
|
|
|
|
```
|
|
Login -> RT1 -> RT2 -> RT3 (familia activa)
|
|
↳ RT2 reusado? -> Invalida RT1, RT2, RT3
|
|
```
|
|
|
|
---
|
|
|
|
## Impacto en Capas
|
|
|
|
### Database
|
|
|
|
| Elemento | Accion | Descripcion |
|
|
|----------|--------|-------------|
|
|
| Tabla | modificar | `refresh_tokens` - agregar campos |
|
|
| Columna | agregar | `family_id` UUID - familia del token |
|
|
| Columna | agregar | `is_used` BOOLEAN - si ya fue usado |
|
|
| Columna | agregar | `used_at` TIMESTAMPTZ - cuando se uso |
|
|
| Columna | agregar | `replaced_by` UUID - token que lo reemplazo |
|
|
| Indice | crear | `idx_refresh_tokens_family` |
|
|
|
|
### Backend
|
|
|
|
| Elemento | Accion | Descripcion |
|
|
|----------|--------|-------------|
|
|
| Controller | crear | `AuthController.refresh()` |
|
|
| Method | crear | `TokenService.refreshTokens()` |
|
|
| Method | crear | `TokenService.detectTokenReuse()` |
|
|
| Method | crear | `TokenService.revokeTokenFamily()` |
|
|
| DTO | crear | `RefreshTokenDto` |
|
|
| DTO | crear | `TokenResponseDto` |
|
|
| Endpoint | crear | `POST /api/v1/auth/refresh` |
|
|
|
|
### Frontend
|
|
|
|
| Elemento | Accion | Descripcion |
|
|
|----------|--------|-------------|
|
|
| Service | modificar | `tokenService.refreshTokens()` |
|
|
| Interceptor | crear | Auto-refresh interceptor |
|
|
| Timer | crear | Refresh timer (1 min antes de exp) |
|
|
| Handler | crear | Token expiration handler |
|
|
|
|
---
|
|
|
|
## Dependencias
|
|
|
|
### Depende de (Bloqueantes)
|
|
|
|
| ID | Requerimiento | Estado |
|
|
|----|---------------|--------|
|
|
| RF-AUTH-001 | Login | Genera tokens iniciales |
|
|
| RF-AUTH-002 | JWT Tokens | Estructura de tokens |
|
|
|
|
### Dependencias Relacionadas
|
|
|
|
| ID | Requerimiento | Relacion |
|
|
|----|---------------|----------|
|
|
| RF-AUTH-004 | Logout | Revoca refresh tokens |
|
|
|
|
---
|
|
|
|
## Especificaciones Tecnicas
|
|
|
|
### Flujo de Refresh
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ 1. Frontend detecta que access token expira pronto │
|
|
│ (1 minuto antes de exp) │
|
|
└───────────────────────────┬─────────────────────────────────────┘
|
|
↓
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ 2. Frontend envia refresh token a POST /api/v1/auth/refresh │
|
|
└───────────────────────────┬─────────────────────────────────────┘
|
|
↓
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ 3. Backend valida refresh token │
|
|
│ - Verifica firma │
|
|
│ - Verifica expiracion │
|
|
│ - Verifica que no este usado (is_used = false) │
|
|
│ - Verifica que no este revocado │
|
|
└───────────────────────────┬─────────────────────────────────────┘
|
|
↓
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ 4. Si es valido: │
|
|
│ - Marca token actual como usado (is_used = true) │
|
|
│ - Genera nuevo access token │
|
|
│ - Genera nuevo refresh token (misma family_id) │
|
|
│ - Actualiza replaced_by del token anterior │
|
|
│ - Retorna nuevos tokens │
|
|
└───────────────────────────┬─────────────────────────────────────┘
|
|
↓
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ 5. Frontend almacena nuevos tokens │
|
|
│ - Actualiza access token en memoria/storage │
|
|
│ - Actualiza refresh token en httpOnly cookie │
|
|
│ - Reinicia timer de refresh │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### Deteccion de Token Replay
|
|
|
|
```typescript
|
|
async refreshTokens(refreshToken: string): Promise<TokenPair> {
|
|
const decoded = this.decodeToken(refreshToken);
|
|
const storedToken = await this.refreshTokenRepo.findOne({
|
|
where: { jti: decoded.jti }
|
|
});
|
|
|
|
// Detectar reuso
|
|
if (storedToken.isUsed) {
|
|
// ALERTA: Token replay detectado
|
|
await this.revokeTokenFamily(storedToken.familyId);
|
|
throw new UnauthorizedException('Sesion comprometida');
|
|
}
|
|
|
|
// Marcar como usado
|
|
storedToken.isUsed = true;
|
|
storedToken.usedAt = new Date();
|
|
|
|
// Generar nuevos tokens
|
|
const newTokens = await this.generateTokenPair(decoded.sub, decoded.tid);
|
|
|
|
// Vincular tokens
|
|
storedToken.replacedBy = newTokens.refreshTokenId;
|
|
await this.refreshTokenRepo.save(storedToken);
|
|
|
|
return newTokens;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Datos de Prueba
|
|
|
|
| Escenario | Entrada | Resultado |
|
|
|-----------|---------|-----------|
|
|
| Refresh exitoso | Refresh token valido | 200, nuevos tokens |
|
|
| Token expirado | RT con exp < now | 401, "Token expirado" |
|
|
| Token ya usado | RT con is_used = true | 401, "Sesion comprometida" |
|
|
| Token revocado | RT en revoked_tokens | 401, "Token revocado" |
|
|
| Sin refresh token | Body vacio | 400, "Refresh token requerido" |
|
|
|
|
---
|
|
|
|
## Estimacion
|
|
|
|
| Capa | Story Points | Notas |
|
|
|------|--------------|-------|
|
|
| Database | 2 | Columnas adicionales en refresh_tokens |
|
|
| Backend | 5 | Logica de rotacion y deteccion reuso |
|
|
| Frontend | 3 | Auto-refresh interceptor y timer |
|
|
| **Total** | **10** | |
|
|
|
|
---
|
|
|
|
## Notas Adicionales
|
|
|
|
- El refresh token debe enviarse en httpOnly cookie, no en body (previene XSS)
|
|
- Considerar sliding window: extender expiracion si hay actividad
|
|
- Implementar rate limiting en endpoint de refresh (max 1 req/segundo)
|
|
- Loguear todos los refreshes para auditoria
|
|
- En caso de breach, proporcionar endpoint para revocar todas las sesiones
|
|
|
|
---
|
|
|
|
## 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 | - | - | [ ] |
|