92 lines
4.1 KiB
C#
92 lines
4.1 KiB
C#
using ModelContextProtocol.Authentication;
|
|
using ModelContextProtocol.Client;
|
|
|
|
namespace ModelContextProtocol.AspNetCore.Tests.OAuth;
|
|
|
|
// SEP-837: the SDK doesn't surface or retry DCR failures itself, but a consumer built on the SDK
|
|
// must be able to. These tests prove that surface: a rejected registration propagates with enough
|
|
// context to build a meaningful error, and a consumer can retry with an adjusted redirect URI.
|
|
public class DcrFailureTests : OAuthTestBase
|
|
{
|
|
public DcrFailureTests(ITestOutputHelper outputHelper)
|
|
: base(outputHelper)
|
|
{
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DcrRejection_PropagatesToConsumer_WithStatusBodyAndSentParameters()
|
|
{
|
|
await using var app = await StartMcpServerAsync();
|
|
|
|
// A custom-scheme redirect URI infers application_type "native"; the OIDC AS rejects it
|
|
// with 400 invalid_redirect_uri because it only registers http/https redirect URIs.
|
|
await using var transport = new HttpClientTransport(new()
|
|
{
|
|
Endpoint = new(McpServerUrl),
|
|
OAuth = new ClientOAuthOptions()
|
|
{
|
|
RedirectUri = new Uri("myapp://callback"),
|
|
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
|
DynamicClientRegistration = new() { ClientName = "Test MCP Client" },
|
|
},
|
|
}, HttpClient, LoggerFactory);
|
|
|
|
var ex = await Assert.ThrowsAsync<McpException>(() => McpClient.CreateAsync(
|
|
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken));
|
|
|
|
// The consumer needs enough to produce a meaningful error: the HTTP status, the AS error
|
|
// body (which echoes the redirect URI), and the application_type the SDK actually sent.
|
|
Assert.Contains("BadRequest", ex.Message);
|
|
Assert.Contains("invalid_redirect_uri", ex.Message);
|
|
Assert.Contains("native", ex.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ConsumerCanRetryRegistration_WithAdjustedRedirectUri_AfterRejection()
|
|
{
|
|
await using var app = await StartMcpServerAsync();
|
|
|
|
// First attempt: a custom-scheme redirect (native) is rejected by the AS. ApplicationType
|
|
// is held constant at "native" so only the redirect URI changes between the two attempts.
|
|
await using var firstTransport = new HttpClientTransport(new()
|
|
{
|
|
Endpoint = new(McpServerUrl),
|
|
OAuth = new ClientOAuthOptions()
|
|
{
|
|
RedirectUri = new Uri("myapp://callback"),
|
|
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
|
DynamicClientRegistration = new()
|
|
{
|
|
ClientName = "Test MCP Client",
|
|
ApplicationType = "native",
|
|
},
|
|
},
|
|
}, HttpClient, LoggerFactory);
|
|
|
|
await Assert.ThrowsAsync<McpException>(() => McpClient.CreateAsync(
|
|
firstTransport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken));
|
|
|
|
// Second attempt: a new provider on the SAME HttpClient with an adjusted (loopback) redirect
|
|
// URI that the AS accepts. The retry must succeed, proving the SEP-837 MAY-retry surface works
|
|
// and that the rejected attempt left no client state behind.
|
|
await using var secondTransport = new HttpClientTransport(new()
|
|
{
|
|
Endpoint = new(McpServerUrl),
|
|
OAuth = new ClientOAuthOptions()
|
|
{
|
|
RedirectUri = new Uri("http://localhost:1179/callback"),
|
|
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
|
DynamicClientRegistration = new()
|
|
{
|
|
ClientName = "Test MCP Client",
|
|
ApplicationType = "native",
|
|
},
|
|
},
|
|
}, HttpClient, LoggerFactory);
|
|
|
|
await using var client = await McpClient.CreateAsync(
|
|
secondTransport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
|
|
|
|
Assert.Equal("native", TestOAuthServer.LastApplicationType);
|
|
}
|
|
}
|