AuthKit: auto-bind token audience to resource URL (RFC 8707)#3905
Merged
AuthKit: auto-bind token audience to resource URL (RFC 8707)#3905
Conversation
WorkOS AuthKit now supports Resource Indicators (RFC 8707): configure your MCP server's resource URL in the WorkOS Dashboard and AuthKit mints tokens with a matching aud claim. This replaces the previous client_id approximation that could never actually bind tokens to a specific server. AuthKitProvider now auto-wires JWTVerifier.audience in set_mcp_path to equal the resource URL it advertises in protected resource metadata. Users pass the same (existing) base_url they always did, copy the URL the server logs on startup into the Dashboard's 'MCP resource indicators' list, and validation happens automatically. No new public parameters; client_id is removed. Other changes: - Move the RemoteAuthProvider lifecycle hook (set_mcp_path call) into the base get_routes so provider subclasses can react to the path becoming known. Previously only OAuthAuthorizationServerProvider invoked it. - Bump JWT rejection reason logs (issuer/audience/scopes) from DEBUG to WARNING. The prior 'Bearer token rejected' line was uninformative, which cost a debugging round-trip during development. - Rename examples/auth/authkit_dcr -> examples/auth/authkit (DCR-vs-CIMD is a Dashboard choice, not an example-level distinction) and rewrite the README around the Dashboard setup. - Normalize all auth examples to 127.0.0.1 for host consistency between server base_url and client SERVER_URL; prevents audience mismatches for any provider that gains RFC 8707 support later.
- Fold the expiry-rejection log into a single INFO line with an explicit comment noting why it intentionally stays quieter than the issuer/ audience/scope WARNINGs (token rotation is expected-path noise, not a configuration error worth surfacing by default). - Move JWTVerifier import to module level in test_workos.py per the project's 'all imports at top' rule. - Add test for set_mcp_path(None): documents the MCP-at-root contract where audience binds to base_url itself. - Tighten AuthKitProvider docstring: 'at startup' -> 'when the MCP mount path becomes known (typically at http_app() construction)'.
strawgate
pushed a commit
that referenced
this pull request
Apr 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
WorkOS AuthKit now supports RFC 8707 Resource Indicators — developers configure allowed MCP resource URLs in the Dashboard and AuthKit mints tokens with a matching
audclaim. That replaces the oldclient_id-as-audience approximation, which was a weak stand-in that never actually bound tokens to a specific server. The scary warning about token replay risk in the AuthKit docs is no longer true; this PR removes it and makes FastMCP's side of the integration match.The ergonomic shape matters. Users already pass
base_url, and FastMCP already computes the server's canonical resource URL from it (base_url + mcp_path). That same URL is what AuthKit needs in the Dashboard, what gets advertised in.well-known/oauth-protected-resource, and what should be validated asaud. So there's no new parameter —AuthKitProviderauto-bindsJWTVerifier.audienceinset_mcp_pathusing the URL it already knows. On startup the server logs the exact string to paste into the Dashboard's "MCP resource indicators" list. One manual action; compliance follows.Two supporting changes were needed to make this clean.
RemoteAuthProvider.get_routesdidn't previously invokeset_mcp_path, which meant subclass lifecycle hooks never fired outside the full OAuth-authorization-server code path — fixed by moving that call into the base. And the JWT verifier's rejection-reason logs (issuer/audience/scopes mismatch) were at DEBUG level, so the generic "Bearer token rejected" was the only signal a user normally saw; those are now WARNING, which would have immediately surfaced the audience mismatch we hit while testing this against a real AuthKit domain.Also folded in some housekeeping while touching the auth examples: renamed
examples/auth/authkit_dcrtoexamples/auth/authkit(DCR vs CIMD is a Dashboard choice, not an example-level distinction), rewrote the README around the Dashboard setup, and normalized every auth example to use127.0.0.1consistently across server/client/README so future RFC 8707 providers don't hit thelocalhost/127.0.0.1audience mismatch that bit us during testing.Breaking:
client_idis removed fromAuthKitProvider. Callers relying on it should either (a) configure Resource Indicators in the Dashboard and let the provider auto-bind the audience, or (b) pass an explicittoken_verifierwith their chosen audience — the provider respects user-supplied verifiers and doesn't touch them.