Skip to content

Change ProtectedResourceMetadata URI properties to strings and build resource strings directly#1264

Merged
stephentoub merged 11 commits intomainfrom
copilot/change-resource-to-string
Feb 13, 2026
Merged

Change ProtectedResourceMetadata URI properties to strings and build resource strings directly#1264
stephentoub merged 11 commits intomainfrom
copilot/change-resource-to-string

Conversation

Copy link
Contributor

Copilot AI commented Feb 12, 2026

Fix ProtectedResourceMetadata.Resource URI Normalization

This PR fixes an issue where using Uri type for the ProtectedResourceMetadata.Resource property and related URI properties caused unwanted normalization during JSON serialization/deserialization, which added trailing slashes to URIs like http://localhost:5000.

Changes

Protocol types

  • Changed URI properties in ProtectedResourceMetadata from Uri?/List<Uri> to string?/List<string>
    • Resource, AuthorizationServers
  • Added [StringSyntax(StringSyntaxAttribute.Uri)] attributes to URI string properties for better IDE support and validation
    • Resource, JwksUri, ResourceDocumentation, ResourcePolicyUri, ResourceTosUri
  • Changed Clone() method to take string? instead of Uri? to avoid intermediate Uri creation and normalization

OAuth provider

  • Modified ClientOAuthProvider.GetRequiredResourceUri() to return string
  • Added NormalizeUri(string) overload with Uri.TryCreate validation
  • Convert authorization server strings to Uri objects with error handling before passing to selector

MCP spec compliance

  • Automatically derived resource URIs now trim trailing slashes per MCP authorization spec recommendation
  • Updated McpAuthenticationHandler.HandleDefaultResourceMetadataRequestAsync() to build resource string directly using string interpolation, avoiding intermediate Uri objects
  • Added IsDefaultPort() helper with case-insensitive scheme comparison to properly format URIs with/without port numbers
  • Trim trailing slashes from path when building derived resource string
  • Per MCP spec: implementations SHOULD consistently use URIs without trailing slashes unless semantically significant

Tests

  • Updated ResourceMetadata_DoesNotAddTrailingSlash test to:
    • Not explicitly set resource (let it be auto-derived)
    • Manually verify PRM document doesn't contain trailing slash via HTTP GET
    • Actually authenticate with the client to ensure end-to-end flow works
  • Updated ResourceMetadata_PreservesExplicitTrailingSlash test to:
    • Manually verify PRM document contains the explicit trailing slash via HTTP GET
    • Actually authenticate to verify explicit trailing slashes work
    • Use instance ValidResources property (no try/finally needed)
  • Updated TestOAuthServer.ValidResources to only accept URIs without trailing slashes by default
  • Changed ValidResources from static to instance property

Example

// Before: Uri constructor adds trailing slash
var metadata = new ProtectedResourceMetadata 
{ 
    Resource = new Uri("http://localhost:5000")  // Becomes "http://localhost:5000/"
};

// After: String preserves exact value, built directly without intermediate Uri
[StringSyntax(StringSyntaxAttribute.Uri)]
public string? Resource { get; set; } = "http://localhost:5000";  // Stays "http://localhost:5000"

// Clone now takes string to avoid normalization
var cloned = metadata.Clone("http://localhost:5000");  // No intermediate Uri, no normalization

// Resource strings built directly in handler
string derivedResource = $"{scheme}://{host}:{port}{path.TrimEnd('/')}";  // No UriBuilder

The [StringSyntax] attribute provides IDE intellisense and validation for URI strings while avoiding the normalization issues that come with the System.Uri type. Building resource strings directly without intermediate Uri objects ensures no unwanted normalization occurs.

Fixes #1122


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 4 commits February 12, 2026 17:50
…ormalization

Co-authored-by: halter73 <54385+halter73@users.noreply.github.com>
…trailing slashes

Co-authored-by: halter73 <54385+halter73@users.noreply.github.com>
… assertion

Co-authored-by: halter73 <54385+halter73@users.noreply.github.com>
Co-authored-by: halter73 <54385+halter73@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix ProtectedResourceMetadata.Resource type to string Change ProtectedResourceMetadata URI properties to strings to prevent normalization Feb 12, 2026
Copilot AI requested a review from halter73 February 12, 2026 18:05
@halter73 halter73 marked this pull request as ready for review February 12, 2026 18:10
Copy link
Contributor

@halter73 halter73 left a comment

Choose a reason for hiding this comment

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

I think using Uri in ProtectedResourceMetadata was a mistake since it does unnecessary normalization during deserialization that prevents us from seeing the original value we are supposed to round trip.

This should fix #1122.

stephentoub
stephentoub previously approved these changes Feb 12, 2026
@halter73
Copy link
Contributor

@copilot Can we just remove the properties from ProtectedResourceMetadata that we don't use/read like ResourcePolicyUri and any others? Please remove any unused non-URI properties too. We're not trying to create a generic OAuth library. We only need the properties necessary to complete client OAuth authentication. We can always add the properties back later if we need them without breaking API changes.

Per feedback, removed properties that are not used for client OAuth authentication:
- BearerMethodsSupported
- JwksUri
- ResourceSigningAlgValuesSupported
- ResourceName
- ResourceDocumentation
- ResourcePolicyUri
- ResourceTosUri
- TlsClientCertificateBoundAccessTokens
- AuthorizationDetailsTypesSupported
- DpopSigningAlgValuesSupported
- DpopBoundAccessTokensRequired

Kept only the properties necessary for OAuth client authentication:
- Resource
- AuthorizationServers
- ScopesSupported
- WwwAuthenticateScope (internal)

Co-authored-by: halter73 <54385+halter73@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 12, 2026

@copilot Can we just remove the properties from ProtectedResourceMetadata that we don't use/read like ResourcePolicyUri and any others? Please remove any unused non-URI properties too. We're not t...

Done. Removed all unused properties from ProtectedResourceMetadata. Only kept the properties necessary for OAuth client authentication: Resource, AuthorizationServers, ScopesSupported, and the internal WwwAuthenticateScope. (f18c391)

Copilot AI changed the title Change ProtectedResourceMetadata URI properties to strings to prevent normalization Change ProtectedResourceMetadata URI properties to strings and remove unused properties Feb 12, 2026
Copilot AI requested a review from halter73 February 12, 2026 18:29
@halter73
Copy link
Contributor

@stephentoub Can you reapprove this? It's exactly as you saw it last.

I reverted the property removals I asked copilot to make, because I noticed/remembered that we use the same DTO to produce the PRM on the server, so it is nice to have all those properties like ResourceDocumentation there. That's why we included them in the first place of course.

@halter73 halter73 changed the title Change ProtectedResourceMetadata URI properties to strings and remove unused properties Change ProtectedResourceMetadata URI properties to strings Feb 12, 2026
@FICTURE7
Copy link

FICTURE7 commented Feb 12, 2026

This should fix #1122.

An alternative would be to use Uri.OriginalString when passing the resource indicator in the token/authorize requests. Unless there is some edge cases am unaware of.

@halter73
Copy link
Contributor

halter73 commented Feb 12, 2026

Thanks @FICTURE7. I considered that after seeing it in your HackPrmJsonType() method in #1122 which I'm sure @eiriktsarpalis will appreciate. I think using string is the right move though. We do the same thing for other JSON-serialized URIs like ReadResourceRequestParams.Uri where we pass the string along to application code. The main motivation is to avoid breaking scenarios that could work with other SDKs because we're being too strict.

[JsonPropertyName("uri")]
[StringSyntax(StringSyntaxAttribute.Uri)]
public required string Uri { get; set; }

Since we do validate that the ProtectedResrouceMetadata.Resource matches the Uri we pass to the HttpClient to connect to the MCP server, I can see the argument that this could stay a Uri, but I still think it's more consistent to use strings for URIs for all of our public JSON-serializable types.

That does make me notice we used [StringSyntax(StringSyntaxAttribute.Uri)] for those though. @copilot can you add the [StringSyntax(StringSyntaxAttribute.Uri)] where appropriate.

Added [StringSyntax(StringSyntaxAttribute.Uri)] to all URI string properties in ProtectedResourceMetadata:
- Resource
- JwksUri
- ResourceDocumentation
- ResourcePolicyUri
- ResourceTosUri

This provides better IDE support and validation for URI strings, consistent with other protocol types like ReadResourceRequestParams.

Co-authored-by: halter73 <54385+halter73@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 12, 2026

Thanks @FICTURE7. I considered that after seeing it in your HackPrmJsonType() method in #1122 which I'm sure @eiriktsarpalis will appreciate. I think using string is the right move though. We do the...

Done. Added [StringSyntax(StringSyntaxAttribute.Uri)] to all URI string properties in ProtectedResourceMetadata: Resource, JwksUri, ResourceDocumentation, ResourcePolicyUri, and ResourceTosUri. Note that AuthorizationServers is a List<string> so the attribute can't be applied directly to the list property (it would apply to the list itself, not the string elements). (45bdb39)

Changes per MCP spec recommendation:
- Remove trailing slash variants from TestOAuthServer.ValidResources (kept only no-slash versions)
- Make ValidResources public static to allow test customization
- Update McpAuthenticationHandler to trim trailing slashes when deriving resource URIs
- Update ProtectedResourceMetadata.Clone to trim trailing slashes from derived URIs
- Improve ResourceMetadata_DoesNotAddTrailingSlash test to actually authenticate (not just fetch metadata)
- Add ResourceMetadata_PreservesExplicitTrailingSlash test to verify explicitly configured slashes work

Per MCP spec: implementations SHOULD use URIs without trailing slashes unless semantically significant.

Co-authored-by: halter73 <54385+halter73@users.noreply.github.com>
Copilot AI changed the title Change ProtectedResourceMetadata URI properties to strings Change ProtectedResourceMetadata URI properties to strings and trim trailing slashes Feb 13, 2026
Copilot AI requested a review from halter73 February 13, 2026 00:50
Per feedback:
- Changed ProtectedResourceMetadata.Clone() to take string? instead of Uri? parameter
- Changed ValidResources from static to instance property (no need for try/finally)
- Updated HandleDefaultResourceMetadataRequestAsync to build resource string directly without intermediate Uri
- Trim trailing slashes from path when building derived resource string
- Added manual HTTP metadata checks to both tests to verify PRM document contents
- Removed try/finally from ResourceMetadata_PreservesExplicitTrailingSlash (not needed with instance property)

Co-authored-by: halter73 <54385+halter73@users.noreply.github.com>
Copilot AI changed the title Change ProtectedResourceMetadata URI properties to strings and trim trailing slashes Change ProtectedResourceMetadata URI properties to strings and build resource strings directly Feb 13, 2026
Copilot AI requested a review from halter73 February 13, 2026 01:40
Use StringComparison.OrdinalIgnoreCase for scheme comparisons to handle HTTP/HTTPS in any case.

Co-authored-by: halter73 <54385+halter73@users.noreply.github.com>
@stephentoub stephentoub enabled auto-merge (squash) February 13, 2026 03:26
@stephentoub stephentoub merged commit dbf91bc into main Feb 13, 2026
10 checks passed
@stephentoub stephentoub deleted the copilot/change-resource-to-string branch February 13, 2026 03:28
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.

MCP client OAuth resource parameter normalization

4 participants