Appearance
PortalAuthMiddleware
Responsabilidad
Middleware que gestiona la resolución de tenant y autenticación de cliente en el portal PWA:
- Resuelve tenant por dominio del request
- Autentica cliente (si ruta protegida) vía session PHP o JWT ligero
- Inyecta contextos (tenant_context, cliente_context) en request
- Delega a ConnectionMiddleware (existente) para configurar DB multi-tenant
Ventajas de Autenticación Simplificada
vs JWT de empresa (que usa machine tokens):
| Aspecto | Auth Simplificada | JWT de Empresa |
|---|---|---|
| Complejidad | Baja | Alta |
| Setup | Rápido | Lento |
| Overhead | Mínimo | Validación RSA |
| Mantenimiento | Simple | Claves, tokens |
Flujo de Operación
1. Resolución de Tenant
Entrada: Dominio del request (ej: ctacte.empresaA.com.ar)
Proceso:
- Extraer host del request
- Buscar en tabla
tenant_domainspor dominio - Verificar que el tenant esté activo
- Si no existe o está inactivo → Error 401
Salida (tenant_context):
json
{
"tenant_id": 1,
"sistema_id": 123,
"database": "empresa_a",
"schema": "public",
"branding": {
"app_name": "Portal Empresa A",
"logo_url": "https://...",
"primary_color": "#1e40af"
},
"domain": "ctacte.empresaA.com.ar"
}2. Autenticación de Cliente
Solo para rutas protegidas (ver lista abajo).
Opción A: Session PHP (Recomendada para MVP)
Después de identificar cliente, se guarda en sesión:
json
{
"cliente_id": 123,
"tenant_id": 1,
"nombre": "Juan Pérez",
"expires": 1738012800
}Validaciones:
- Verificar que la sesión existe
- Verificar que no esté expirada
- Verificar que el cliente pertenece al tenant actual
Opción B: JWT Ligero
Payload simple:
json
{
"cliente_id": 123,
"tenant_id": 1,
"nombre": "Juan Pérez",
"exp": 1738012800
}Firmado con HS256 (secret compartido).
3. Inyección de Contextos
Los contextos se inyectan como atributos del request:
$request->getAttribute('tenant_context'): Info del tenant$request->getAttribute('cliente_context'): Info del cliente (si auth requerida)
Los controladores pueden acceder a estos contextos directamente.
Rutas Públicas vs Protegidas
Rutas Públicas (sin autenticación de cliente)
POST /portal/auth/identify-client- Login del cliente
Rutas Protegidas (requieren autenticación)
Todas las demás rutas:
GET /portal/mi-cuentaGET /portal/deudasPOST /portal/pagos/iniciarGET /portal/pagos/historialPOST /portal/cupones/generarGET /portal/cupones/*
Validaciones de Seguridad
Verificación de Tenant
- El dominio debe existir en
tenant_domains - El tenant debe estar activo (
status = 'active') - Si no cumple → Error 401 "Tenant no encontrado"
Verificación de Cliente
- Debe tener sesión/token válido
- La sesión no debe estar expirada
- El cliente debe pertenecer al tenant actual (
cliente.tenant_id = tenant.tenant_id) - Si no cumple → Error 401 "No autenticado" o "Cliente no pertenece a este tenant"
Protección contra Cross-Tenant Access
Crítico: Verificar que el cliente del token pertenece al tenant resuelto por dominio.
Escenario de ataque:
- Usuario tiene token de
empresaA.com - Intenta acceder a
empresaB.comcon ese token - Middleware rechaza el request porque
cliente.tenant_id != tenant.tenant_id
Integración con ConnectionMiddleware
El PortalAuthMiddleware inyecta tenant_context en el request:
php
$request = $request->withAttribute('tenant_context', [
'database' => 'empresa_a',
'schema' => 'public'
]);El ConnectionMiddleware existente lee este contexto y configura la conexión:
php
$tenantContext = $request->getAttribute('tenant_context');
$connectionManager->setCurrentDatabase($tenantContext['database']);
$connectionManager->setSchema($tenantContext['schema']);Beneficio: No necesita modificar ConnectionMiddleware existente, solo reutilizarlo.
Consideraciones de Implementación
Session PHP vs JWT
Session PHP:
- ✅ Más simple para MVP
- ✅ Manejo automático de expiración
- ✅ Soporte nativo en PHP
- ❌ Requiere almacenamiento server-side
- ❌ No funciona bien con múltiples servidores (sin sticky sessions)
JWT Ligero:
- ✅ Stateless (no requiere almacenamiento)
- ✅ Funciona en múltiples servidores
- ✅ Fácil de implementar con
firebase/php-jwt - ❌ No se puede invalidar (hasta que expire)
- ❌ Requiere gestión de secrets
Recomendación: Empezar con Session PHP para MVP, migrar a JWT si se necesita escalar.
Expiración
Session PHP: Configurar timeout de 1 hora
php
$_SESSION['portal_cliente']['expires'] = time() + 3600;JWT: Configurar exp claim a 1 hora
json
{
"exp": 1738012800
}Rate Limiting
Aunque no es responsabilidad directa del middleware, considerar agregar:
- Max 5 intentos de login/minuto por IP
- Max 100 requests/minuto por dominio
- Bloqueo temporal después de 3 intentos fallidos
Próximos Pasos
- Implementar
TenantDomainModelpara resolver dominios - Configurar rutas en Slim Framework
- Ver autenticación de clientes en ../services/client-identification-service.md