Skip to content

RFC 8707 Resource Indicators Implementation #638

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 41 commits into from
Jun 18, 2025
Merged

Conversation

ochafik
Copy link
Contributor

@ochafik ochafik commented Jun 16, 2025

Implements Resource Indicators validation for OAuth 2.0 (RFC 8707; spec change).

Fixes #592, Fixes #635

  • set the resource parameter in OAuth authorization and token exchange flows to bind tokens to the MCP server.
  • updated simpleStreamableHttp.ts to show how to perform strict verification (disabled by default, run with --oauth --oauth-strict to enable)
  • strict validation of resource returned by the PRM

Note: Inspector is being updated in pcarleton/auth-debugger-resource

Motivation and Context

Facilitate prevention of token theft/confusion attacks where a malicious MCP server steals tokens meant for other services by explicitly binding tokens to their intended resources and showing how a server can do strict checks of this binding.

This security vulnerability was identified in modelcontextprotocol/modelcontextprotocol#544.

How Has This Been Tested?

Test coverage has been added, and MCP inspector's auth flows (quick & guided) were tested manually (w/ pending modelcontextprotocol/inspector#526 changes applied)

Breaking Changes

While the change is breaking at a protocol level, it should not require code changes from SDK users (just SDK version bumping).

  • Client developers: No code changes required. The SDK automatically extracts and includes the resource parameter from the server URL
  • Server developers:
    • Resource validation is left to implementors (see demoInMemoryOAuthProvider.ts and simpleStreamableHttp.ts).
    • Note that before an MCP server can fail on missing or mismatching resource param, the Authorization Server (if external) needs to support / relay the resource param, and enough of the clients need to update their SDK to start setting the resource param in their auth requests. This is why simpleStreamableHttp.ts only validates the resource if --oauth-strict is set.
    • Each server should ideally treat each MCP server as a distinct resource in their PRM. If they host many servers under the same domain, the resource should be specific to a single server, not be domain-wide (otherwise any "bad apple" server might be able to benefit from resource confusion w/ its siblings). This may be facilitated in follow up changes to the SDK.
  • Auth providers: should be updated to accept and relay resource parameter.

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 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

Implementation Approach

Resource URIs are used as-is with only fragment removal. This allows having different MCP servers under different subpaths (even w/ different query URLs) w/o sharing spilling their resource authorization to each other (to allow a variety of MCP server federation use cases).

Key Components Added

  1. Shared utilities (auth-utils.ts): Resource URI handling and validation
  2. Client auth modifications: Resource parameter support in authorization/token flows
  3. Transport layers: Automatic resource extraction from server URLs
  4. Server handlers: Resource parameter acceptance and forwarding
  5. Demo provider
  6. Error handling: New InvalidTargetError for RFC 8707 compliance

References

ochafik and others added 5 commits June 16, 2025 19:29
…emo provider

- Clarify that core server handlers only pass through resource parameter
- Emphasize that server URL validation is demonstrated in the demo provider
- Update issue references to show #592 is fixed, #635 is related
- Update examples to show DemoOAuthProviderConfig usage

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
Copy link
Contributor

@pcarleton pcarleton left a comment

Choose a reason for hiding this comment

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

The plumbing looks good, but a few thoughts on the demo implementation:

The RFC 8707 is intending to make audience restricted tokens , so a key thing is that if we get a token with a different resource we reject it. So far, it looks like the demo is focused on preventing a token from ever being created with a bad resource, but doesn't exercise the token rejection flow that would happen during e.g. the bearerAuth.ts middleware.

So, e.g. if I got a token for resource=https://foo.example.com/mcp, and I used that on https://bar.example.com/mcp we should reject it.

I think it's useful to prevent invalid tokens from being created, but I think it's important we demonstrate that the AS needs to either store the resource with the token (and check it), or it needs to encode the resource data into the token itself (and check it).

* RFC 8707 section 2 states that resource URIs "MUST NOT include a fragment component".
* Keeps everything else unchanged (scheme, domain, port, path, query).
*/
export function resourceUrlFromServerUrl(url: string): string {
Copy link
Contributor

Choose a reason for hiding this comment

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

optional: setting url.hash = "" will remove the hash too, so you could keep it as a URL

https://developer.mozilla.org/en-US/docs/Web/API/URL/hash

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hah cool, thanks!

@@ -28,18 +43,28 @@ export class DemoInMemoryClientsStore implements OAuthRegisteredClientsStore {
* - Persistent token storage
* - Rate limiting
*/
interface ExtendedAuthInfo extends AuthInfo {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it makes sense to add this to the base AuthInfo

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done, thanks

if (resource) {
await this.validateResource(client, resource);
}
throw new Error('Refresh tokens not implemented for example demo');
}

async verifyAccessToken(token: string): Promise<AuthInfo> {
Copy link
Contributor

Choose a reason for hiding this comment

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

i think we want to change this method as well, and test it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Implementations that decode / parse the token probably would check the resource, but here we just lookup the tokenData we've validated in exchangeAuthorizationCode, and the resource expectations haven't changed since (same mcpServerUrl).

Copy link
Contributor Author

@ochafik ochafik left a comment

Choose a reason for hiding this comment

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

Simplified the code a bit :-)

@@ -28,18 +43,28 @@ export class DemoInMemoryClientsStore implements OAuthRegisteredClientsStore {
* - Persistent token storage
* - Rate limiting
*/
interface ExtendedAuthInfo extends AuthInfo {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done, thanks

if (resource) {
await this.validateResource(client, resource);
}
throw new Error('Refresh tokens not implemented for example demo');
}

async verifyAccessToken(token: string): Promise<AuthInfo> {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Implementations that decode / parse the token probably would check the resource, but here we just lookup the tokenData we've validated in exchangeAuthorizationCode, and the resource expectations haven't changed since (same mcpServerUrl).

* RFC 8707 section 2 states that resource URIs "MUST NOT include a fragment component".
* Keeps everything else unchanged (scheme, domain, port, path, query).
*/
export function resourceUrlFromServerUrl(url: string): string {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hah cool, thanks!

ochafik and others added 3 commits June 17, 2025 11:53
…eware

- Tests that when mcp-protocol-version header is missing, the middleware
  uses DEFAULT_NEGOTIATED_PROTOCOL_VERSION when calling verifyAccessToken
- Ensures proper fallback behavior for protocol version negotiation

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
@ochafik ochafik requested a review from pcarleton June 17, 2025 17:45
@ochafik ochafik force-pushed the ochafik/auth-resource branch from 7b073bf to c2150f0 Compare June 17, 2025 18:19
ochafik and others added 11 commits June 18, 2025 11:50
Fixed 5 instances of hardcoded "https://resource.example.com" in OAuth
protected resource metadata mocks to use the actual resourceBaseUrl.href.
This resolves test failures where the auth validation was rejecting
requests because the resource URL in the metadata didn't match the
actual test server URL.

The failing tests were:
- attempts auth flow on 401 during SSE connection
- attempts auth flow on 401 during POST request
- refreshes expired token during SSE connection
- refreshes expired token during POST request
- redirects to authorization if refresh token flow fails

All SSE tests now pass (17/17).

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
@ochafik ochafik marked this pull request as ready for review June 18, 2025 15:02
Copy link
Contributor

@ihrpr ihrpr left a comment

Choose a reason for hiding this comment

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

LGTM

@ihrpr ihrpr merged commit 90569d8 into main Jun 18, 2025
5 checks passed
@ihrpr ihrpr deleted the ochafik/auth-resource branch June 18, 2025 16:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants