Skip to content

PortalAuthMiddleware

Responsabilidad

Middleware que gestiona la resolución de tenant y autenticación de cliente en el portal PWA:

  1. Resuelve tenant por dominio del request
  2. Autentica cliente (si ruta protegida) vía session PHP o JWT ligero
  3. Inyecta contextos (tenant_context, cliente_context) en request
  4. Delega a ConnectionMiddleware (existente) para configurar DB multi-tenant

Ventajas de Autenticación Simplificada

vs JWT de empresa (que usa machine tokens):

AspectoAuth SimplificadaJWT de Empresa
ComplejidadBajaAlta
SetupRápidoLento
OverheadMínimoValidación RSA
MantenimientoSimpleClaves, tokens

Flujo de Operación

1. Resolución de Tenant

Entrada: Dominio del request (ej: ctacte.empresaA.com.ar)

Proceso:

  1. Extraer host del request
  2. Buscar en tabla tenant_domains por dominio
  3. Verificar que el tenant esté activo
  4. 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-cuenta
  • GET /portal/deudas
  • POST /portal/pagos/iniciar
  • GET /portal/pagos/historial
  • POST /portal/cupones/generar
  • GET /portal/cupones/*

Validaciones de Seguridad

Verificación de Tenant

  1. El dominio debe existir en tenant_domains
  2. El tenant debe estar activo (status = 'active')
  3. Si no cumple → Error 401 "Tenant no encontrado"

Verificación de Cliente

  1. Debe tener sesión/token válido
  2. La sesión no debe estar expirada
  3. El cliente debe pertenecer al tenant actual (cliente.tenant_id = tenant.tenant_id)
  4. 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.com con 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

  1. Implementar TenantDomainModel para resolver dominios
  2. Configurar rutas en Slim Framework
  3. Ver autenticación de clientes en ../services/client-identification-service.md