Auth & Security

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

MethodCredentialHeaderTurned on byIdentity source
OIDC / JWTSigned JWTAuthorization: Bearer <jwt>api.oidc_issuer + api.oidc_audienceYour IdP (via JWKS)
DB-backed API keyduk_… secretX-API-Key or Bearerauth.api_key_enabled + key storetenant_api_key table
SAML sessionHS256 session JWTAuthorization: Bearer <token>identity.saml.enabledPer-tenant IdP → minted session
Service tokenHS256 tokenAuthorization: Bearer <token>auth.service_token_secretInternal 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 — RS256 and ES256 by default. The verifier pins the allowed methods, which blocks the classic algorithm-downgrade attack (an HS256 token signed with the public key as the secret is rejected).
  • Claims → Principal. sub becomes Subject (required), tenant_idTenantID, org_idOrgID, client_id/azpClientID, the space-delimited scopeScopes, and the permissions claim (default name permissions) → Roles.
  • Expiry. exp is 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:

refresh fails refresh succeeds grace window elapsed IdP recovers Fresh Stale FailClosed

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=true

Callers present the raw key in either header:

X-API-Key: duk_live_9f2c…            # preferred
Authorization: Bearer duk_live_9f2c… # also accepted

How 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: Subject is apikey:<id>, TenantID/Roles/Scopes/environment scope come from the row, and a fire-and-forget update stamps last_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 HTTPS

The 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 varConfig keyPurpose
DUCTOR_AUTH_ENABLEDauth.enabledMaster switch for authentication
DUCTOR_AUTH_ALLOW_ANONYMOUSauth.allow_anonymousDev-only escape hatch (requires environment=development)
DUCTOR_API_OIDC_ISSUERapi.oidc_issuerOIDC issuer URL
DUCTOR_API_OIDC_AUDIENCEapi.oidc_audienceExpected JWT aud
DUCTOR_AUTH_API_KEY_ENABLEDauth.api_key_enabledEnable DB-backed API keys
DUCTOR_AUTH_CLAIM_MAPPERauth.claim_mapperSelect/compose the mapper(s)
DUCTOR_AUTH_SERVICE_TOKEN_SECRETauth.service_token_secretHS256 service-token secret
DUCTOR_IDENTITY_SAML_ENABLEDidentity.saml.enabledEnable 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.