Add AuthSettings.validate_token_resource to check a bearer token's resource - #3447
Merged
Conversation
…source BearerAuthBackend takes an optional resource_server_url; when it is set, only a token the verifier reports as issued for that URL (AccessToken.resource, the RFC 8707 resource indicator, compared as a URL with a trailing slash tolerated) is accepted, and anything else is answered 401 like an unrecognized token. AuthSettings.validate_token_resource (default False) turns this on for the Streamable HTTP and SSE apps; it requires resource_server_url. TokenVerifier.verify_token's docstring and docs/run/authorization.md say where the token's audience goes and when to enable the option versus checking the audience in the verifier. The simple-auth example enables it. The interaction suite enables it for the bearer tests and records that it is off by default on hosting:auth:aud-validation.
maxisbey
marked this pull request as ready for review
September 4, 2026 23:20
Contributor
📚 Documentation preview
|
There was a problem hiding this comment.
1 issue found across 12 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="docs/run/authorization.md">
<violation number="1" location="docs/run/authorization.md:26">
P2: When introspection returns an array-valued `aud`, the bundled verifier forwards the array as `AccessToken.resource`, so Pydantic rejects the token before resource validation. Update the example verifier to select the URL-matching audience and reject the token when none matches.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
RefreshToken gains an optional resource (RFC 8707 resource indicator) that a provider propagates to refreshed access tokens, as it does subject, so a server with validate_token_resource keeps accepting tokens after a refresh. The interaction suite's provider does so and a lifecycle test covers it. Also: log the reported resource with %r; note in the docs that resource_server_url should be the exact endpoint URL when the option is on; pin the default-off behaviour with an interaction test; have the simple-auth verifier pick the aud entry for this server when aud is a list.
There was a problem hiding this comment.
All reported issues were addressed across 8 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
maxisbey
commented
Sep 5, 2026
There was a problem hiding this comment.
All reported issues were addressed across 2 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
validate_token_resource becomes bool | None (default None). With a resource_server_url configured and no explicit choice, AuthSettings emits an MCPDeprecationWarning and behaves as False, so existing deployments keep working but are asked to decide; 3.0 makes True the default. An explicit False (the verifier checks the audience itself) is silent. The docs tutorials, the bearer_auth and oauth_client_credentials stories, and the oauth_server snippet now set it to True and issue tokens bound to their resource URL; docs/deprecated.md lists the new warning.
There was a problem hiding this comment.
All reported issues were addressed across 14 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
…nippet URL The 3.0 default applies when resource_server_url is set (the field description, warning text, docs and deprecated.md now say so). The oauth_server snippet's resource_server_url matches the address mcp.run() serves on.
11 tasks
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.
Adds an opt-in
AuthSettings.validate_token_resource. When set, the bearer gate only accepts a token that theTokenVerifierreports as issued forresource_server_url(viaAccessToken.resource); a token reporting another resource, or none, gets the same 401 as an unrecognized token. Leaving it unset on a resource server emits anMCPDeprecationWarningand behaves as off; 3.0 makes it the default there.Motivation and Context
The resource-server gate checks scopes and expiry but has never compared the token's RFC 8707 resource indicator with the server's own identifier; the interaction suite records this as the
hosting:auth:aud-validationdivergence. In practice that comparison was left to eachTokenVerifier, without the docs or the protocol docstring saying so.This does two things:
TokenVerifier.verify_tokenanddocs/run/authorization.mdnow say to put the token's audience inAccessToken.resource, and that otherwise confirming the token was issued for this server is the verifier's job.resourcethe client requested (which is what MCP clients send). It isn't enforced by default becauseAccessToken.resourcegets populated in lots of ways today (IdP-specific audience identifiers, for instance), and doing so unconditionally would reject valid tokens in those setups; instead an unset value warns so each deployment choosesTrueorFalseonce.RefreshTokengains an optionalresourceso providers can carry the binding through the refresh grant.The comparison is URL equality with a trailing slash tolerated; both sides go through
AnyHttpUrlso host case and an explicit default port don't matter. It lives inBearerAuthBackend(new keyword-onlyresource_server_url) so a refused token never reachesget_access_token()or custom routes, and a refusal logs both values at warning level.How Has This Been Tested?
BearerAuthBackendwith and withoutresource_server_url(match, trailing slash, case/port spelling, parent/child/sibling path, other origin, non-URL value, no resource), plus theAuthSettingsvalidation../scripts/test, ruff, pyright.Breaking Changes
None. Unset behaves as off (with the deprecation warning);
BearerAuthBackend(verifier)and existing verifiers behave as before. Setting it toTruewithout aresource_server_urlraises at construction.Types of changes
Checklist
help wanted, or I'm a maintainer)Additional context
The docs tutorials, the
bearer_auth/oauth_client_credentialsstories, theoauth_serversnippet and thesimple-authexample set the option explicitly and issue tokens bound to their resource URL;docs/deprecated.mdlists the warning.AI Disclaimer