Defensa Perimetral, Sesión Única & 2FA
La capa de identidad de Mañuti implementa un esquema de seguridad bancaria orientado a mitigar el secuestro de sesiones, ataques de fuerza bruta distribuidos y reutilización indebida de credenciales concurrentes.
1. Autenticación Dual & Cierre de Sesión Criptográfico
El punto de acceso intercepta identificadores heterogéneos sin revelar la existencia previa del registro:
flowchart TD
INPUT["[Input del Usuario]"]
VALIDATE{"¿filter_var(...,<br>FILTER_VALIDATE_EMAIL)?"}
CORREO["[Columna: correo]"]
USUARIO["[Columna: usuario]"]
HASH["[Hash::check(Bcrypt)]"]
INPUT --> VALIDATE
VALIDATE -->|"Sí"| CORREO
VALIDATE -->|"No"| USUARIO
CORREO --> HASH
USUARIO --> HASH
Protocolo de Salida Segura (Anti-CSRF)
Queda estrictamente prohibido el logout mediante verbos GET. El cierre de sesión requiere POST protegido con token CSRF validado:
public function logout(Request $request): RedirectResponse
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/login');
}
2. Política de Sesión Única (Single Session Policy) & Kill Switch por WebSockets
Mañuti impone concurrencia cero por cuenta de usuario. La coexistencia de dos terminales activas bajo la misma identidad está prohibida a nivel de protocolo.
Arquitectura de Espejo Relacional (login)
Mientras que el payload de la sesión reside en memoria volátil dentro de Redis (SESSION_DRIVER=redis), la tabla relacional login mantiene un puntero sincronizado al identificador de la sesión activa:
CREATE TABLE `login` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`usuario_id` BIGINT UNSIGNED NOT NULL,
`session_id` VARCHAR(255) NOT NULL UNIQUE,
`ip_address` VARCHAR(45) NULL,
`user_agent` TEXT NULL,
`last_activity` TIMESTAMP NOT NULL,
`open` TINYINT(1) DEFAULT 1,
FOREIGN KEY (`usuario_id`) REFERENCES `usuarios`(`id`) ON DELETE CASCADE
);
Ciclo de Expulsión Remota Reactiva
Cuando se detecta un nuevo inicio de sesión o un administrador ejecuta una revocación en /sesiones-activas:
- El servidor actualiza
login.open = 0en MariaDB. - El backend purga el ID en Redis:
Redis::del('laravel-cache-:' . $session_id). - Se despacha un evento en tiempo real a través de Laravel Reverb:
UserSessionTerminated::dispatch($usuario->id); - El navegador de la terminal desplazada escucha en el canal privado
private-user.{id}y fuerza la redirección inmediata hacia/loginincluso si el usuario se encuentra navegando en páginas404mediante el blindaje de la Ruta Fallback.
// resources/js/layout/global-listeners.js
window.Echo.private('user.' + userId)
.listen('.session.terminated', (e) => {
window.location.href = '/administracion';
});
3. Rate Limiting Dinámico & Bloqueo Progresivo
Para neutralizar ataques de diccionario o fuerza bruta:
- Clave de Throttle Compuesta:
Str::lower($request->input('login')) . '|' . $request->ip(). - Umbral de Disparo: Al alcanzar 5 fallos consecutivos (parametrizables en
configuracion_sistema), la cuenta transiciona visualmente en MariaDB al catálogoUSR_BLOQUEO_TEMP. - Registro Forense en MongoDB: El evento dispara de forma asíncrona el log
sistema_bloqueo_usuariocon código HTTP429 Too Many Requests. - Lazy Unlock (Desbloqueo Perezoso): Transcurridos los 180 segundos de enfriamiento (evaluados en Redis), el sistema restaura el estatus a
USR_ACTIVOde forma transparente únicamente si el siguiente intento ingresa la contraseña válida.
4. Segundo Factor de Autenticación (2FA TOTP RFC 6238)
El módulo 2FA se integra de forma desacoplada mediante algoritmos TOTP compatibles con Google Authenticator y Microsoft Authenticator.
Almacenamiento Seguro de Secretos
Los secretos TOTP y los códigos de respaldo de emergencia se resguardan mediante cifrado simétrico AES-256-CBC:
// app/Models/Usuario.php
public function setTwoFactorSecretAttribute($value): void
{
$this->attributes['two_factor_secret'] = $value ? Crypt::encryptString($value) : null;
}
public function getTwoFactorSecretAttribute($value): ?string
{
return $value ? Crypt::decryptString($value) : null;
}
Códigos de Recuperación Dinámicos
- La cantidad de códigos generados (
Str::random(10) . '-' . Str::random(10)) se parametriza globalmente mediante la clavesecurity_2fa_codes_count. - El consumo de un código de recuperación es de uso único (burn on use), purgado de la colección encriptada al procesar el acceso.
- En caso de pérdida física del dispositivo móvil, el Super Admin dispone del bypass auditado
users.reset_2fa, revocando la configuración para exigir un enrolamiento limpio en el próximo ciclo de login.
5. Bloqueo de Pantalla por Inactividad (Idle Lock Screen)
El frontend monitorea continuamente la inactividad del operador a través de eventos de entrada (mousemove, keydown, scroll, visibilitychange).
flowchart TD
ACT["[Actividad Normal]"]
TIMER["Timer: 15 min (security_idle_timeout)"]
MODAL["[Modal de Advertencia: 60 segs]"]
RESET["[Reset Timer]"]
LOCK["[POST /api/system/lock]"]
SCREEN["[Pantalla /bloqueo]"]
ACT --> TIMER
TIMER --> MODAL
MODAL -->|"Clic 'Mantener Activa'"| RESET
MODAL -->|"Timeout cumplido"| LOCK
LOCK --> SCREEN
- Al transcurrir la ventana de advertencia, el frontend envía una petición atómica que marca
session(['is_locked' => true])y redirige a la vista aislada/bloqueo. - El middleware
CheckSessionLockintercepta el tráfico administrativo, exigiendo la revalidación de la contraseña del usuario en sesión (Hash::check) para restaurar la navegación exactamente en la URL previa.
Enlaces Relacionados
- RBAC Aditivo & Capacidades de Negocio
- Gobierno y Ciclo de Vida de Contraseñas
- Hardening HTTP, CSP & Control de Caché