Authenticate to your backend¶
A connector calls a backend on the caller's behalf. How it authenticates to that backend is independent of how the caller authenticated to Pontifex.
One question decides which mode you want:
Does the backend need to know which user is calling?
- No, it only needs to trust Pontifex. Use a service credential: one identity for every caller.
- Yes, it enforces its own per-user permissions. Use token exchange: Pontifex swaps the caller's token for a downstream one minted for that user.
Service credential¶
The right default for most internal APIs. Per-user authorization still happens at Pontifex's scope layer, and the audit log still records who made each call. The backend sees one trusted identity.
bearer_env- Sends
Authorization: Bearer <token>, read from the named environment variable. header_env(add aheader:name)- Sends a static header (e.g.
X-API-Key), read from the named environment variable.
A missing variable fails at startup. The value is re-read on every request, so rotating the secret doesn't need a restart.
Token exchange (per-user)¶
For a backend that shares your OAuth provider and enforces per-user authorization.
Pontifex takes the caller's inbound token and exchanges it at the IdP (RFC 8693) for a new token carrying the backend's audience, on behalf of the user. The downstream then applies that user's own permissions.
auth:
type: token_exchange
token_endpoint: https://idp.example.com/oauth/token
audience: https://api.internal # the downstream's audience
client_id_env: PONTIFEX_OAUTH_CLIENT_ID # Pontifex's own IdP client,
client_secret_env: PONTIFEX_OAUTH_CLIENT_SECRET # presence checked at boot
The caller's token is never forwarded as-is. No passthrough. A token minted for Pontifex's audience wouldn't be accepted downstream, and forwarding it would break the trust chain.
API-key callers can't use this
A connector is either service-auth or user-auth, never both. API-key callers
carry no token to exchange, so a token_exchange connector rejects them with a
clear invalid_input. If a backend needs both modes, define two
connector entries with distinct namespaces.
Provider differences¶
The request is plain RFC 8693, so any compliant provider works: Keycloak, Auth0, Microsoft Entra (on-behalf-of), Okta. Two things differ per provider:
- What
audiencemeans. It's the downstream's identifier as your provider expects it: a client ID (Keycloak), an API identifier URL (Auth0), a resource/scope (Entra). Set it to whatever your provider puts in the exchanged token'saud. - Provider-side authorization. Each provider gates which clients may exchange for which audiences in its own model. That setup lives in your IdP, not in Pontifex.
Two optional knobs cover protocol differences:
client_auth: post(default) sends client credentials in the form.basicsends them as an HTTP Basic header, for providers that require it.default_ttl_seconds.expires_inis optional in RFC 8693. By default Pontifex rejects a response without it, since it can't size the cache TTL. Set this to supply a fallback.
The exchanged-token cache¶
Pontifex caches exchanged tokens so a user's repeated calls don't re-hit the IdP. The backend is a deployment-level setting:
PONTIFEX_TOKEN_CACHE=memory(default): in-process only. Tokens never leave the process or hit disk. Each worker caches on its own.PONTIFEX_TOKEN_CACHE=redis: shared across workers via Redis (reusesREDIS_URL). Pontifex encrypts the tokens at rest with a Fernet key fromPONTIFEX_TOKEN_CACHE_KEY:
A Redis dump yields only ciphertext. The key lives in the environment, not in Redis.
Missing REDIS_URL or PONTIFEX_TOKEN_CACHE_KEY fails at startup.
Note
Using a managed KMS instead of an env-held key is tracked in #52.
What you can observe¶
With Logfire configured, the token-exchange path emits metrics. Labels cover audience, outcome, and cache result only, never tokens:
pontifex.token_exchange.requests, byoutcome(ok/rejected/unavailable/error).pontifex.token_exchange.duration_ms, the IdP exchange latency.pontifex.token_cache.requests, byresult(hit/miss/coalesced).
The IdP sits on the call path. Its failures surface as source_unavailable, and
Pontifex circuit-breaks them independently of the downstream. A refused exchange surfaces
as invalid_input.