Skip to main content

Autenticación y scopes (cliente → gateway)

El cliente se autentica contra POST /fhir con una API key. El gateway no emite ni guarda las keys: delega la validación al SSO externo ajolote. Localmente sólo mantiene una tabla client que mapea el cliente de ajolote a un clientId interno y al tenant de Proteus.

Este es el primer salto de auth; el segundo (gateway → backend) es un secreto compartido aparte (ver Visión general).

Cómo se autentica el cliente

  • Header x-api-key: <key> (o Authorization: Bearer <key>).
  • POST /fhir es la única ruta protegida (ApiKeyGuard); responde 202 si la ingesta se acepta.
  • La key se crea en el panel de Pupo y la valida ajolote. Para el gateway la key es opaca: no parsea ningún formato, la pasa tal cual a ajolote.

Flujo de validación

POST /fhir (x-api-key)


ApiKeyGuard ── extrae key + IP real (x-forwarded-for / req.ip)


ClientService.resolveClientFromKey(key, ip)
│ └─► ExternalAuthClient.validate(key, ip) ─► ajolote: POST /api/auth/api-key/verify
│ body { key, ip }, header X-App-Id
│ ← { valid, client:{ id, name, appId, scopes[] }, reason }

resuelve/crea la fila `client` (por externalClientId) → adjunta clientId al request

Si ajolote responde valid:false (key inválida/expirada/revocada, o ip_not_allowed), o si no hay fila client activa, el guard responde 401. La autorización es binaria: una key válida y activa que resuelva a un client activo puede ingerir. No hay scope por-recurso ni un scope de "ingesta" que habilite /fhir.

Variables de entorno

EnvUso
EXTERNAL_AUTH_USE_MOCKtrue usa el adapter mock (local/e2e); si no, el real (ajolote)
AUTH_SERVICE_URLBase URL de ajolote (adapter real)
AUTH_APP_IDX-App-Id enviado a ajolote (default proteus)
CLIENT_COMPANY_SCOPE_PREFIXPrefijo del scope de tenant (default company:)
CLIENT_AUTO_PROVISIONtrue auto-provisiona la fila client en el primer request válido
MOCK_API_KEY / MOCK_EXTERNAL_CLIENT_IDFixtures del adapter mock

Scopes → selección de tenant

El único scope que el gateway consume es el de empresa/tenant, con formato company:<appId>. De la lista scopes que devuelve ajolote, el gateway toma el primero que empieza con el prefijo (CLIENT_COMPANY_SCOPE_PREFIX) y usa el sufijo como proteusAppId.

Ese proteusAppId es el tenant de Proteus (COMPANIES.AJOLOTEAPPID) y viaja como clientAppId en cada mensaje al backend, que lo resuelve a un Company (ver El endpoint /inbound).

Los scopes no se persisten

La tabla client no guarda los scopes: sólo el proteusAppId derivado del scope company:<appId>. Los scopes viven en ajolote y se leen en cada validación.

La tabla client y el auto-provisioning

client mapea la identidad de ajolote al mundo interno:

CampoQué es
idPK interna; es el clientId que se adjunta al request
externalClientIdUUID del cliente en ajolote (único)
proteusAppIdTenant de Proteus, derivado del scope company:<appId> (o seteado a mano)
activeHabilitación local

Flujo (ClientService.resolveClientFromKey):

  1. Valida la key contra ajolote. Si no es válida/activa → null (401).
  2. Busca client por externalClientId activo. Si existe → sincroniza proteusAppId desde el scope (el scope gana; si la key no trae scope de empresa, no se pisa el valor existente).
  3. Si no existe y CLIENT_AUTO_PROVISION=truecrea la fila (externalClientId, name, proteusAppId del scope, active). Idempotente ante concurrencia. Si el auto-provision está apagado, una key válida sin fila sembrada se rechaza.

Seed manual

scripts/seed-client.ts siembra/actualiza la fila sin tocar ninguna key. Lee EXTERNAL_CLIENT_ID (UUID de ajolote), CLIENT_NAME y PROTEUS_APP_ID. El proteus_app_id usa COALESCE, así que omitirlo no pisa un tenant ya sincronizado desde el scope.

IP allowlist (fail-closed)

Cada API key de ajolote tiene un allowlist de IPs obligatorio, y la verificación es fail-closed: IP faltante o no permitida → valid:false, reason:"ip_not_allowed" → 401.

El gateway sólo extrae la IP real (primer valor de x-forwarded-for, si no req.ip; normaliza IPv4-mapeado) y se la pasa a ajolote. Además agrega su propia capa fail-closed: ante cualquier error, 5xx o servicio de auth inalcanzable, el adapter devuelve null → 401 (deniega, no habilita).

Las keys no viven en la documentación

Las API keys se crean en el panel de Pupo y las valida ajolote; nunca se commitean ni aparecen acá. La config del gateway (AUTH_SERVICE_URL, etc.) va por variables de entorno del ambiente.

Modo mock (local / e2e)

Con EXTERNAL_AUTH_USE_MOCK=true el gateway no llama a ajolote: acepta MOCK_API_KEY y resuelve a MOCK_EXTERNAL_CLIENT_ID (ignora la IP). Es lo que usa la suite e2e para no depender del SSO.