Skip to content

Add AuthSettings.validate_token_resource to check a bearer token's resource - #3447

Merged
maxisbey merged 5 commits into
mainfrom
bearer-resource-check
Sep 5, 2026
Merged

Add AuthSettings.validate_token_resource to check a bearer token's resource#3447
maxisbey merged 5 commits into
mainfrom
bearer-resource-check

Conversation

@maxisbey

@maxisbey maxisbey commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Adds an opt-in AuthSettings.validate_token_resource. When set, the bearer gate only accepts a token that the TokenVerifier reports as issued for resource_server_url (via AccessToken.resource); a token reporting another resource, or none, gets the same 401 as an unrecognized token. Leaving it unset on a resource server emits an MCPDeprecationWarning and 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-validation divergence. In practice that comparison was left to each TokenVerifier, without the docs or the protocol docstring saying so.

This does two things:

  • Documents the contract. TokenVerifier.verify_token and docs/run/authorization.md now say to put the token's audience in AccessToken.resource, and that otherwise confirming the token was issued for this server is the verifier's job.
  • Adds the option for deployments whose authorization server binds tokens to the resource the client requested (which is what MCP clients send). It isn't enforced by default because AccessToken.resource gets 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 chooses True or False once. RefreshToken gains an optional resource so providers can carry the binding through the refresh grant.

The comparison is URL equality with a trailing slash tolerated; both sides go through AnyHttpUrl so host case and an explicit default port don't matter. It lives in BearerAuthBackend (new keyword-only resource_server_url) so a refused token never reaches get_access_token() or custom routes, and a refusal logs both values at warning level.

How Has This Been Tested?

  • Unit table for BearerAuthBackend with and without resource_server_url (match, trailing slash, case/port spelling, parent/child/sibling path, other origin, non-URL value, no resource), plus the AuthSettings validation.
  • Interaction suite: the bearer tests enable the option (other-resource and no-resource tokens are answered 401, a token for this resource is served); the requirement keeps a note that it's off by default.
  • End to end under uvicorn with an introspection-backed verifier over Streamable HTTP: option on, a token for another resource gets 401 and one for this server 200; option off, behaviour unchanged.
  • ./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 to True without a resource_server_url raises at construction.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I am assigned to the linked issue (or it is labeled help wanted, or I'm a maintainer)
  • I have disclosed any AI assistance and can explain the change in my own words
  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

The docs tutorials, the bearer_auth / oauth_client_credentials stories, the oauth_server snippet and the simple-auth example set the option explicitly and issue tokens bound to their resource URL; docs/deprecated.md lists the warning.

AI Disclaimer

…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
maxisbey marked this pull request as ready for review September 4, 2026 23:20
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

📚 Documentation preview

Preview https://pr-3447.mcp-python-docs.pages.dev
Deployment https://870c6f06.mcp-python-docs.pages.dev
Commit 5e8ba84
Triggered by @maxisbey
Updated 2026-09-05 02:23:14 UTC

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread docs/run/authorization.md Outdated
Comment thread src/mcp/server/auth/middleware/bearer_auth.py Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Comment thread src/mcp/server/auth/middleware/bearer_auth.py
Comment thread src/mcp/server/auth/middleware/bearer_auth.py
Comment thread tests/interaction/_requirements.py
Comment thread examples/servers/simple-auth/mcp_simple_auth/server.py
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.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Comment thread examples/servers/simple-auth/mcp_simple_auth/token_verifier.py

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 8 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread examples/servers/simple-auth/mcp_simple_auth/token_verifier.py

@maxisbey maxisbey left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

t

Comment thread tests/interaction/auth/test_bearer.py Outdated
Comment thread docs/run/authorization.md Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread docs/run/authorization.md
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.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Comment thread src/mcp/server/auth/settings.py Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread examples/snippets/servers/oauth_server.py Outdated
…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.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

@maxisbey
maxisbey merged commit 0c91368 into main Sep 5, 2026
43 checks passed
@maxisbey
maxisbey deleted the bearer-resource-check branch September 5, 2026 02:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant