346 lines
15 KiB
Markdown
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 | - | - | [ ] |
|