diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthTests.cs index be831d523..8a320e622 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthTests.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.DependencyInjection; +using ModelContextProtocol; using ModelContextProtocol.AspNetCore.Authentication; using ModelContextProtocol.Authentication; using ModelContextProtocol.Client; @@ -527,6 +528,47 @@ await Assert.ThrowsAsync(() => McpClient.CreateAsync( transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken)); } + [Fact] + public async Task CannotAuthenticate_WhenProtectedResourceMetadataMissingResource() + { + TestOAuthServer.RequireResource = false; + + Builder.Services.Configure(McpAuthenticationDefaults.AuthenticationScheme, options => + { + options.Events.OnResourceMetadataRequest = async context => + { + context.HandleResponse(); + + var metadata = new ProtectedResourceMetadata + { + AuthorizationServers = { new Uri(OAuthServerUrl) }, + ScopesSupported = ["mcp:tools"], + }; + + await Results.Json(metadata, McpJsonUtilities.DefaultOptions).ExecuteAsync(context.HttpContext); + }; + }); + + await using var app = await StartMcpServerAsync(); + + 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"), + AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + }, + }, HttpClient, LoggerFactory); + + var ex = await Assert.ThrowsAsync(() => McpClient.CreateAsync( + transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken)); + + Assert.Contains("Resource URI in metadata", ex.Message); + } + [Fact] public async Task CanAuthenticate_WithAuthorizationServerPathInsertionMetadata() { diff --git a/tests/ModelContextProtocol.TestOAuthServer/Program.cs b/tests/ModelContextProtocol.TestOAuthServer/Program.cs index 364836311..446e46dc9 100644 --- a/tests/ModelContextProtocol.TestOAuthServer/Program.cs +++ b/tests/ModelContextProtocol.TestOAuthServer/Program.cs @@ -66,6 +66,14 @@ public Program(ILoggerProvider? loggerProvider = null, IConnectionListenerFactor /// public bool ClientIdMetadataDocumentSupported { get; set; } = true; + /// + /// Gets or sets a value indicating whether the authorization server requires a resource parameter. + /// + /// + /// The default value is true. + /// + public bool RequireResource { get; set; } = true; + public HashSet DisabledMetadataPaths { get; } = new(StringComparer.OrdinalIgnoreCase); public IReadOnlyCollection MetadataRequests => _metadataRequests.ToArray(); @@ -289,7 +297,7 @@ IResult HandleMetadataRequest(HttpContext context, string? issuerPath = null) } // Validate resource in accordance with RFC 8707 - if (string.IsNullOrEmpty(resource) || !ValidResources.Contains(resource)) + if (RequireResource && (string.IsNullOrEmpty(resource) || !ValidResources.Contains(resource))) { return Results.Redirect($"{redirect_uri}?error=invalid_target&error_description=The+specified+resource+is+not+valid&state={state}"); } @@ -337,7 +345,7 @@ IResult HandleMetadataRequest(HttpContext context, string? issuerPath = null) // Validate resource in accordance with RFC 8707 var resource = form["resource"].ToString(); - if (string.IsNullOrEmpty(resource) || !ValidResources.Contains(resource)) + if (RequireResource && (string.IsNullOrEmpty(resource) || !ValidResources.Contains(resource))) { return Results.BadRequest(new OAuthErrorResponse {