363 lines
10 KiB
Markdown
363 lines
10 KiB
Markdown
# RF-USER-004: Cambio de Password
|
|
|
|
## Identificacion
|
|
|
|
| Campo | Valor |
|
|
|-------|-------|
|
|
| **ID** | RF-USER-004 |
|
|
| **Modulo** | MGN-002 |
|
|
| **Nombre Modulo** | Users - Gestion de Usuarios |
|
|
| **Prioridad** | P0 |
|
|
| **Complejidad** | Baja |
|
|
| **Estado** | Aprobado |
|
|
| **Autor** | System |
|
|
| **Fecha** | 2025-12-05 |
|
|
|
|
---
|
|
|
|
## Descripcion
|
|
|
|
El sistema debe permitir a los usuarios cambiar su contraseña de forma segura, requiriendo la contraseña actual para autorizar el cambio. Este proceso es diferente a la recuperacion de password (RF-AUTH-005) ya que el usuario conoce su password actual.
|
|
|
|
### Contexto de Negocio
|
|
|
|
El cambio de password es necesario para:
|
|
- Cumplir con politicas de rotacion de passwords
|
|
- Responder a sospechas de compromiso
|
|
- Buenas practicas de seguridad
|
|
- Requisitos de compliance
|
|
|
|
---
|
|
|
|
## Criterios de Aceptacion
|
|
|
|
- [x] **CA-001:** El usuario debe proporcionar su password actual para cambiar
|
|
- [x] **CA-002:** El nuevo password debe cumplir politica de complejidad
|
|
- [x] **CA-003:** El nuevo password no puede ser igual a los ultimos 5
|
|
- [x] **CA-004:** El sistema debe invalidar todas las otras sesiones (opcional)
|
|
- [x] **CA-005:** El sistema debe notificar via email del cambio
|
|
- [x] **CA-006:** El sistema debe registrar el cambio en password_history
|
|
- [x] **CA-007:** La sesion actual puede mantenerse activa
|
|
|
|
### Ejemplos de Verificacion
|
|
|
|
```gherkin
|
|
Scenario: Cambio de password exitoso
|
|
Given un usuario autenticado
|
|
When proporciona password actual correcto
|
|
And nuevo password "NuevoPass123!"
|
|
And el nuevo password cumple requisitos
|
|
Then el sistema actualiza el password
|
|
And guarda en password_history
|
|
And envia email de notificacion
|
|
And opcionalmente invalida otras sesiones
|
|
And responde con status 200
|
|
|
|
Scenario: Password actual incorrecto
|
|
Given un usuario autenticado
|
|
When proporciona password actual incorrecto
|
|
Then el sistema responde con status 400
|
|
And el mensaje es "Password actual incorrecto"
|
|
|
|
Scenario: Nuevo password no cumple requisitos
|
|
Given un usuario autenticado
|
|
When el nuevo password es "abc123"
|
|
Then el sistema responde con status 400
|
|
And lista los requisitos no cumplidos
|
|
|
|
Scenario: Password igual a anterior
|
|
Given un usuario que uso "MiPass123!" hace 2 meses
|
|
When intenta cambiar a "MiPass123!" nuevamente
|
|
Then el sistema responde con status 400
|
|
And el mensaje es "No puedes reusar passwords anteriores"
|
|
```
|
|
|
|
---
|
|
|
|
## Reglas de Negocio
|
|
|
|
| ID | Regla | Validacion |
|
|
|----|-------|------------|
|
|
| RN-001 | Password actual requerido | Verificacion bcrypt |
|
|
| RN-002 | Nuevo password min 8 caracteres | String length |
|
|
| RN-003 | Nuevo password requiere mayuscula | Regex [A-Z] |
|
|
| RN-004 | Nuevo password requiere minuscula | Regex [a-z] |
|
|
| RN-005 | Nuevo password requiere numero | Regex [0-9] |
|
|
| RN-006 | Nuevo password requiere especial | Regex [@$!%*?&] |
|
|
| RN-007 | No reusar ultimos 5 passwords | Check password_history |
|
|
| RN-008 | Nuevo != Actual | Comparacion |
|
|
|
|
### Politica de Password
|
|
|
|
```
|
|
Requisitos minimos:
|
|
├── Longitud: 8-128 caracteres
|
|
├── Al menos 1 mayuscula (A-Z)
|
|
├── Al menos 1 minuscula (a-z)
|
|
├── Al menos 1 numero (0-9)
|
|
├── 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 | usar | `users` - actualizar password_hash |
|
|
| Tabla | usar | `password_history` - registrar cambio |
|
|
| Tabla | usar | `session_history` - registrar evento |
|
|
|
|
### Backend
|
|
|
|
| Elemento | Accion | Descripcion |
|
|
|----------|--------|-------------|
|
|
| Controller | agregar | `UsersController.changePassword()` |
|
|
| Method | crear | `UsersService.changePassword()` |
|
|
| Method | usar | `PasswordService.validatePolicy()` |
|
|
| Method | usar | `PasswordService.checkHistory()` |
|
|
| DTO | crear | `ChangePasswordDto` |
|
|
| Endpoint | crear | `POST /api/v1/users/me/password` |
|
|
|
|
### Frontend
|
|
|
|
| Elemento | Accion | Descripcion |
|
|
|----------|--------|-------------|
|
|
| Componente | crear | `ChangePasswordForm` |
|
|
| Componente | usar | `PasswordStrengthIndicator` |
|
|
| Pagina | agregar | Seccion en ProfilePage |
|
|
|
|
---
|
|
|
|
## Dependencias
|
|
|
|
### Depende de (Bloqueantes)
|
|
|
|
| ID | Requerimiento | Estado |
|
|
|----|---------------|--------|
|
|
| RF-USER-001 | CRUD Usuarios | Tabla users |
|
|
| RF-AUTH-001 | Login | Autenticacion |
|
|
|
|
### Reutiliza de
|
|
|
|
| ID | Requerimiento | Elementos |
|
|
|----|---------------|-----------|
|
|
| RF-AUTH-005 | Password Recovery | password_history, validaciones |
|
|
|
|
---
|
|
|
|
## Especificaciones Tecnicas
|
|
|
|
### Endpoint POST /api/v1/users/me/password
|
|
|
|
```typescript
|
|
// Request
|
|
{
|
|
"currentPassword": "MiPasswordActual123!",
|
|
"newPassword": "MiNuevoPassword456!",
|
|
"confirmPassword": "MiNuevoPassword456!",
|
|
"logoutOtherSessions": true // opcional, default false
|
|
}
|
|
|
|
// Response 200
|
|
{
|
|
"message": "Password actualizado exitosamente",
|
|
"sessionsInvalidated": 2 // si logoutOtherSessions = true
|
|
}
|
|
|
|
// Response 400 - Password actual incorrecto
|
|
{
|
|
"statusCode": 400,
|
|
"message": "Password actual incorrecto"
|
|
}
|
|
|
|
// Response 400 - No cumple politica
|
|
{
|
|
"statusCode": 400,
|
|
"message": "El password no cumple los requisitos",
|
|
"errors": [
|
|
"Debe incluir al menos una mayuscula",
|
|
"Debe incluir al menos un caracter especial"
|
|
]
|
|
}
|
|
|
|
// Response 400 - Password reutilizado
|
|
{
|
|
"statusCode": 400,
|
|
"message": "No puedes usar un password que hayas usado anteriormente"
|
|
}
|
|
```
|
|
|
|
### Implementacion del Service
|
|
|
|
```typescript
|
|
// users.service.ts
|
|
async changePassword(
|
|
userId: string,
|
|
dto: ChangePasswordDto,
|
|
): Promise<ChangePasswordResult> {
|
|
const user = await this.usersRepository.findOne({ where: { id: userId } });
|
|
|
|
// 1. Verificar password actual
|
|
const isCurrentValid = await bcrypt.compare(dto.currentPassword, user.passwordHash);
|
|
if (!isCurrentValid) {
|
|
throw new BadRequestException('Password actual incorrecto');
|
|
}
|
|
|
|
// 2. Verificar que nuevo != actual
|
|
if (dto.currentPassword === dto.newPassword) {
|
|
throw new BadRequestException('El nuevo password debe ser diferente al actual');
|
|
}
|
|
|
|
// 3. Validar confirmacion
|
|
if (dto.newPassword !== dto.confirmPassword) {
|
|
throw new BadRequestException('Los passwords no coinciden');
|
|
}
|
|
|
|
// 4. Validar politica de password
|
|
const policyErrors = this.passwordService.validatePolicy(dto.newPassword, user.email);
|
|
if (policyErrors.length > 0) {
|
|
throw new BadRequestException({
|
|
message: 'El password no cumple los requisitos',
|
|
errors: policyErrors,
|
|
});
|
|
}
|
|
|
|
// 5. Verificar historial
|
|
const isReused = await this.passwordService.isPasswordReused(userId, dto.newPassword);
|
|
if (isReused) {
|
|
throw new BadRequestException('No puedes usar un password que hayas usado anteriormente');
|
|
}
|
|
|
|
// 6. Hashear nuevo password
|
|
const newHash = await bcrypt.hash(dto.newPassword, 12);
|
|
|
|
// 7. Guardar en historial
|
|
await this.passwordHistoryRepository.save({
|
|
userId,
|
|
tenantId: user.tenantId,
|
|
passwordHash: newHash,
|
|
});
|
|
|
|
// 8. Actualizar usuario
|
|
await this.usersRepository.update(userId, {
|
|
passwordHash: newHash,
|
|
updatedBy: userId,
|
|
});
|
|
|
|
// 9. Registrar evento
|
|
await this.sessionHistoryRepository.save({
|
|
userId,
|
|
tenantId: user.tenantId,
|
|
action: 'password_change',
|
|
});
|
|
|
|
// 10. Invalidar otras sesiones si se solicita
|
|
let sessionsInvalidated = 0;
|
|
if (dto.logoutOtherSessions) {
|
|
sessionsInvalidated = await this.tokenService.revokeAllUserTokens(
|
|
userId,
|
|
'password_change',
|
|
);
|
|
}
|
|
|
|
// 11. Enviar email de notificacion
|
|
await this.emailService.sendPasswordChangedEmail(user.email, user.firstName);
|
|
|
|
return {
|
|
message: 'Password actualizado exitosamente',
|
|
sessionsInvalidated,
|
|
};
|
|
}
|
|
```
|
|
|
|
### Validacion de Politica
|
|
|
|
```typescript
|
|
// password.service.ts
|
|
validatePolicy(password: string, email: string): string[] {
|
|
const errors: string[] = [];
|
|
|
|
if (password.length < 8) {
|
|
errors.push('Debe tener al menos 8 caracteres');
|
|
}
|
|
if (password.length > 128) {
|
|
errors.push('No puede exceder 128 caracteres');
|
|
}
|
|
if (!/[A-Z]/.test(password)) {
|
|
errors.push('Debe incluir al menos una mayuscula');
|
|
}
|
|
if (!/[a-z]/.test(password)) {
|
|
errors.push('Debe incluir al menos una minuscula');
|
|
}
|
|
if (!/[0-9]/.test(password)) {
|
|
errors.push('Debe incluir al menos un numero');
|
|
}
|
|
if (!/[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]/.test(password)) {
|
|
errors.push('Debe incluir al menos un caracter especial');
|
|
}
|
|
if (password.toLowerCase().includes(email.split('@')[0].toLowerCase())) {
|
|
errors.push('No puede contener tu nombre de usuario');
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Datos de Prueba
|
|
|
|
| Escenario | Entrada | Resultado |
|
|
|-----------|---------|-----------|
|
|
| Cambio exitoso | currentPassword correcto, newPassword valido | 200, actualizado |
|
|
| Password actual incorrecto | currentPassword erroneo | 400, "Password actual incorrecto" |
|
|
| Passwords no coinciden | newPassword != confirmPassword | 400, "No coinciden" |
|
|
| Password muy corto | newPassword: "Ab1!" | 400, "Min 8 caracteres" |
|
|
| Sin mayuscula | newPassword: "password123!" | 400, "Requiere mayuscula" |
|
|
| Password reutilizado | newPassword en historial | 400, "No reusar" |
|
|
| Con logout otras sesiones | logoutOtherSessions: true | 200, sesiones cerradas |
|
|
|
|
---
|
|
|
|
## Estimacion
|
|
|
|
| Capa | Story Points | Notas |
|
|
|------|--------------|-------|
|
|
| Database | 0 | Usa tablas existentes |
|
|
| Backend | 2 | Endpoint + validaciones |
|
|
| Frontend | 2 | Form + strength indicator |
|
|
| **Total** | **4** | |
|
|
|
|
---
|
|
|
|
## Notas Adicionales
|
|
|
|
- Considerar forzar cambio de password cada N dias (configurable)
|
|
- Implementar indicador de fuerza de password en tiempo real
|
|
- Rate limiting: max 3 intentos por hora
|
|
- Log detallado para auditoria de seguridad
|
|
- Considerar 2FA antes de cambio de password (futuro)
|
|
|
|
---
|
|
|
|
## 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 | - | - | [ ] |
|