Appearance
PortalAuthMiddleware
Responsabilidad
Middleware que gestiona la autenticacion JWT y resolucion de tenant para el portal de clientes:
- Valida JWT del header
Authorization: Bearer(rutas protegidas) - Extrae
tenant_idysucursal_iddel JWT o del request body (rutas publicas) - Valida el tenant consultando
ini.sistema - Resuelve la base de datos a partir de
tenant_id - Resuelve el schema a partir de
sucursal_id - Establece la conexion delegando a
ConnectionMiddleware - Inyecta contextos (
tenant_context,portal_user_context) en el request
Flujo Completo
mermaid
flowchart TD
A[Request entrante] --> B{Ruta publica?}
B -->|Si| C[Extraer tenant_id + sucursal_id del body/headers]
B -->|No| D[Extraer JWT del header Authorization]
D --> E{JWT presente?}
E -->|No| F[401 Unauthorized]
E -->|Si| G[Validar firma JWT HS256 + PORTAL_JWT_SECRET]
G --> H{Firma valida?}
H -->|No| F
H -->|Si| I{Token expirado?}
I -->|Si| J[401 Token Expired]
I -->|No| K[Extraer payload: portal_user_id, tenant_id, sucursal_id]
K --> L[Validar tenant en ini.sistema]
C --> L
L --> M{Tenant existe y activo?}
M -->|No| N[401 Tenant invalido]
M -->|Si| O[Resolver database name]
O --> P[Resolver schema desde sucursal_id]
P --> Q[Inyectar tenant_context en request]
Q --> R{Ruta protegida?}
R -->|Si| S[Inyectar portal_user_context en request]
R -->|No| T[ConnectionMiddleware]
S --> T
T --> U[Controller]Detalle por Paso
1. Clasificacion de Ruta
Rutas publicas (no requieren JWT):
| Ruta | Metodo | Descripcion |
|---|---|---|
/portal/auth/register | POST | Auto-registro con DNI/CUIT |
/portal/auth/login | POST | Login con password |
/portal/auth/forgot-password | POST | Solicitar codigo de reset |
/portal/auth/reset-password | POST | Resetear password con codigo |
Todas las demas rutas son protegidas y requieren JWT valido.
Las rutas publicas igualmente necesitan tenant_id y sucursal_id para resolver la base de datos y el schema. Estos datos llegan en el body del request o en headers custom (X-Tenant-Id, X-Sucursal-Id).
2. Extraccion y Validacion de JWT
Header esperado: Authorization: Bearer <token>
Payload del JWT:
json
{
"portal_user_id": "uuid-del-usuario",
"tenant_id": 1,
"sucursal_id": 1,
"iat": 1738000000,
"exp": 1738003600
}Validaciones:
- Header
Authorizationpresente y con formatoBearer <token> - Firma valida con algoritmo HS256 usando
PORTAL_JWT_SECRETdel.env - Token no expirado (
exp> tiempo actual) - Claims requeridos presentes:
portal_user_id,tenant_id,sucursal_id
Separacion natural del Admin UI: El portal usa HS256 con PORTAL_JWT_SECRET, mientras que el Admin UI usa RS256 con claves diferentes. Esta separacion por algoritmo + secreto garantiza que un JWT del portal NUNCA puede funcionar en endpoints del Admin UI y viceversa — incluso si un atacante obtiene un token del portal, es criptograficamente imposible que pase la validacion del Admin UI.
3. Resolucion de Tenant
Entrada: tenant_id del JWT o del request body
Proceso:
- Conectar a base de datos
ini - Buscar en tabla
sistemaportenant_id - Verificar que el registro exista y este activo
- Obtener el nombre de la base de datos del tenant
Si el tenant no existe o esta inactivo: Retornar 401 Tenant invalido.
4. Resolucion de Schema
Entrada: sucursal_id del JWT o del request body
Proceso:
- Con la base de datos del tenant resuelta, determinar el schema
sucursal_idse traduce a schemasucXXXX(ej: sucursal 1 →suc0001)- Verificar que el schema exista en
information_schema.schemata
Caso especial: Si el tenant tiene ordcon configurado en public (LEVEL_EMPRESA), las tablas del portal tambien estan en public. El middleware debe respetar esta configuracion.
5. Inyeccion de Contextos
tenant_context (siempre inyectado):
php
$request = $request->withAttribute('tenant_context', [
'tenant_id' => 1,
'sucursal_id' => 1,
'database' => 'empresa_a',
'schema' => 'suc0001',
]);portal_user_context (solo rutas protegidas):
php
$request = $request->withAttribute('portal_user_context', [
'portal_user_id' => 'uuid-del-usuario',
'cliente_id' => 123, // obtenido de portal_users
]);6. Delegacion a ConnectionMiddleware
El ConnectionMiddleware existente recibe tenant_context y configura la conexion:
php
$tenantContext = $request->getAttribute('tenant_context');
$connectionManager->setCurrentDatabase($tenantContext['database']);
$connectionManager->setSchema($tenantContext['schema']);No requiere modificaciones al ConnectionMiddleware. El PortalAuthMiddleware simplemente inyecta el contexto en el formato que ConnectionMiddleware ya espera.
Proteccion contra Cross-Tenant Access
El JWT contiene tenant_id y sucursal_id firmados. Un atacante no puede modificar estos valores sin invalidar la firma del token. Esto elimina el riesgo de acceso cross-tenant porque:
- El token es firmado por el servidor al momento del login
tenant_idysucursal_idestan embebidos en el payload firmado- Cualquier modificacion invalida el token
- El middleware rechaza tokens con firma invalida antes de procesar cualquier logica
Validaciones de Seguridad
Verificacion de Tenant
tenant_iddebe existir enini.sistema- El tenant debe estar activo
- Si no cumple: Error
401 Tenant invalido
Verificacion de Usuario
- JWT debe tener firma valida y no estar expirado
portal_user_iddel JWT debe existir enportal_usersdel schema resuelto- El usuario no debe estar bloqueado (
locked_untilNULL o en el pasado) - Si no cumple: Error
401 No autenticadoo423 Cuenta bloqueada
Rate Limiting
- Max 10 requests de login por minuto por IP
- Max 5 requests de registro por minuto por IP
- Max 3 requests de forgot-password por hora por email
Consideraciones de Implementacion
Ubicacion en el Stack de Middleware
PortalCorsMiddleware (global, path-aware: solo actua en /backend/portal)
→ PortalAuthMiddleware (grupo de rutas /backend/portal)
→ ConnectionMiddleware (existente)
→ Route Handler / ControllerPortalCorsMiddleware se registra como middleware global en index.php pero actua solo cuando el path comienza con /backend/portal. Para todas las demas rutas delega sin modificar la respuesta. Esto garantiza que los preflight OPTIONS sean respondidos antes de cualquier validacion de autenticacion.
El PortalAuthMiddleware se registra ANTES del ConnectionMiddleware porque necesita resolver el tenant para que ConnectionMiddleware pueda configurar la conexion.
Manejo de Errores
| Codigo | Situacion |
|---|---|
| 401 | JWT ausente, invalido, expirado, o tenant invalido |
| 423 | Cuenta bloqueada por intentos fallidos |
| 500 | Error interno al resolver tenant o schema |
Todos los errores retornan JSON con estructura consistente:
json
{
"error": "UNAUTHORIZED",
"message": "Token expirado"
}