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
Ejemplos de Verificacion
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
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 |
- |
- |
[ ] |