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>(oAuthorization: Bearer <key>). POST /fhires 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
| Env | Uso |
|---|---|
EXTERNAL_AUTH_USE_MOCK | true usa el adapter mock (local/e2e); si no, el real (ajolote) |
AUTH_SERVICE_URL | Base URL de ajolote (adapter real) |
AUTH_APP_ID | X-App-Id enviado a ajolote (default proteus) |
CLIENT_COMPANY_SCOPE_PREFIX | Prefijo del scope de tenant (default company:) |
CLIENT_AUTO_PROVISION | true auto-provisiona la fila client en el primer request válido |
MOCK_API_KEY / MOCK_EXTERNAL_CLIENT_ID | Fixtures 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).
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:
| Campo | Qué es |
|---|---|
id | PK interna; es el clientId que se adjunta al request |
externalClientId | UUID del cliente en ajolote (único) |
proteusAppId | Tenant de Proteus, derivado del scope company:<appId> (o seteado a mano) |
active | Habilitación local |
Flujo (ClientService.resolveClientFromKey):
- Valida la key contra ajolote. Si no es válida/activa →
null(401). - Busca
clientporexternalClientIdactivo. Si existe → sincronizaproteusAppIddesde el scope (el scope gana; si la key no trae scope de empresa, no se pisa el valor existente). - Si no existe y
CLIENT_AUTO_PROVISION=true→ crea la fila (externalClientId,name,proteusAppIddel 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 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.