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