JWT/OIDC Inbound Auth¶
This page records the design of optional JWT validation for inbound bearer tokens (issue #2375) and describes the behavior that shipped. Configuration and operational guidance live in Security & Admin.
Shipped Behavior¶
- Config surface:
api_keys.jwtnames the issuer, audience, one key source (jwks_uriorstatic_jwks), the accepted asymmetric algorithms, and the claim mappings. The section is off by default. Static-key authentication is unchanged while it is absent or disabled. - Resolution order: the auth middleware matches the presented credential against the key store first. Only a miss is examined as a JWT, so a static key always wins. A token whose unverified
issis not the configured issuer falls through to the existing key-miss path. - Validator: the JWT branch verifies the signature against the JWKS and checks
iss,aud,exp, andnbfwith a bounded clock skew. It accepts only the configured asymmetric algorithms. - Same
AuthContext: a valid token produces the sameAuthContexta static key produces, with the key id derived from the issuer and subject. Per-key rate limits,allowed_models,allowed_backends, budgets, and request attribution work unchanged. - Claim mapping:
subject_claim,user_claim, andteam_claimmap identity;scopes_claim,allowed_models_claim, andallowed_backends_claimmap grants. A JWT scope list is authoritative, including an empty one, so an unmapped scope claim grants nothing. - Key caching: the JWKS is cached with a TTL. An unknown
kidtriggers at most one refresh per configured interval, which picks up a rotated signing key. Each fetch has a hard timeout and a body-size limit. - Failure behavior: an expired, wrong-audience, or wrong-signer token returns
401with the standard bad-key body, in blocking and permissive mode alike. A JWKS fetch failure fails closed. Rejections and fetch outcomes are counted in Prometheus. - Build graph: the validator needs
jsonwebtoken, which theembednegative dependency contract denies. The runtime is therefore behind thejwt-authCargo feature, which is outsidedefaultandfull. The config types stay compiled in every build. A binary without the feature warns at load time that the section is inert.
Design Decisions¶
- Asymmetric only:
none, HMAC algorithms, and every algorithm outside the allow-list are refused before key selection, and akty: octJWK can never be selected. - No token-selected key material:
jku,jwk, andx5uheaders are refused. A token cannot choose the key or endpoint used to verify it. - Bounded work: token size, algorithm count,
kidlength, JWKS body size, key count, claim lengths, list counts, and scope count all have fixed limits. - Bounded network behavior: the JWKS client follows no redirects, and the URI must be
https, orhttpon a loopback host. The streamed body is cut off at the configured byte limit. - Single-flight refresh: concurrent unknown-
kidrequests share one fetch, and a failed fetch is not retried inside the refresh interval. A stale cache is never used after its TTL. - Explicit scope denial: a principal with an authoritative but empty scope list satisfies only routes that require no scope. This keeps an unmapped JWT from inheriting the static-key "empty list is unrestricted" reading.
- Fail closed on the configured issuer only: a token that names the configured issuer is refused on any later failure. A token for another issuer is indistinguishable from a malformed static key and keeps its previous behavior.
Not Covered¶
- Token introspection, refresh-token exchange, and provider discovery (
/.well-known/openid-configuration). - Per-provider claim mappings: one
jwtsection maps one issuer. - JWKS pushes or scheduled pre-refresh; the cache refreshes on a TTL or an unknown
kid. - Validating tokens for endpoints with their own authentication (Admin, Files, Metrics), which are unaffected.
History¶
The issue was opened as a design note. The initial page proposed the config surface, the validator branch, AuthContext reuse, claim mapping, key caching, failure behavior, and the build-graph constraint above. It left two questions open: the exact claim names, and whether the runtime gets its own Cargo feature or joins full. The implementation settled both: claim names are configurable with generic defaults, and the runtime is a separate opt-in feature.