Skip to content

Fix: Relax resource URI validation to accept base URL#1517

Open
JBallan wants to merge 9 commits into
modelcontextprotocol:mainfrom
JBallan:fix/1119-oauth-resource-uri-validation-flexibility
Open

Fix: Relax resource URI validation to accept base URL#1517
JBallan wants to merge 9 commits into
modelcontextprotocol:mainfrom
JBallan:fix/1119-oauth-resource-uri-validation-flexibility

Conversation

@JBallan
Copy link
Copy Markdown

@JBallan JBallan commented Apr 15, 2026

Fix #1119

Summary

Per MCP specification, OAuth authorization operates at the base URL level (path discarded). The SDK now accepts OAuth metadata when the resource field contains either:

  • The full MCP endpoint URI (e.g., https://server.example.com/mcp/tools)
  • The base URL only (e.g., https://server.example.com)

This aligns with RFC 9728 § 3.1 which states that protected resource metadata can be scoped at the authority level.

Motivation and Context

The SDK's OAuth client was throwing a validation error when the resource field in OAuth protected resource metadata returned the base URL instead of the full MCP endpoint path. This prevented legitimate OAuth flows where servers declare protected resources at the authority level, which is:

  • ✅ Spec-compliant per MCP OAuth specification
  • ✅ Valid per RFC 9728 § 3.1 (authorization server metadata)
  • ✅ Common practice for multi-endpoint OAuth servers

Example scenario that now works:

↑ Now validates successfully

Implementation Details

Updated OAuthClientHandler.TryGetResourceMetadataAsync:

  • Compares base URLs (scheme + authority) when resource URI has no path
  • Falls back to full URI comparison for backward compatibility
  • Maintains security by rejecting mismatched authorities

How Has This Been Tested?

Test Changes

Renamed test:

  • CannotAuthenticate_WhenWwwAuthenticateResourceMetadataIsRootPathCanAuthenticate_WhenWwwAuthenticateResourceMetadataIsRootPath
    • Purpose: Verifies OAuth succeeds when resource metadata URI is root-level while MCP endpoint has a subpath
    • Validates: Flexible URI matching behavior

New test:

  • CannotAuthenticate_WhenResourceMetadataUriDoesNotMatch
    • Purpose: Ensures authentication fails when resource URI points to a completely different server
    • Validates: Security - prevents cross-server token misuse

Breaking Changes

No breaking changes. This is a behavioral enhancement that makes OAuth resource URI validation more flexible while maintaining backward compatibility.

Behavior Change Details

Previous behavior:

  • OAuth resource URI validation required strict matching between the protected resource metadata URI and the MCP endpoint URI.

New behavior:

  • OAuth resource URI validation now follows a more flexible matching strategy that aligns with the MCP specification:
  • A resource URI without a path (e.g., https://server.example.com) will successfully match endpoints with paths (e.g., https://server.example.com/mcp/tools)
  • This allows servers to declare protected resources at a broader scope while still protecting specific endpoints

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

Jérémy Ballan added 2 commits April 15, 2026 10:27
Per MCP spec, authorization operates at base URL level (path discarded).
The SDK now accepts OAuth metadata when the 'resource' field contains
either the full MCP endpoint URI or the base URL (authority only).
Copy link
Copy Markdown
Author

@JBallan JBallan left a comment

Choose a reason for hiding this comment

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

Hi! The workflows are currently awaiting approval, so CodeQL checks can't run yet and the PR is blocked. Could a maintainer approve the workflows and review the PR when possible? Thanks!

Copy link
Copy Markdown
Author

@JBallan JBallan left a comment

Choose a reason for hiding this comment

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

Could a maintainer review the PR when possible? Thanks!

@matt-gribben
Copy link
Copy Markdown

Hope somebody reviews this soon... the current strict evaluation of URI's breaks the SDK Client for a lot of popular MCP services (Linear for example)

@scrodde
Copy link
Copy Markdown

scrodde commented May 29, 2026

This is a real issue could a reviewer have a look? @tarekgh @halter73 @mikekistler @jeffhandley

@tarekgh
Copy link
Copy Markdown
Contributor

tarekgh commented May 29, 2026

I'll take a look today.

@tarekgh
Copy link
Copy Markdown
Contributor

tarekgh commented May 29, 2026

I suggest if we can edit the section https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#canonical-server-uri (in another PR) relaxing the statement aligns with the resource parameter in RFC 9728 to something like effectively extending the RFC 9728's resource parameter validation model

/// </summary>
[Fact]
public async Task CannotAuthenticate_WhenWwwAuthenticateResourceMetadataIsRootPath()
public async Task CanAuthenticate_WhenWwwAuthenticateResourceMetadataIsRootPath()
Copy link
Copy Markdown
Contributor

@tarekgh tarekgh May 29, 2026

Choose a reason for hiding this comment

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

nit: The comment in CannotAuthenticate_WhenResourceMetadataResourceIsNonRootParentPath at line 771 still references the old test name and makes the opposite claim:

// CannotAuthenticate_WhenWwwAuthenticateResourceMetadataIsRootPath validates we won't fall back to root in this case.

Since this test now validates that root-level resource is accepted, that cross-reference comment should be updated to reflect the new name and the new behavior.

string normalizedResourceLocation = NormalizeUri(resourceLocation);

return string.Equals(normalizedMetadataResource, normalizedResourceLocation, StringComparison.OrdinalIgnoreCase);
// Accept exact match with the full MCP endpoint URI
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.

The XML doc comment on VerifyResourceMatch (lines 776-785 above the diff) still says:

  • Verifies that the resource URI in the metadata exactly matches the original request URL as required by the RFC.
  • Per RFC: The resource value must be identical to the URL that the client used to make the request to the resource server.
  • <returns>True if the resource URI exactly matches the original request URL, otherwise false.</returns>

Since the method now also accepts authority-level (base URL) matches, the doc comment should be updated to reflect the new matching behavior.

@tarekgh
Copy link
Copy Markdown
Contributor

tarekgh commented May 29, 2026

nit (minor): The comment at ClientOAuthProvider.cs line 926 (in the PR branch) at the VerifyResourceMatch call site still says:

// Per RFC: The resource value must be identical to the URL that the client used to make the request to the resource server

Since VerifyResourceMatch now also accepts authority-level matches, this comment could be misleading to a future reader. Consider softening it to reflect the relaxed matching, e.g.:

// Per RFC 9728, validate that the resource URI in metadata corresponds to the server we're connecting to.
// VerifyResourceMatch accepts both exact URI match and authority-level (base URL) match per MCP spec.

return true;
}

// Per MCP spec: "The authorization base URL MUST be derived by discarding the path component from the MCP server URL"
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.

The quoted MCP spec sentence ("The authorization base URL MUST be derived by discarding the path component from the MCP server URL") is about how to derive the authorization server's well-known URL for discovery, not about resource metadata validation.

The more relevant justification is the Canonical Server URI section, which explicitly lists both https://mcp.example.com/mcp and https://mcp.example.com as valid canonical URIs for resource identification. Consider updating the comment to reference that section instead.

/// use OAuth tokens intended for one server to access a different server.
/// </summary>
[Fact]
public async Task CannotAuthenticate_WhenResourceMetadataUriDoesNotMatch()
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.

suggestion (minor): Consider adding a test for same-authority but different-path resource mismatch, e.g. resource=https://example.com/service-a vs endpoint https://example.com/service-b. The existing tests cover different authorities and parent-path mismatches, but this scenario (unrelated path on the same host) would strengthen confidence that the authority-level fallback only accepts authority-only resources and not arbitrary paths on the same host.

Copy link
Copy Markdown
Contributor

@tarekgh tarekgh left a comment

Choose a reason for hiding this comment

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

Added minor comment, LGTM otherwise.

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.

OAuth Resource URI Validation Too Strict - Fails When MCP Server Uses Subpath

4 participants