diff --git a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs index ecef8e15e..2b0f4f5a5 100644 --- a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs +++ b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs @@ -718,12 +718,18 @@ private async Task PerformDynamicClientRegistrationAsync( { return protectedResourceMetadata.WwwAuthenticateScope; } - else if (protectedResourceMetadata.ScopesSupported.Count > 0) + + if (!string.IsNullOrEmpty(_configuredScopes)) + { + return _configuredScopes; + } + + if (protectedResourceMetadata.ScopesSupported.Count > 0) { return string.Join(" ", protectedResourceMetadata.ScopesSupported); } - return _configuredScopes; + return null; } /// diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthTests.cs index c4979fb10..e5e6e48de 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthTests.cs @@ -412,6 +412,42 @@ public async Task AuthorizationFlow_UsesScopeFromProtectedResourceMetadata() Assert.Equal("mcp:tools files:read", requestedScope); } + [Fact] + public async Task AuthorizationFlow_UsesConfiguredScopesBeforeProtectedResourceMetadata() + { + Builder.Services.Configure(McpAuthenticationDefaults.AuthenticationScheme, options => + { + options.ResourceMetadata!.ScopesSupported = ["mcp:tools", "files:read"]; + }); + + await using var app = await StartMcpServerAsync(); + + string? requestedScope = null; + + await using var transport = new HttpClientTransport(new() + { + Endpoint = new(McpServerUrl), + OAuth = new() + { + ClientId = "demo-client", + ClientSecret = "demo-secret", + RedirectUri = new Uri("http://localhost:1179/callback"), + Scopes = ["custom:read", "custom:write"], + AuthorizationRedirectDelegate = (uri, redirect, ct) => + { + var query = QueryHelpers.ParseQuery(uri.Query); + requestedScope = query["scope"].ToString(); + return HandleAuthorizationUrlAsync(uri, redirect, ct); + }, + }, + }, HttpClient, LoggerFactory); + + await using var client = await McpClient.CreateAsync( + transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken); + + Assert.Equal("custom:read custom:write", requestedScope); + } + [Fact] public async Task AuthorizationFlow_UsesScopeFromChallengeHeader() { @@ -853,7 +889,7 @@ public async Task ResourceMetadata_DoesNotAddTrailingSlash() { // This test verifies that automatically derived resource URIs don't have trailing slashes // and that the client doesn't add them during authentication - + // Don't explicitly set Resource - let it be derived from the request await using var app = await StartMcpServerAsync(); @@ -993,10 +1029,10 @@ public async Task ResourceMetadata_PreservesExplicitTrailingSlash() { // This test verifies that explicitly configured trailing slashes are preserved const string resourceWithTrailingSlash = "http://localhost:5000/"; - + // Configure ValidResources to accept the trailing slash version for this test TestOAuthServer.ValidResources = [resourceWithTrailingSlash, "http://localhost:5000/mcp"]; - + Builder.Services.Configure(McpAuthenticationDefaults.AuthenticationScheme, options => { options.ResourceMetadata = new ProtectedResourceMetadata