Skip to content

PortalAuthMiddleware

Responsabilidad

Middleware que gestiona la autenticacion JWT y resolucion de tenant para el portal de clientes:

  1. Valida JWT del header Authorization: Bearer (rutas protegidas)
  2. Extrae tenant_id y sucursal_id del JWT o del request body (rutas publicas)
  3. Valida el tenant consultando ini.sistema
  4. Resuelve la base de datos a partir de tenant_id
  5. Resuelve el schema a partir de sucursal_id
  6. Establece la conexion delegando a ConnectionMiddleware
  7. 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):

RutaMetodoDescripcion
/portal/auth/registerPOSTAuto-registro con DNI/CUIT
/portal/auth/loginPOSTLogin con password
/portal/auth/forgot-passwordPOSTSolicitar codigo de reset
/portal/auth/reset-passwordPOSTResetear 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:

  1. Header Authorization presente y con formato Bearer <token>
  2. Firma valida con algoritmo HS256 usando PORTAL_JWT_SECRET del .env
  3. Token no expirado (exp > tiempo actual)
  4. 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:

  1. Conectar a base de datos ini
  2. Buscar en tabla sistema por tenant_id
  3. Verificar que el registro exista y este activo
  4. 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:

  1. Con la base de datos del tenant resuelta, determinar el schema
  2. sucursal_id se traduce a schema sucXXXX (ej: sucursal 1 → suc0001)
  3. 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:

  1. El token es firmado por el servidor al momento del login
  2. tenant_id y sucursal_id estan embebidos en el payload firmado
  3. Cualquier modificacion invalida el token
  4. El middleware rechaza tokens con firma invalida antes de procesar cualquier logica

Validaciones de Seguridad

Verificacion de Tenant

  1. tenant_id debe existir en ini.sistema
  2. El tenant debe estar activo
  3. Si no cumple: Error 401 Tenant invalido

Verificacion de Usuario

  1. JWT debe tener firma valida y no estar expirado
  2. portal_user_id del JWT debe existir en portal_users del schema resuelto
  3. El usuario no debe estar bloqueado (locked_until NULL o en el pasado)
  4. Si no cumple: Error 401 No autenticado o 423 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 / Controller

El PortalAuthMiddleware se registra ANTES del ConnectionMiddleware porque necesita resolver el tenant para que ConnectionMiddleware pueda configurar la conexion.

Manejo de Errores

CodigoSituacion
401JWT ausente, invalido, expirado, o tenant invalido
423Cuenta bloqueada por intentos fallidos
500Error interno al resolver tenant o schema

Todos los errores retornan JSON con estructura consistente:

json
{
  "error": "UNAUTHORIZED",
  "message": "Token expirado"
}