Autenticación Frontend
Floutic implementa un sistema de autenticación seguro con múltiples capas de protección, utilizando HttpOnly cookies, CSRF tokens y session IDs únicos.
Sistema de Autenticación Segura
Características Principales
- ✅ HttpOnly cookies: Tokens almacenados de forma segura
- ✅ CSRF protection: Tokens CSRF en cookies HttpOnly
- ✅ Session ID único: Por ventana para aislar sesiones
- ✅ Refresh automático: Renovación transparente de tokens
- ✅ Logout por inactividad: Configurable por rol
- ✅ Validación de session_id: En todas las peticiones
Implementación
Servicio de Autenticación (src/lib/auth/secureAuth.ts)
Métodos Principales
class SecureAuthService {
// Login con credenciales
async login(credentials: { username: string; password: string }): Promise<void>
// Logout seguro
async logout(): Promise<void>
// Verificar sesión actual
async verifySession(): Promise<boolean>
// Refresh automático de tokens
async refreshToken(): Promise<void>
// Obtener usuario actual
getCurrentUser(): User | null
}
Session ID Único
Cada ventana genera un session_id único:
- Almacenado en
sessionStorage(aislado entre ventanas privadas) - Enviado en header
X-Session-IDen cada petición - Incluido en el payload del JWT al crear tokens
- Validado en el servidor para prevenir fuga de sesiones
Instancia Global (window.secureAuthService)
Para garantizar una única fuente de verdad y permitir la interoperabilidad entre el ciclo de vida de React (Hooks/Context) y la lógica de negocio pura (API Clients, utilidades), el servicio se expone globalmente.
// Acceso global para depuración o uso fuera de React
window.secureAuthService = secureAuthService;
Características:
- Singleton: Una única instancia maneja el estado para toda la aplicación.
- Interoperabilidad: Permite que
apiClientacceda al token actual o realice logout sin depender del árbol de componentes de React. - Limpieza:
AuthService.logout()se encarga explícitamente de limpiar esta referencia y resetear el estado global para evitar fugas de datos entre sesiones.
Hook principal para usar autenticación en componentes:
const {
user,
isAuthenticated,
isLoading,
login,
logout,
verifySession
} = useSecureAuth();
Características:
- Estado reactivo de autenticación
- Inicialización automática al montar
- Refresh automático de tokens
- Manejo de errores
Cliente API
El cliente Axios (src/lib/api/client.ts) incluye interceptores:
Request Interceptor
- Añade
X-Session-IDheader - Añade
X-CSRF-Tokenheader - Añade
Authorizationheader con access token
Response Interceptor
- Maneja errores 401 (no autorizado)
- Refresh automático de tokens
- Retry logic para requests fallidos
Flujo de Autenticación
Registro
- Usuario completa formulario de registro
- Frontend envía
POST /api/auth/registercon datos del usuario - Backend crea usuario y:
- Genera token de verificación de email
- Envía email de verificación
- Realiza login automático
- Frontend:
- Usuario queda autenticado automáticamente
- Se muestra banner de verificación de email
- Usuario puede usar la plataforma normalmente
- Usuario verifica email:
- Hace clic en link del email →
/verificar-email?token=<token> - Backend verifica email y envía email de bienvenida
- Banner desaparece automáticamente
- Hace clic en link del email →
Componentes relacionados:
RegisterForm- Formulario de registroEmailVerificationBanner- Banner recordatorioEmailVerification- Página de verificación
Login
- Usuario ingresa credenciales
- Limpieza de estado anterior: Antes del login, el frontend limpia automáticamente:
- Estado de autenticación en
secureAuthService - Estado en Zustand store (sin hacer logout al servidor)
session_idanterior de sessionStorage- Estado global
window.__AUTH_STATE__ - Esto previene mostrar datos del usuario anterior
- Estado de autenticación en
- Frontend envía
POST /api/auth/login_securecon:usernameypassword- Header
X-Session-ID(generado nuevo en cliente)
- Backend valida credenciales y crea:
refresh_tokenen cookie HttpOnlycsrf_tokenen cookie HttpOnlyaccess_tokenen respuesta JSON- JWT incluye
session_iddel header
- Frontend almacena:
access_tokenen memoria (no localStorage)session_iden sessionStorage- Estado de autenticación en Zustand store
- Sincroniza
secureAuthServicecon el nuevo usuario
Verificación de Sesión
- Al cargar la app, se llama a
verify_secure - Backend valida:
refresh_tokenen cookiessession_iddel header coincide con el del token
- Si es válido, devuelve usuario y tokens
- Frontend actualiza estado de autenticación
Refresh de Tokens
- Cuando el
access_tokenexpira (o está próximo a expirar) - Frontend llama a
POST /api/auth/refresh_secure - Backend valida:
refresh_tokenen cookiessession_iddel header
- Genera nuevos tokens y los devuelve
- Frontend actualiza
access_tokenen memoria
Logout
- Usuario hace logout
- Frontend llama a
POST /api/auth/logout_secure - Backend:
- Invalida
refresh_tokenen Redis blacklist - Elimina cookies
- Invalida
- Frontend:
- Limpia estado de autenticación
- Limpia
session_idde sessionStorage - Redirige a login
Protección de Rutas
Middleware de Astro (src/middleware.ts)
El middleware verifica autenticación antes de servir páginas:
export async function onRequest(context, next) {
const { url, cookies } = context;
// Rutas públicas
if (isPublicRoute(url.pathname)) {
return next();
}
// Verificar autenticación
const authenticated = await verifyAuth(cookies);
if (!authenticated) {
return redirect('/login');
}
return next();
}
Protección de Componentes
Componentes protegidos verifican autenticación:
const Dashboard = () => {
const { isAuthenticated, isLoading } = useSecureAuth();
if (isLoading) return <Loading />;
if (!isAuthenticated) return <Redirect to="/login" />;
return <DashboardContent />;
};
Patrón isInitialized (Race Condition Prevention)
El Problema
En páginas protegidas que cargan antes de que el servicio de autenticación termine de inicializarse, puede ocurrir un race condition:
- El componente verifica
isAuthenticatedantes de que cargue la sesión - Resultado: Redirección incorrecta a login
La Solución
El hook useSecureAuth expone isInitialized para distinguir entre:
isLoading: Cargando datos del usuarioisInitialized: El servicio de autenticación ha terminado de inicializarse
Patrón Correcto:
const { isAuthenticated, isInitialized } = useSecureAuth();
// Esperar a que el servicio se inicialice ANTES de verificar autenticación
if (!isInitialized) {
return <Loading />;
}
if (!isAuthenticated) {
return <Redirect to="/login" />;
}
return <ProtectedContent />;
Patrón Incorrecto (causa redirección prematura):
const { isAuthenticated, isLoading } = useSecureAuth();
// ❌ PROBLEMA: isLoading puede ser false antes de que isInitialized sea true
if (isLoading) return <Loading />;
if (!isAuthenticated) return <Redirect to="/login" />;
Advertencia: NO usar defineMiddleware() en Páginas de Perfil
El middleware de Astro (defineMiddleware()) se ejecuta antes de que los componentes React se hidraten. Si la autenticación se maneja a nivel de componente (con useSecureAuth), NO uses defineMiddleware() porque:
- Se ejecuta antes de que
secureAuthServicetermine su inicialización asíncrona - No tiene acceso al estado de React
- Causa redirecciones prematuras a login
Incorrecto:
// ❌ En una página de perfil
export const onRequest = defineMiddleware(async (context, next) => {
if (!isAuthenticated) return context.redirect('/login');
return next();
});
Correcto:
// ✅ Dejar la autenticación al componente React
const PerfilPage = () => {
const { isAuthenticated, isInitialized } = useSecureAuth();
if (!isInitialized) return <Loading />;
if (!isAuthenticated) return <Navigate to="/login" />;
return <PerfilContent />;
};
Seguridad
HttpOnly Cookies
refresh_token: Token de renovación (HttpOnly, SameSite=Strict)csrf_token: Token CSRF (HttpOnly, SameSite=Strict)
Configuración:
- Desarrollo:
domain=None,secure=False,SameSite=Strict - Producción:
domain="floutic.com",secure=True,SameSite=Strict
CSRF Protection
- Token CSRF en cookie HttpOnly
- Token enviado en header
X-CSRF-Token - Validación en servidor para requests mutantes (POST, PUT, DELETE, PATCH)
Session ID Único
- Generado por ventana en
sessionStorage - Enviado en header
X-Session-ID - Validado en servidor para aislar sesiones entre ventanas privadas
Aislamiento de Sesiones
Triple protección:
- Cookies con
SameSite=Strict(aislamiento a nivel de cookies) - Sin
domainen desarrollo (aislamiento a nivel de origen) - Validación de
session_idúnico (aislamiento a nivel de aplicación)
Logout por Inactividad
Configuración
const IDLE_TIMEOUTS = {
admin: 30 * 60 * 1000, // 30 minutos
empresa: 60 * 60 * 1000, // 1 hora
experto: 60 * 60 * 1000, // 1 hora
};
Hook useIdleTimeout
useIdleTimeout({
timeout: IDLE_TIMEOUTS[user.role],
onIdle: () => {
logout();
showWarning('Sesión expirada por inactividad');
}
});
Almacenamiento
sessionStorage
session_id: ID único por ventana- Aislado entre ventanas privadas
Memoria (no persistente)
access_token: Token de acceso actual- Se pierde al recargar la página
Cookies (HttpOnly)
refresh_token: Token de renovacióncsrf_token: Token CSRF
Más Información
- Seguridad - Autenticación - Documentación completa de seguridad
- Seguridad - Cookies - Seguridad de cookies y sesiones
- Arquitectura - Arquitectura del frontend