erp-core/docs/01-fase-foundation/MGN-002-users/requerimientos/RF-USER-004.md

10 KiB

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

  • CA-001: El usuario debe proporcionar su password actual para cambiar
  • CA-002: El nuevo password debe cumplir politica de complejidad
  • CA-003: El nuevo password no puede ser igual a los ultimos 5
  • CA-004: El sistema debe invalidar todas las otras sesiones (opcional)
  • CA-005: El sistema debe notificar via email del cambio
  • CA-006: El sistema debe registrar el cambio en password_history
  • CA-007: La sesion actual puede mantenerse activa

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