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
CORS Middleware
→ PortalAuthMiddleware (nuevo)
→ ConnectionMiddleware (existente)
→ Route Handler / ControllerEl 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"
}