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

346 lines
15 KiB
Markdown

# RF-AUTH-005: Recuperacion de Password
## Identificacion
| Campo | Valor |
|-------|-------|
| **ID** | RF-AUTH-005 |
| **Modulo** | MGN-001 |
| **Nombre Modulo** | Auth - Autenticacion |
| **Prioridad** | P1 |
| **Complejidad** | Media |
| **Estado** | Aprobado |
| **Autor** | System |
| **Fecha** | 2025-12-05 |
---
## Descripcion
El sistema debe permitir a los usuarios recuperar el acceso a su cuenta cuando olvidan su contraseña. El proceso incluye solicitar un enlace de recuperacion por email, validar el token de recuperacion, y establecer una nueva contraseña de forma segura.
### Contexto de Negocio
La recuperacion de password es un proceso critico que debe balancear:
- Usabilidad: El usuario debe poder recuperar acceso facilmente
- Seguridad: El proceso no debe permitir acceso no autorizado
- Cumplimiento: Debe registrarse para auditoria y prevencion de abuso
---
## Criterios de Aceptacion
- [x] **CA-001:** El sistema debe generar un token unico de recuperacion con expiracion de 1 hora
- [x] **CA-002:** El sistema debe enviar email con enlace de recuperacion
- [x] **CA-003:** El sistema debe validar el token antes de permitir cambio de password
- [x] **CA-004:** El sistema debe invalidar el token despues de un uso exitoso
- [x] **CA-005:** El sistema debe invalidar el token despues de 3 intentos fallidos
- [x] **CA-006:** El sistema debe aplicar las mismas reglas de complejidad al nuevo password
- [x] **CA-007:** El sistema debe forzar logout de todas las sesiones al cambiar password
- [x] **CA-008:** El sistema debe notificar al usuario via email que su password fue cambiado
- [x] **CA-009:** El sistema NO debe revelar si el email existe o no (seguridad)
### Ejemplos de Verificacion
```gherkin
Scenario: Solicitud de recuperacion exitosa
Given un usuario registrado con email "user@example.com"
When el usuario solicita recuperacion de password
Then el sistema genera un token de recuperacion
And envia email con enlace de recuperacion
And responde con mensaje generico "Si el email existe, recibiras instrucciones"
And el token expira en 1 hora
Scenario: Cambio de password exitoso
Given un usuario con token de recuperacion valido
When el usuario envia nuevo password cumpliendo requisitos
Then el sistema actualiza el password hasheado
And invalida el token de recuperacion
And cierra todas las sesiones activas del usuario
And envia email confirmando el cambio
And responde con status 200
Scenario: Token de recuperacion expirado
Given un token de recuperacion emitido hace mas de 1 hora
When el usuario intenta usarlo para cambiar password
Then el sistema responde con status 400
And el mensaje es "Token de recuperacion expirado"
Scenario: Email no registrado (seguridad)
Given un email que NO existe en el sistema
When alguien solicita recuperacion para ese email
Then el sistema responde con el mismo mensaje generico
And NO envia ningun email
And NO revela que el email no existe
```
---
## Reglas de Negocio
| ID | Regla | Validacion |
|----|-------|------------|
| RN-001 | El token de recuperacion expira en 1 hora | Campo expires_at |
| RN-002 | Solo un token activo por usuario | Invalida tokens anteriores |
| RN-003 | Maximo 3 solicitudes por hora por email | Rate limiting |
| RN-004 | El nuevo password no puede ser igual al anterior | Comparar hashes |
| RN-005 | El nuevo password debe cumplir politica de complejidad | Min 8 chars, mayus, minus, numero |
| RN-006 | Cambio de password fuerza logout-all | Seguridad |
| RN-007 | Respuesta generica para solicitud (no revelar existencia) | Mensaje fijo |
| RN-008 | Token de uso unico | Invalida inmediatamente despues de uso |
### Politica de Complejidad de Password
```
- Minimo 8 caracteres
- Al menos 1 letra mayuscula
- Al menos 1 letra minuscula
- Al menos 1 numero
- Al menos 1 caracter especial (!@#$%^&*)
- No puede contener el email del usuario
- No puede ser igual a los ultimos 5 passwords
```
---
## Impacto en Capas
### Database
| Elemento | Accion | Descripcion |
|----------|--------|-------------|
| Tabla | crear | `password_reset_tokens` |
| Columna | - | `id` UUID PK |
| Columna | - | `user_id` UUID FK → users |
| Columna | - | `token_hash` VARCHAR(255) |
| Columna | - | `expires_at` TIMESTAMPTZ |
| Columna | - | `used_at` TIMESTAMPTZ NULL |
| Columna | - | `attempts` INTEGER DEFAULT 0 |
| Columna | - | `created_at` TIMESTAMPTZ |
| Tabla | crear | `password_history` - historial de passwords |
| Indice | crear | `idx_password_reset_tokens_user` |
| Indice | crear | `idx_password_reset_tokens_expires` |
### Backend
| Elemento | Accion | Descripcion |
|----------|--------|-------------|
| Controller | crear | `AuthController.requestPasswordReset()` |
| Controller | crear | `AuthController.resetPassword()` |
| Method | crear | `PasswordService.generateResetToken()` |
| Method | crear | `PasswordService.validateResetToken()` |
| Method | crear | `PasswordService.resetPassword()` |
| Method | crear | `PasswordService.validatePasswordPolicy()` |
| DTO | crear | `RequestPasswordResetDto` |
| DTO | crear | `ResetPasswordDto` |
| Service | usar | `EmailService.sendPasswordResetEmail()` |
| Endpoint | crear | `POST /api/v1/auth/password/request-reset` |
| Endpoint | crear | `POST /api/v1/auth/password/reset` |
| Endpoint | crear | `GET /api/v1/auth/password/validate-token/:token` |
### Frontend
| Elemento | Accion | Descripcion |
|----------|--------|-------------|
| Pagina | crear | `ForgotPasswordPage` |
| Pagina | crear | `ResetPasswordPage` |
| Componente | crear | `ForgotPasswordForm` |
| Componente | crear | `ResetPasswordForm` |
| Componente | crear | `PasswordStrengthIndicator` |
| Service | crear | `passwordService.requestReset()` |
| Service | crear | `passwordService.resetPassword()` |
---
## Dependencias
### Depende de (Bloqueantes)
| ID | Requerimiento | Estado |
|----|---------------|--------|
| RF-AUTH-001 | Login | Estructura de usuarios |
| RF-AUTH-004 | Logout | Logout-all despues de cambio |
### Dependencias Externas
| Servicio | Descripcion |
|----------|-------------|
| Email Service | Envio de emails transaccionales |
| Template Engine | Templates de email |
---
## Especificaciones Tecnicas
### Flujo de Recuperacion
```
┌─────────────────────────────────────────────────────────────────┐
│ FASE 1: SOLICITUD DE RECUPERACION │
├─────────────────────────────────────────────────────────────────┤
│ 1. Usuario ingresa email en formulario │
│ 2. Frontend POST /api/v1/auth/password/request-reset │
│ 3. Backend busca usuario por email │
│ - Si existe: genera token, envia email │
│ - Si no existe: no hace nada (seguridad) │
│ 4. Backend responde con mensaje generico │
│ "Si el email esta registrado, recibiras instrucciones" │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ FASE 2: EMAIL DE RECUPERACION │
├─────────────────────────────────────────────────────────────────┤
│ 1. Usuario recibe email con enlace │
│ https://app.erp.com/reset-password?token={token} │
│ 2. El enlace incluye token de uso unico (NO el hash) │
│ 3. El token tiene validez de 1 hora │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ FASE 3: VALIDACION DE TOKEN │
├─────────────────────────────────────────────────────────────────┤
│ 1. Usuario hace clic en enlace │
│ 2. Frontend GET /api/v1/auth/password/validate-token/:token │
│ 3. Backend valida: │
│ - Token existe │
│ - Token no expirado │
│ - Token no usado │
│ - Intentos < 3 │
│ 4. Si valido: muestra formulario de nuevo password │
│ Si invalido: muestra error apropiado │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ FASE 4: CAMBIO DE PASSWORD │
├─────────────────────────────────────────────────────────────────┤
│ 1. Usuario ingresa nuevo password (2 veces) │
│ 2. Frontend POST /api/v1/auth/password/reset │
│ Body: { token, newPassword } │
│ 3. Backend: │
│ a. Valida token nuevamente │
│ b. Valida politica de password │
│ c. Verifica no sea igual a anteriores │
│ d. Hashea nuevo password │
│ e. Actualiza users.password_hash │
│ f. Marca token como usado │
│ g. Guarda en password_history │
│ h. Ejecuta logout-all │
│ i. Envia email de confirmacion │
│ 4. Responde 200 OK │
└─────────────────────────────────────────────────────────────────┘
```
### Generacion de Token Seguro
```typescript
async generateResetToken(email: string): Promise<void> {
const user = await this.userRepo.findByEmail(email);
// IMPORTANTE: No revelar si el usuario existe
if (!user) {
// Log para auditoria pero no revelar al cliente
this.logger.warn(`Password reset requested for non-existent email: ${email}`);
return; // Respuesta identica a caso exitoso
}
// Invalida tokens anteriores
await this.passwordResetRepo.invalidateUserTokens(user.id);
// Genera token seguro (32 bytes = 256 bits)
const token = crypto.randomBytes(32).toString('hex');
const tokenHash = await bcrypt.hash(token, 10);
// Guarda en BD
await this.passwordResetRepo.create({
userId: user.id,
tokenHash,
expiresAt: addHours(new Date(), 1),
attempts: 0,
});
// Envia email (async, no bloquea respuesta)
await this.emailService.sendPasswordResetEmail(user.email, token);
}
```
### Template de Email
```html
<h2>Recuperacion de Contraseña</h2>
<p>Hola {{userName}},</p>
<p>Recibimos una solicitud para restablecer tu contraseña.</p>
<p>Haz clic en el siguiente enlace para crear una nueva contraseña:</p>
<a href="{{resetUrl}}">Restablecer Contraseña</a>
<p>Este enlace expira en 1 hora.</p>
<p>Si no solicitaste este cambio, ignora este email.</p>
<p><strong>Por seguridad, nunca compartas este enlace.</strong></p>
```
---
## Datos de Prueba
| Escenario | Entrada | Resultado |
|-----------|---------|-----------|
| Solicitud email existente | email: "test@erp.com" | 200, email enviado |
| Solicitud email no existe | email: "noexiste@erp.com" | 200, mensaje generico (no revela) |
| Token valido | Token < 1 hora | 200, permite cambio |
| Token expirado | Token > 1 hora | 400, "Token expirado" |
| Token usado | Token ya utilizado | 400, "Token ya utilizado" |
| Password debil | "123456" | 400, "Password no cumple requisitos" |
| Password igual anterior | Mismo que actual | 400, "No puede ser igual al anterior" |
| Demasiados intentos | 3+ intentos fallidos | 400, "Token invalidado" |
---
## Estimacion
| Capa | Story Points | Notas |
|------|--------------|-------|
| Database | 2 | Tablas password_reset_tokens, password_history |
| Backend | 5 | Services, validaciones, email |
| Frontend | 3 | Formularios, validacion, UX |
| **Total** | **10** | |
---
## Notas Adicionales
- Usar crypto.randomBytes para generacion de tokens (no UUID)
- Almacenar solo el HASH del token, no el token plano
- Implementar rate limiting estricto para prevenir enumeracion de emails
- Los enlaces de reset deben ser HTTPS obligatoriamente
- Considerar CAPTCHA para solicitudes de recuperacion
- Implementar honeypot para detectar bots
- El email de confirmacion de cambio debe incluir IP y timestamp
---
## Consideraciones de Seguridad
| Amenaza | Mitigacion |
|---------|------------|
| Enumeracion de emails | Respuesta identica para email existe/no existe |
| Fuerza bruta en token | Token de 256 bits, max 3 intentos |
| Intercepcion de email | HTTPS, token de uso unico |
| Session fixation | Logout-all despues de cambio |
| Password spraying | Rate limiting, CAPTCHA |
---
## 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 | - | - | [ ] |