Authentication
Every credential type Ductor accepts — OIDC/JWT, DB-backed API keys, SAML browser sessions, and internal service tokens — and how to configure each.
Authentication answers one question: who is calling? Ductor's API server
accepts four credential types, each backed by a ClaimMapper that validates the
credential and returns a verified authn.Principal. This page covers how each one
works and how to turn it on.
How the credential is read
On every request the authn layer checks the X-API-Key header first, then falls
back to Authorization: Bearer <token>. Over gRPC the equivalents are the
x-api-key and authorization metadata keys. The same header carries both
API keys and JWTs — the ClaimMapper chain decides which validator claims it.
At a glance
| Method | Credential | Header | Turned on by | Identity source |
|---|---|---|---|---|
| OIDC / JWT | Signed JWT | Authorization: Bearer <jwt> | api.oidc_issuer + api.oidc_audience | Your IdP (via JWKS) |
| DB-backed API key | duk_… secret | X-API-Key or Bearer | auth.api_key_enabled + key store | tenant_api_key table |
| SAML session | HS256 session JWT | Authorization: Bearer <token> | identity.saml.enabled | Per-tenant IdP → minted session |
| Service token | HS256 token | Authorization: Bearer <token> | auth.service_token_secret | Internal transport plane |
OIDC / JWT
The primary mechanism for human and machine callers alike. Ductor validates bearer JWTs against your identity provider's published keys — it never holds a shared secret for this path.
Configure
export DUCTOR_AUTH_ENABLED=true
export DUCTOR_API_OIDC_ISSUER="https://your-idp.example.com/"
export DUCTOR_API_OIDC_AUDIENCE="ductor-api"Both issuer and audience are required; setting only one leaves OIDC inert (the JWT mapper is not registered). Clients then send:
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6...How validation works
- Keys (JWKS). On startup Ductor fetches the issuer's JWKS. By default the
endpoint is derived as
<issuer>/.well-known/jwks.json; explicit JWKS URLs can be supplied in config. Keys are cached and refreshed every 5 minutes in the background. - Algorithms. Only asymmetric signatures are accepted —
RS256andES256by default. The verifier pins the allowed methods, which blocks the classic algorithm-downgrade attack (anHS256token signed with the public key as the secret is rejected). - Claims → Principal.
subbecomesSubject(required),tenant_id→TenantID,org_id→OrgID,client_id/azp→ClientID, the space-delimitedscope→Scopes, and the permissions claim (default namepermissions) →Roles. - Expiry.
expis honored with a default 1 minute of clock-skew tolerance.
Failure handling: the JWKS grace window
The subtlety worth seeing: an IdP outage does not break auth immediately. Tokens keep validating against cached keys right up to the edge of the grace window — and then flip to rejecting everything at once:
If the IdP's JWKS endpoint becomes unreachable, Ductor keeps trusting its cached keys for a bounded grace window (default 1 hour). Past that window without a successful refresh, the provider clears its cache and rejects all tokens (fail-closed) until the IdP recovers.
Watch the JWKS health signal
A stale cache emits an error-level log (jwks cache stale: rejecting tokens)
and increments a stale refresh counter exactly once per episode. Wire the
provider's health into your readiness probe so operators see degraded auth
before users report 401s. See
Observability.
DB-backed API keys
Durable, revocable, per-tenant credentials for service-to-service traffic — minted and managed through the Tenant API Key API. Enable them with:
export DUCTOR_AUTH_API_KEY_ENABLED=trueCallers present the raw key in either header:
X-API-Key: duk_live_9f2c… # preferred
Authorization: Bearer duk_live_9f2c… # also acceptedHow validation works
- Ductor keys always begin with the
duk_prefix. The first 8 characters are stored as a lookup prefix, so a presented key is resolved to at most one or two candidate rows — the expensive bcrypt comparison runs against that tiny set, not every tenant key. - The stored bcrypt hash is compared in constant time. A token that carries
the
duk_prefix but is too short is rejected up front, so an attacker cannot turn a short key into a broad lookup prefix (a CPU-amplification guard). - On success the Principal is built from the key:
Subjectisapikey:<id>,TenantID/Roles/Scopes/environment scope come from the row, and a fire-and-forget update stampslast_used_at. - Only active, non-expired keys authenticate. Revoked or expired keys are rejected immediately.
See the API keys deep dive for minting, roles, scopes, environment scope, expiry, storage, and rotation.
Use database-backed API keys
The v2 API supports OIDC, database-backed API keys, SAML, and service tokens.
The legacy auth.api_keys and DUCTOR_AUTH_API_KEYS settings are not supported
for request authentication. Set auth.api_key_enabled=true and mint keys through the
Tenant API Key API.
SAML browser sessions
For enterprise SSO, Ductor runs a SAML 2.0 Service Provider. It is off by default; when disabled, no SAML route is mounted and behavior is byte-identical to a build without it.
export DUCTOR_IDENTITY_SAML_ENABLED=true
export DUCTOR_IDENTITY_SAML_SP_ENTITY_ID="https://app.example.com/saml/{tenant}/metadata"
export DUCTOR_IDENTITY_SAML_ACS_BASE_URL="https://app.example.com" # must be HTTPSThe flow
One SP serves all tenants via per-tenant path scoping, with per-tenant IdP config
managed out of band (CLI: ductor saml-idp set/get/disable). After a successful
assertion at the ACS endpoint, Ductor mints an HS256 session JWT for the
(tenant, NameID) pair. That token carries a fixed audience (saml_session) and
issuer (ductor), the user's email and roles, a unique jti, and a default
8-hour TTL.
The browser then presents that session token as a normal Authorization: Bearer
credential; the SAML session mapper verifies it. Its signing key is a per-tenant
secret (minimum 32 bytes) stored AEAD-encrypted at rest.
Single Logout (SLO) and revocation
With identity.saml.slo_enabled=true, the /saml/{tenant}/slo endpoint is
mounted. Both IdP-initiated and SP-initiated logout revoke the session's jti
(recorded in saml_session_revocation), and an sso.logout audit event is
emitted. Session verification then consults the revocation store and rejects a
logged-out session even before its exp.
SLO enforcement is mandatory when enabled
If slo_enabled=true but no revocation checker is wired, startup fails — the
system refuses to silently downgrade logout to plain bearer validation with no
revocation lookup. On a revocation-store outage, verification fails closed
(treats the session as invalid) rather than admitting a possibly-logged-out
session.
Service tokens (internal plane)
For coordinator-to-coordinator and worker-to-coordinator traffic on the internal transport plane, Ductor validates HS256 service tokens signed with a shared secret:
export DUCTOR_AUTH_SERVICE_TOKEN_SECRET="<32+ byte high-entropy secret>"An empty secret disables service tokens. These carry the internal-only service
role, whose scope bundle is read access plus the workflow-control surface; the
internal RPC surface is additionally gated by an internal-only method option, not
just by scope. Do not assign the service role to human identities.
Selecting and composing mappers
By default the API composes API key → JWT (with SAML inserted between them when
enabled). To pin or compose explicitly, set auth.claim_mapper:
# Single validator
export DUCTOR_AUTH_CLAIM_MAPPER="jwt"
# Explicit chain (tried left to right; ErrMissingToken falls through)
export DUCTOR_AUTH_CLAIM_MAPPER="apikey,jwt"Built-in factory names are jwt, apikey, and service-token (plus the SAML
session factory when SAML is enabled). Operators can register custom factories via
fx.Decorate(*claimmapper.Registry) and select them here by name.
Config reference
| Env var | Config key | Purpose |
|---|---|---|
DUCTOR_AUTH_ENABLED | auth.enabled | Master switch for authentication |
DUCTOR_AUTH_ALLOW_ANONYMOUS | auth.allow_anonymous | Dev-only escape hatch (requires environment=development) |
DUCTOR_API_OIDC_ISSUER | api.oidc_issuer | OIDC issuer URL |
DUCTOR_API_OIDC_AUDIENCE | api.oidc_audience | Expected JWT aud |
DUCTOR_AUTH_API_KEY_ENABLED | auth.api_key_enabled | Enable DB-backed API keys |
DUCTOR_AUTH_CLAIM_MAPPER | auth.claim_mapper | Select/compose the mapper(s) |
DUCTOR_AUTH_SERVICE_TOKEN_SECRET | auth.service_token_secret | HS256 service-token secret |
DUCTOR_IDENTITY_SAML_ENABLED | identity.saml.enabled | Enable the SAML SP |
Next
Ready to issue credentials? Go to the API keys deep dive. Want to control what an authenticated caller may do? See Authorization.