Enable additional client conformance scenarios (#1763)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
314bdb2f49
commit
e8b80f0e6d
2 changed files with 971 additions and 44 deletions
|
|
@ -63,9 +63,12 @@ public class ClientConformanceTests
|
|||
[InlineData("auth/2025-03-26-oauth-metadata-backcompat")]
|
||||
[InlineData("auth/2025-03-26-oauth-endpoint-fallback")]
|
||||
|
||||
// Extensions: Require ES256 JWT signing (private_key_jwt) and client_credentials grant support.
|
||||
// [InlineData("auth/client-credentials-jwt")]
|
||||
// [InlineData("auth/client-credentials-basic")]
|
||||
[InlineData("auth/authorization-server-migration")]
|
||||
[InlineData("auth/client-credentials-jwt")]
|
||||
[InlineData("auth/client-credentials-basic")]
|
||||
[InlineData("auth/enterprise-managed-authorization")]
|
||||
[InlineData("sep-2322-client-request-state")]
|
||||
[InlineData("json-schema-ref-no-deref")]
|
||||
|
||||
public async Task RunConformanceTest(string scenario)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using ModelContextProtocol.Authentication;
|
||||
using ModelContextProtocol.Client;
|
||||
using ModelContextProtocol.Protocol;
|
||||
|
||||
namespace ModelContextProtocol.AspNetCore.Tests.OAuth;
|
||||
|
||||
|
|
@ -187,6 +188,182 @@ public class TokenCacheTests : OAuthTestBase
|
|||
Assert.NotEqual("invalid-token", tokenCache.LastStoredToken.AccessToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTokenAsync_ExplicitClientDoesNotRefreshTokenFromDifferentAuthorizationServer()
|
||||
{
|
||||
await using var app = await StartMcpServerAsync();
|
||||
|
||||
var tokenCache = new TestTokenCache();
|
||||
await using var setupTransport = new HttpClientTransport(new()
|
||||
{
|
||||
Endpoint = new(McpServerUrl),
|
||||
OAuth = new()
|
||||
{
|
||||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
TokenCache = tokenCache,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
await using (var setupClient = await McpClient.CreateAsync(
|
||||
setupTransport,
|
||||
loggerFactory: LoggerFactory,
|
||||
cancellationToken: TestContext.Current.CancellationToken))
|
||||
{
|
||||
}
|
||||
|
||||
Assert.NotNull(tokenCache.LastStoredToken);
|
||||
tokenCache.LastStoredToken.AccessToken = "invalid-token";
|
||||
tokenCache.LastStoredToken.AuthorizationServer = "https://different-authorization-server.example.com";
|
||||
var authorizationCallbackCalled = false;
|
||||
|
||||
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"),
|
||||
AuthorizationCallbackHandler = (context, ct) =>
|
||||
{
|
||||
authorizationCallbackCalled = true;
|
||||
return HandleAuthorizationUrlAsync(context, ct);
|
||||
},
|
||||
TokenCache = tokenCache,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<McpException>(() => McpClient.CreateAsync(
|
||||
transport,
|
||||
loggerFactory: LoggerFactory,
|
||||
cancellationToken: TestContext.Current.CancellationToken));
|
||||
|
||||
Assert.Contains("explicitly configured client credentials", exception.Message);
|
||||
Assert.False(authorizationCallbackCalled);
|
||||
Assert.False(TestOAuthServer.HasRefreshedToken);
|
||||
Assert.Equal("https://different-authorization-server.example.com", tokenCache.LastStoredToken.AuthorizationServer);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTokenAsync_ExplicitClientDoesNotRefreshTokenIssuedToDifferentClient()
|
||||
{
|
||||
await using var app = await StartMcpServerAsync();
|
||||
|
||||
var tokenCache = new TestTokenCache();
|
||||
await using var setupTransport = new HttpClientTransport(new()
|
||||
{
|
||||
Endpoint = new(McpServerUrl),
|
||||
OAuth = new()
|
||||
{
|
||||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
TokenCache = tokenCache,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
await using (var setupClient = await McpClient.CreateAsync(
|
||||
setupTransport,
|
||||
loggerFactory: LoggerFactory,
|
||||
cancellationToken: TestContext.Current.CancellationToken))
|
||||
{
|
||||
}
|
||||
|
||||
Assert.NotNull(tokenCache.LastStoredToken);
|
||||
tokenCache.LastStoredToken.AccessToken = "invalid-token";
|
||||
tokenCache.LastStoredToken.ClientId = "different-client";
|
||||
var authorizationCallbackCalled = false;
|
||||
|
||||
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"),
|
||||
AuthorizationCallbackHandler = (context, ct) =>
|
||||
{
|
||||
authorizationCallbackCalled = true;
|
||||
return HandleAuthorizationUrlAsync(context, ct);
|
||||
},
|
||||
TokenCache = tokenCache,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
await using var client = await McpClient.CreateAsync(
|
||||
transport,
|
||||
loggerFactory: LoggerFactory,
|
||||
cancellationToken: TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.True(authorizationCallbackCalled);
|
||||
Assert.False(TestOAuthServer.HasRefreshedToken);
|
||||
Assert.Equal("demo-client", tokenCache.LastStoredToken.ClientId);
|
||||
Assert.Equal(OAuthServerUrl, tokenCache.LastStoredToken.AuthorizationServer);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTokenAsync_ExplicitClientWithLegacyUnboundCache_Reauthorizes()
|
||||
{
|
||||
await using var app = await StartMcpServerAsync();
|
||||
|
||||
var tokenCache = new TestTokenCache();
|
||||
await using var setupTransport = new HttpClientTransport(new()
|
||||
{
|
||||
Endpoint = new(McpServerUrl),
|
||||
OAuth = new()
|
||||
{
|
||||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
TokenCache = tokenCache,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
await using (var setupClient = await McpClient.CreateAsync(
|
||||
setupTransport,
|
||||
loggerFactory: LoggerFactory,
|
||||
cancellationToken: TestContext.Current.CancellationToken))
|
||||
{
|
||||
}
|
||||
|
||||
Assert.NotNull(tokenCache.LastStoredToken);
|
||||
tokenCache.LastStoredToken.AccessToken = "invalid-token";
|
||||
tokenCache.LastStoredToken.AuthorizationServer = null;
|
||||
var authorizationCallbackCalled = false;
|
||||
|
||||
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"),
|
||||
AuthorizationCallbackHandler = (context, ct) =>
|
||||
{
|
||||
authorizationCallbackCalled = true;
|
||||
return HandleAuthorizationUrlAsync(context, ct);
|
||||
},
|
||||
TokenCache = tokenCache,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
await using var client = await McpClient.CreateAsync(
|
||||
transport,
|
||||
loggerFactory: LoggerFactory,
|
||||
cancellationToken: TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.True(authorizationCallbackCalled);
|
||||
Assert.False(TestOAuthServer.HasRefreshedToken);
|
||||
Assert.Equal(OAuthServerUrl, tokenCache.LastStoredToken.AuthorizationServer);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTokenAsync_ColdStartWithDynamicRegistration_RefreshesUsingPersistedCredentials()
|
||||
{
|
||||
|
|
@ -227,6 +404,7 @@ public class TokenCacheTests : OAuthTestBase
|
|||
Assert.False(
|
||||
string.IsNullOrEmpty(tokenCache.LastStoredToken.ClientId),
|
||||
"The dynamically registered client ID should be persisted alongside the tokens");
|
||||
Assert.Equal(OAuthServerUrl, tokenCache.LastStoredToken.AuthorizationServer);
|
||||
|
||||
// Simulate a cold start: the access token is no longer valid, but the refresh token persists.
|
||||
// The new provider has no client ID configured and must restore it from the cache to refresh
|
||||
|
|
@ -261,6 +439,119 @@ public class TokenCacheTests : OAuthTestBase
|
|||
Assert.NotEqual("invalid-token", tokenCache.LastStoredToken.AccessToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTokenAsync_ColdStartWithCredentialsFromDifferentAuthorizationServer_Reregisters()
|
||||
{
|
||||
await using var app = await StartMcpServerAsync();
|
||||
|
||||
var tokenCache = new TestTokenCache();
|
||||
await using var setupTransport = new HttpClientTransport(new()
|
||||
{
|
||||
Endpoint = new(McpServerUrl),
|
||||
OAuth = new()
|
||||
{
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
DynamicClientRegistration = new() { ClientName = "Test MCP Client" },
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
TokenCache = tokenCache,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
await using (var setupClient = await McpClient.CreateAsync(
|
||||
setupTransport,
|
||||
loggerFactory: LoggerFactory,
|
||||
cancellationToken: TestContext.Current.CancellationToken))
|
||||
{
|
||||
}
|
||||
|
||||
Assert.NotNull(tokenCache.LastStoredToken);
|
||||
tokenCache.LastStoredToken.AccessToken = "invalid-token";
|
||||
tokenCache.LastStoredToken.AuthorizationServer = "https://different-authorization-server.example.com";
|
||||
var authorizationCallbackCalled = false;
|
||||
|
||||
await using var transport = new HttpClientTransport(new()
|
||||
{
|
||||
Endpoint = new(McpServerUrl),
|
||||
OAuth = new()
|
||||
{
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
DynamicClientRegistration = new() { ClientName = "Test MCP Client" },
|
||||
AuthorizationCallbackHandler = (context, ct) =>
|
||||
{
|
||||
authorizationCallbackCalled = true;
|
||||
return HandleAuthorizationUrlAsync(context, ct);
|
||||
},
|
||||
TokenCache = tokenCache,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
await using var client = await McpClient.CreateAsync(
|
||||
transport,
|
||||
loggerFactory: LoggerFactory,
|
||||
cancellationToken: TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.True(authorizationCallbackCalled);
|
||||
Assert.False(TestOAuthServer.HasRefreshedToken);
|
||||
Assert.Equal(OAuthServerUrl, tokenCache.LastStoredToken.AuthorizationServer);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTokenAsync_ColdStartWithLegacyUnboundCredentials_Reregisters()
|
||||
{
|
||||
await using var app = await StartMcpServerAsync();
|
||||
|
||||
var tokenCache = new TestTokenCache();
|
||||
await using var setupTransport = new HttpClientTransport(new()
|
||||
{
|
||||
Endpoint = new(McpServerUrl),
|
||||
OAuth = new()
|
||||
{
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
DynamicClientRegistration = new() { ClientName = "Test MCP Client" },
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
TokenCache = tokenCache,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
await using (var setupClient = await McpClient.CreateAsync(
|
||||
setupTransport,
|
||||
loggerFactory: LoggerFactory,
|
||||
cancellationToken: TestContext.Current.CancellationToken))
|
||||
{
|
||||
}
|
||||
|
||||
Assert.NotNull(tokenCache.LastStoredToken);
|
||||
Assert.False(string.IsNullOrEmpty(tokenCache.LastStoredToken.ClientId));
|
||||
tokenCache.LastStoredToken.AccessToken = "invalid-token";
|
||||
tokenCache.LastStoredToken.AuthorizationServer = null;
|
||||
var authorizationCallbackCalled = false;
|
||||
|
||||
await using var transport = new HttpClientTransport(new()
|
||||
{
|
||||
Endpoint = new(McpServerUrl),
|
||||
OAuth = new()
|
||||
{
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
DynamicClientRegistration = new() { ClientName = "Test MCP Client" },
|
||||
AuthorizationCallbackHandler = (context, ct) =>
|
||||
{
|
||||
authorizationCallbackCalled = true;
|
||||
return HandleAuthorizationUrlAsync(context, ct);
|
||||
},
|
||||
TokenCache = tokenCache,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
await using var client = await McpClient.CreateAsync(
|
||||
transport,
|
||||
loggerFactory: LoggerFactory,
|
||||
cancellationToken: TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.True(authorizationCallbackCalled);
|
||||
Assert.False(TestOAuthServer.HasRefreshedToken);
|
||||
Assert.Equal(OAuthServerUrl, tokenCache.LastStoredToken.AuthorizationServer);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTokenAsync_ColdStartWithoutPersistedClientId_FallsBackToReauthorization()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue