diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthTests.cs index d400783..265090e 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthTests.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.DependencyInjection; +using ModelContextProtocol; using ModelContextProtocol.AspNetCore.Authentication; using ModelContextProtocol.Authentication; using ModelContextProtocol.Client; @@ -528,6 +529,47 @@ public class AuthTests : OAuthTestBase 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 d8923b7..94cc3a4 100644 --- a/tests/ModelContextProtocol.TestOAuthServer/Program.cs +++ b/tests/ModelContextProtocol.TestOAuthServer/Program.cs @@ -67,6 +67,14 @@ public sealed class Program /// 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(); @@ -290,7 +298,7 @@ public sealed class Program } // 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}"); } @@ -338,7 +346,7 @@ public sealed class Program // 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 {