# 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 { 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 | - | - | [ ] |