You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Today the only auth providers for mcp serve are BearerTokenAuth (static token → subject map) and ForwardedUserAuth (reads a header set by an upstream reverse proxy like oauth2-proxy). Both cover small deployments well, but neither fits the case of a team already using an OIDC provider (Keycloak, Auth0, Okta, Google Workspace, Azure AD) and wanting to hand out short-lived JWTs to users or services instead of managing a static token map.
With #53 in place, identities can carry real roles. A JWT provider is the natural way to feed those roles from an existing identity platform — the claims are already there, we just need to read them.
Depends on: #53 (so the provider has a well-defined contract for populating roles on AuthIdentity).
Goal
Add a new auth provider that validates JWTs on incoming requests, extracts subject and roles from configurable claims, and plugs into the same AuthIdentity pipeline the other providers use. It must work against standard OIDC providers out of the box without custom glue.
Expected behavior
Configuration
Extend serverAuth in servers.json to accept a new provider type:
issuer (required): expected iss claim. Tokens with a different issuer are rejected.
audience (required): one or more expected aud values. Token is valid if its aud intersects this list.
jwksUrl (required for asymmetric keys): URL to fetch the JWKS document. Cached in memory, refreshed on unknown kid, with a sensible minimum refresh interval to avoid hammering the IdP.
subjectClaim (default "sub"): which claim to read as the identity subject.
rolesClaim (default "groups"): which claim to read as the roles list. Must support:
A top-level array claim ("groups": ["dev", "oncall"]).
A dot-path into nested claims ("realm_access.roles" → common Keycloak layout).
A single string value, treated as a one-element array.
Optional future-proofing fields (document as reserved but not required in v1): algorithms allow-list, requiredClaims, staticJwks for offline/testing.
Token extraction
Read the bearer token from the Authorization: Bearer <token> header. Same extraction path as BearerTokenAuth, but validation differs.
Missing / malformed header → Unauthorized, no identity synthesized, same behavior as other providers on missing creds.
Validation
Signature verified against the JWKS, matching kid from the token header.
iss, aud, exp, nbf, iat all validated, with leewaySeconds applied to time claims.
Any failure → Unauthorized. Failures must be logged with enough context to debug (expected issuer vs actual, etc.) but must never log the token itself.
On JWKS fetch failures for an unknown kid: retry once, then reject the token. Never fall back to an expired cached key silently — log loudly.
Identity population
subject: value of the configured subjectClaim. Missing claim → reject.
roles: value of the configured rolesClaim, normalized to Vec<String>. Missing claim → empty vec (not an error — an authenticated user with no roles is valid).
In-memory cache keyed by jwksUrl. No on-disk persistence.
Cache miss on an unknown kid triggers a refresh, rate-limited to avoid DoS amplification (e.g., at most one refresh per N seconds per URL, implementation choice).
Reasonable default TTL so revoked keys eventually stop working even without traffic-driven invalidation.
Out of scope
Token issuance or user management. This provider validates tokens; it does not mint them.
Opaque token introspection (RFC 7662). Only JWTs. Introspection can be a separate provider later if needed.
mTLS / DPoP / token binding.
Dynamic per-request claim transformation pipelines. The mapping is: one claim for subject, one claim for roles. If users need more, they can configure their IdP to shape the claim how they want.
Replacing BearerTokenAuth or ForwardedUserAuth. This is an additional provider, not a replacement.
Technical pointers
A maintained Rust JWT library handles signature validation, claims parsing, and JWKS — pick one already in the ecosystem rather than rolling crypto by hand. The choice of crate is up to the implementer; preference for something with active maintenance and support for the common OIDC algorithms (RS256, RS384, RS512, ES256, EdDSA).
Provider code belongs alongside the existing providers in src/server_auth/providers.rs (or a new module under src/server_auth/ if it gets large).
The AuthProvider trait may need a small extension to allow async operations (JWKS fetching). If it is sync today, the cleanest path is to make the validation step async and let the proxy already-async request handler await it.
Tests: use a fixture JWKS and pre-signed tokens (valid, expired, wrong issuer, wrong audience, missing subject claim, nested roles claim, unknown kid). Do not call out to a real IdP in unit tests; a local static JWKS is enough.
Integration test against mcp acl check (from ACL: mcp acl check CLI and enriched audit log entries #56) would be nice: feed it a synthetic JWT-backed identity and verify the ACL decision, but only if the provider test surface allows it cleanly.
Success criteria
A team using Keycloak can point mcp serve at their realm with only the four required config fields and have tokens issued by that realm authenticate correctly.
Today the only auth providers for
mcp serveareBearerTokenAuth(static token → subject map) andForwardedUserAuth(reads a header set by an upstream reverse proxy like oauth2-proxy). Both cover small deployments well, but neither fits the case of a team already using an OIDC provider (Keycloak, Auth0, Okta, Google Workspace, Azure AD) and wanting to hand out short-lived JWTs to users or services instead of managing a static token map.With #53 in place, identities can carry real roles. A JWT provider is the natural way to feed those roles from an existing identity platform — the claims are already there, we just need to read them.
Depends on: #53 (so the provider has a well-defined contract for populating roles on
AuthIdentity).Goal
Add a new auth provider that validates JWTs on incoming requests, extracts subject and roles from configurable claims, and plugs into the same
AuthIdentitypipeline the other providers use. It must work against standard OIDC providers out of the box without custom glue.Expected behavior
Configuration
Extend
serverAuthinservers.jsonto accept a new provider type:issuer(required): expectedissclaim. Tokens with a different issuer are rejected.audience(required): one or more expectedaudvalues. Token is valid if itsaudintersects this list.jwksUrl(required for asymmetric keys): URL to fetch the JWKS document. Cached in memory, refreshed on unknownkid, with a sensible minimum refresh interval to avoid hammering the IdP.subjectClaim(default"sub"): which claim to read as the identity subject.rolesClaim(default"groups"): which claim to read as the roles list. Must support:"groups": ["dev", "oncall"])."realm_access.roles"→ common Keycloak layout).leewaySeconds(default30): clock skew tolerance forexp/nbf/iat.algorithmsallow-list,requiredClaims,staticJwksfor offline/testing.Token extraction
Authorization: Bearer <token>header. Same extraction path asBearerTokenAuth, but validation differs.Unauthorized, no identity synthesized, same behavior as other providers on missing creds.Validation
kidfrom the token header.iss,aud,exp,nbf,iatall validated, withleewaySecondsapplied to time claims.Unauthorized. Failures must be logged with enough context to debug (expected issuer vs actual, etc.) but must never log the token itself.kid: retry once, then reject the token. Never fall back to an expired cached key silently — log loudly.Identity population
subject: value of the configuredsubjectClaim. Missing claim → reject.roles: value of the configuredrolesClaim, normalized toVec<String>. Missing claim → empty vec (not an error — an authenticated user with no roles is valid).AuthIdentityis indistinguishable from one produced by the other providers, so all of ACL: new role-based schema, union evaluation, and read/write enforcement #55's enforcement applies uniformly.JWKS caching
jwksUrl. No on-disk persistence.kidtriggers a refresh, rate-limited to avoid DoS amplification (e.g., at most one refresh per N seconds per URL, implementation choice).Out of scope
BearerTokenAuthorForwardedUserAuth. This is an additional provider, not a replacement.Technical pointers
src/server_auth/providers.rs(or a new module undersrc/server_auth/if it gets large).AuthProvidertrait may need a small extension to allow async operations (JWKS fetching). If it is sync today, the cleanest path is to make the validation stepasyncand let the proxy already-async request handlerawaitit.mcp acl check(from ACL: mcp acl check CLI and enriched audit log entries #56) would be nice: feed it a synthetic JWT-backed identity and verify the ACL decision, but only if the provider test surface allows it cleanly.Success criteria
mcp serveat their realm with only the four required config fields and have tokens issued by that realm authenticate correctly.groups,roles, orrealm_access.roles) flow intoAuthIdentity.rolesand are honored by the ACL evaluator from ACL: new role-based schema, union evaluation, and read/write enforcement #55.kidtriggers a refresh and the token is accepted on the next try without restarting the proxy.See the full redesign plan at
docs/acl-redesign-plan.md.