resources/views/pages/frontend/docs/gobierno-contrasenas.md
Última modificación: 24 Oct, 2026

Gobierno y Ciclo de Vida de Contraseñas

Mañuti implementa un esquema de gobierno de contraseñas dinámico que desacopla las políticas de seguridad del código fuente, centralizando las reglas en la tabla configuracion_sistema y aplicando cumplimiento forzoso en tiempo de ejecución.


1. Parametrización Dinámica de Políticas

Las restricciones no son estáticas ni requieren despliegues de código para ser modificadas. Se gestionan mediante el patrón Key-Value:

Clave de Configuración Tipo de Dato Valor por Defecto Propósito Normativo
security_password_min_length number 8 Longitud mínima de la cadena.
security_require_uppercase boolean true Exige presencia de letras mayúsculas $[A-Z]$.
security_require_lowercase boolean true Exige presencia de letras minúsculas $[a-z]$.
security_require_numbers boolean true Exige presencia de dígitos $[0-9]$.
security_require_symbols boolean false Exige caracteres especiales $[!@#$].
security_password_lifetime number 90 Período de caducidad en días ($0 = \text{inactivo}$).
security_history_limit number 3 Profundidad de rotación. Impide reusar las últimas $N$ contraseñas.

2. Motor Centralizado de Validación (PasswordSecurity)

La clase de servicio App\Services\PasswordSecurity abstrae la generación de reglas tanto para peticiones de usuario final como para la consola de administración:

// app/Services/PasswordSecurity.php
namespace App\Services;

use App\Models\ConfiguracionSistema;
use Illuminate\Validation\Rules\Password;

class PasswordSecurity
{
    public static function getValidationRules(bool $requireConfirmation = true): array
    {
        $length = (int) ConfiguracionSistema::getValor('security_password_min_length', 8);
        $rule = Password::min($length);

        if (ConfiguracionSistema::getValor('security_require_uppercase', false)) $rule->mixedCase();
        if (ConfiguracionSistema::getValor('security_require_numbers', false))   $rule->numbers();
        if (ConfiguracionSistema::getValor('security_require_symbols', false))   $rule->symbols();
        if (ConfiguracionSistema::getValor('security_require_lowercase', false)) $rule->letters();

        $rules = ['required', $rule];

        if ($requireConfirmation) {
            $rules[] = 'confirmed';
        }

        return $rules;
    }
}

3. Prevención de Reutilización (password_history)

Para evitar la alternancia cíclica de credenciales vulnerables, cada modificación persiste el hash previo en la tabla relacional password_history.

flowchart TD
    NEW_PASS["[Nuevo Password Propuesto]"]
    CHECK{"Hash::check() contra las<br/>últimas N filas en<br/>'password_history'"}
    ERROR["[Error: Contraseña ya utilizada]"]
    SUCCESS["1. Inserta hash actual en 'password_history'<br/>2. Actualiza hash en tabla 'usuarios'<br/>3. Setea password_changed_at = now()"]

    NEW_PASS --> CHECK
    CHECK -->|"Coincide (true)"| ERROR
    CHECK -->|"No coincide (false)"| SUCCESS
public static function checkHistory(Usuario $user, string $newPassword): bool
{
    $limit = (int) ConfiguracionSistema::getValor('security_history_limit', 0);
    if ($limit <= 0) return true;

    $historial = $user->historialPasswords()->take($limit)->get();

    foreach ($historial as $record) {
        if (Hash::check($newPassword, $record->password)) {
            return false;
        }
    }
    return true;
}

4. Caducidad Forzosa & Cuarentena (CheckPasswordExpiration)

El middleware con alias 'password.expired' intercepta el tráfico administrativo evaluando la antigüedad de la credencial:

$$\text{Fecha de Expiración} = (\text{password_changed_at} \lor \text{created_at}) + \text{Días de Vigencia}$$

Si la fecha actual supera el límite calculado:

  1. Se bloquea el acceso al panel administrativo.
  2. Se desvía al usuario forzosamente a /password/expired.
  3. La cuarentena no permite navegación lateral ni consumo de APIs hasta completar la renovación bajo las políticas de complejidad vigentes.

5. Cambio Forzoso en Primer Inicio (must_change_password)

Cuando un administrador da de alta a un operador desde la consola de gestión, puede activar la bandera must_change_password:

  • El middleware CheckForcedPasswordChange detecta la bandera en el modelo Usuario.
  • Desvía el flujo del usuario inmediatamente tras la autenticación hacia /password/change-required.
  • El usuario está obligado a ingresar una credencial personal secreta para que el sistema desactive la bandera (must_change_password = 0) y libere el acceso al sistema.

6. Checklist Interactivo en Tiempo Real (UX Client-Side)

Para minimizar la fricción operativa durante la captura de credenciales complejas, la vista implementa reactividad client-side:

<ul class="space-y-2 text-sm">
    <li id="rule_length" class="flex items-center gap-2">Mínimo 8 caracteres</li>
    <li id="rule_upper" class="flex items-center gap-2">Una letra mayúscula</li>
    <li id="rule_number" class="flex items-center gap-2">Al menos un número</li>
    <li id="rule_match" class="flex items-center gap-2">Las contraseñas coinciden</li>
</ul>

JavaScript evalúa la expresión regular al evento input, alternando iconos de cruz roja por marcas de verificación verde en milisegundos, garantizando que el formulario no sea enviado hasta satisfacer todas las condiciones del sistema.


Enlaces Relacionados

Edita este documento en >> gobierno-contrasenas.md
← Volver a la Landing Page