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:
- Se bloquea el acceso al panel administrativo.
- Se desvía al usuario forzosamente a
/password/expired. - 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
CheckForcedPasswordChangedetecta la bandera en el modeloUsuario. - 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.