Implement RFC 9207 issuer validation in ClientOAuthProvider (#1605)
This commit is contained in:
parent
990bd76b53
commit
4aade88558
1 changed files with 707 additions and 149 deletions
|
|
@ -42,7 +42,7 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
|
|
@ -50,6 +50,132 @@ public class AuthTests : OAuthTestBase
|
|||
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AuthorizationCallbackHandler_ReceivesConfiguredRedirectUri()
|
||||
{
|
||||
await using var app = await StartMcpServerAsync();
|
||||
|
||||
var redirectUri = new Uri("http://localhost:1179/callback");
|
||||
AuthorizationCallbackContext? callbackContext = null;
|
||||
|
||||
await using var transport = new HttpClientTransport(new()
|
||||
{
|
||||
Endpoint = new(McpServerUrl),
|
||||
OAuth = new()
|
||||
{
|
||||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = redirectUri,
|
||||
AuthorizationCallbackHandler = (context, cancellationToken) =>
|
||||
{
|
||||
callbackContext = context;
|
||||
return HandleAuthorizationUrlAsync(context, cancellationToken);
|
||||
},
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
await using var client = await McpClient.CreateAsync(
|
||||
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.NotNull(callbackContext);
|
||||
Assert.Equal(redirectUri, callbackContext.RedirectUri);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, null)]
|
||||
[InlineData(true, "https://localhost:7029")]
|
||||
[InlineData(true, "https://attacker.example")]
|
||||
public async Task AuthorizationRedirectDelegate_ReceivesConfiguredUrisAndSkipsResponseIssuerValidation(
|
||||
bool authorizationResponseIssParameterSupported,
|
||||
string? authorizationResponseIssuer)
|
||||
{
|
||||
TestOAuthServer.AuthorizationResponseIssParameterSupported = authorizationResponseIssParameterSupported;
|
||||
TestOAuthServer.AuthorizationResponseIssuer = authorizationResponseIssuer;
|
||||
await using var app = await StartMcpServerAsync();
|
||||
|
||||
var redirectUri = new Uri("http://localhost:1179/callback");
|
||||
Uri? receivedAuthorizationUri = null;
|
||||
Uri? receivedRedirectUri = null;
|
||||
|
||||
await using var transport = new HttpClientTransport(new()
|
||||
{
|
||||
Endpoint = new(McpServerUrl),
|
||||
OAuth = new()
|
||||
{
|
||||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = redirectUri,
|
||||
#pragma warning disable MCP9007 // Verify the obsolete callback remains functional during its compatibility window.
|
||||
AuthorizationRedirectDelegate = (authorizationUri, callbackRedirectUri, cancellationToken) =>
|
||||
{
|
||||
receivedAuthorizationUri = authorizationUri;
|
||||
receivedRedirectUri = callbackRedirectUri;
|
||||
return HandleAuthorizationUrlAsync(authorizationUri, callbackRedirectUri, cancellationToken);
|
||||
},
|
||||
#pragma warning restore MCP9007
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
await using var client = await McpClient.CreateAsync(
|
||||
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.NotNull(receivedAuthorizationUri);
|
||||
Assert.Equal(redirectUri, receivedRedirectUri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AuthorizationRedirectDelegate_DoesNotSkipMetadataIssuerValidation()
|
||||
{
|
||||
TestOAuthServer.MetadataIssuerOverride = "https://attacker.example";
|
||||
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"),
|
||||
#pragma warning disable MCP9007 // Verify the obsolete callback retains metadata issuer validation.
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
#pragma warning restore MCP9007
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
var ex = await Assert.ThrowsAsync<McpException>(() => McpClient.CreateAsync(
|
||||
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken));
|
||||
|
||||
Assert.Contains("does not match the expected issuer", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HttpClientTransport_RejectsBothAuthorizationCallbacks()
|
||||
{
|
||||
#pragma warning disable MCP9007 // Verify ambiguous legacy and current callback configuration is rejected.
|
||||
var options = new ClientOAuthOptions
|
||||
{
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationCallbackHandler = (_, _) => Task.FromResult<ModelContextProtocol.Authentication.AuthorizationResult?>(new()),
|
||||
AuthorizationRedirectDelegate = (_, _, _) => Task.FromResult<string?>("code"),
|
||||
};
|
||||
#pragma warning restore MCP9007
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => new HttpClientTransport(
|
||||
new()
|
||||
{
|
||||
Endpoint = new(McpServerUrl),
|
||||
OAuth = options,
|
||||
},
|
||||
HttpClient,
|
||||
LoggerFactory));
|
||||
|
||||
Assert.Contains(nameof(ClientOAuthOptions.AuthorizationCallbackHandler), ex.Message);
|
||||
#pragma warning disable MCP9007 // The obsolete property name should be included in the diagnostic.
|
||||
Assert.Contains(nameof(ClientOAuthOptions.AuthorizationRedirectDelegate), ex.Message);
|
||||
#pragma warning restore MCP9007
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CannotAuthenticate_WithoutOAuthConfiguration()
|
||||
{
|
||||
|
|
@ -79,7 +205,7 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "unregistered-demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
|
|
@ -99,7 +225,7 @@ public class AuthTests : OAuthTestBase
|
|||
OAuth = new ClientOAuthOptions()
|
||||
{
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
DynamicClientRegistration = new()
|
||||
{
|
||||
ClientName = "Test MCP Client",
|
||||
|
|
@ -125,7 +251,7 @@ public class AuthTests : OAuthTestBase
|
|||
OAuth = new ClientOAuthOptions()
|
||||
{
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
DynamicClientRegistration = new()
|
||||
{
|
||||
ApplicationType = "web",
|
||||
|
|
@ -150,7 +276,7 @@ public class AuthTests : OAuthTestBase
|
|||
OAuth = new ClientOAuthOptions()
|
||||
{
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
ClientMetadataDocumentUri = new Uri(ClientMetadataDocumentUrl),
|
||||
DynamicClientRegistration = new()
|
||||
{
|
||||
|
|
@ -177,7 +303,7 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
|
|
@ -205,7 +331,7 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
|
|
@ -234,7 +360,7 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
|
|
@ -258,7 +384,7 @@ public class AuthTests : OAuthTestBase
|
|||
OAuth = new ClientOAuthOptions()
|
||||
{
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
ClientMetadataDocumentUri = new Uri("http://invalid-cimd.example.com"),
|
||||
DynamicClientRegistration = new()
|
||||
{
|
||||
|
|
@ -287,7 +413,7 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
ClientMetadataDocumentUri = new Uri("http://invalid-cimd.example.com"),
|
||||
DynamicClientRegistration = new()
|
||||
{
|
||||
|
|
@ -313,7 +439,7 @@ public class AuthTests : OAuthTestBase
|
|||
OAuth = new ClientOAuthOptions()
|
||||
{
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
ClientMetadataDocumentUri = new Uri(uri),
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
|
@ -378,7 +504,7 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
|
|
@ -405,10 +531,10 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = (uri, redirect, ct) =>
|
||||
AuthorizationCallbackHandler = (context, ct) =>
|
||||
{
|
||||
lastAuthorizationUri = uri;
|
||||
return HandleAuthorizationUrlAsync(uri, redirect, ct);
|
||||
lastAuthorizationUri = context.AuthorizationUri;
|
||||
return HandleAuthorizationUrlAsync(context, ct);
|
||||
},
|
||||
AdditionalAuthorizationParameters = new Dictionary<string, string>
|
||||
{
|
||||
|
|
@ -437,7 +563,7 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
AdditionalAuthorizationParameters = new Dictionary<string, string>
|
||||
{
|
||||
["redirect_uri"] = "custom_value",
|
||||
|
|
@ -462,7 +588,7 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
|
|
@ -484,7 +610,7 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
|
|
@ -512,11 +638,11 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = (uri, redirect, ct) =>
|
||||
AuthorizationCallbackHandler = (context, ct) =>
|
||||
{
|
||||
var query = QueryHelpers.ParseQuery(uri.Query);
|
||||
var query = QueryHelpers.ParseQuery(context.AuthorizationUri.Query);
|
||||
requestedScope = query["scope"].ToString();
|
||||
return HandleAuthorizationUrlAsync(uri, redirect, ct);
|
||||
return HandleAuthorizationUrlAsync(context, ct);
|
||||
},
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
|
@ -565,11 +691,11 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = (uri, redirect, ct) =>
|
||||
AuthorizationCallbackHandler = (context, ct) =>
|
||||
{
|
||||
var query = QueryHelpers.ParseQuery(uri.Query);
|
||||
var query = QueryHelpers.ParseQuery(context.AuthorizationUri.Query);
|
||||
requestedScope = query["scope"].ToString();
|
||||
return HandleAuthorizationUrlAsync(uri, redirect, ct);
|
||||
return HandleAuthorizationUrlAsync(context, ct);
|
||||
},
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
|
@ -660,11 +786,11 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = (uri, redirect, ct) =>
|
||||
AuthorizationCallbackHandler = (context, ct) =>
|
||||
{
|
||||
var query = QueryHelpers.ParseQuery(uri.Query);
|
||||
var query = QueryHelpers.ParseQuery(context.AuthorizationUri.Query);
|
||||
requestedScope = query["scope"].ToString();
|
||||
return HandleAuthorizationUrlAsync(uri, redirect, ct);
|
||||
return HandleAuthorizationUrlAsync(context, ct);
|
||||
},
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
|
@ -765,11 +891,11 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = (uri, redirect, ct) =>
|
||||
AuthorizationCallbackHandler = (context, ct) =>
|
||||
{
|
||||
var query = QueryHelpers.ParseQuery(uri.Query);
|
||||
var query = QueryHelpers.ParseQuery(context.AuthorizationUri.Query);
|
||||
requestedScopes.Add(query["scope"].ToString());
|
||||
return HandleAuthorizationUrlAsync(uri, redirect, ct);
|
||||
return HandleAuthorizationUrlAsync(context, ct);
|
||||
},
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
|
@ -806,6 +932,8 @@ public class AuthTests : OAuthTestBase
|
|||
// before either has stepped up. They serialize on the provider's token acquisition lock: the
|
||||
// first runs the step-up and caches the broader token, and the second must reuse that token
|
||||
// instead of failing as a "repeated" challenge. Only one interactive step-up should run.
|
||||
TestOAuthServer.AuthorizationResponseIssParameterSupported = true;
|
||||
TestOAuthServer.AuthorizationResponseIssuer = OAuthServerUrl;
|
||||
|
||||
Builder.Services.AddMcpServer()
|
||||
.WithTools([
|
||||
|
|
@ -879,14 +1007,14 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = (uri, redirect, ct) =>
|
||||
AuthorizationCallbackHandler = (context, cancellationToken) =>
|
||||
{
|
||||
var query = QueryHelpers.ParseQuery(uri.Query);
|
||||
var query = QueryHelpers.ParseQuery(context.AuthorizationUri.Query);
|
||||
lock (scopeLock)
|
||||
{
|
||||
requestedScopes.Add(query["scope"].ToString());
|
||||
}
|
||||
return HandleAuthorizationUrlAsync(uri, redirect, ct);
|
||||
return HandleAuthorizationUrlAsync(context, cancellationToken);
|
||||
},
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
|
@ -977,11 +1105,11 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = (uri, redirect, ct) =>
|
||||
AuthorizationCallbackHandler = (context, ct) =>
|
||||
{
|
||||
var query = QueryHelpers.ParseQuery(uri.Query);
|
||||
var query = QueryHelpers.ParseQuery(context.AuthorizationUri.Query);
|
||||
requestedScopes.Add(query["scope"].ToString());
|
||||
return HandleAuthorizationUrlAsync(uri, redirect, ct);
|
||||
return HandleAuthorizationUrlAsync(context, ct);
|
||||
},
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
|
@ -1072,11 +1200,11 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = (uri, redirect, ct) =>
|
||||
AuthorizationCallbackHandler = (context, ct) =>
|
||||
{
|
||||
var query = QueryHelpers.ParseQuery(uri.Query);
|
||||
var query = QueryHelpers.ParseQuery(context.AuthorizationUri.Query);
|
||||
requestedScopes.Add(query["scope"].ToString());
|
||||
return HandleAuthorizationUrlAsync(uri, redirect, ct);
|
||||
return HandleAuthorizationUrlAsync(context, ct);
|
||||
},
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
|
@ -1120,7 +1248,7 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
|
|
@ -1159,7 +1287,7 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
|
|
@ -1187,7 +1315,7 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
|
|
@ -1220,7 +1348,7 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
|
|
@ -1237,6 +1365,69 @@ public class AuthTests : OAuthTestBase
|
|||
TestOAuthServer.MetadataRequests);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CannotAuthenticate_WhenAuthorizationServerMetadataIssuerMismatches()
|
||||
{
|
||||
TestOAuthServer.MetadataIssuerOverride = "https://attacker.example";
|
||||
|
||||
await using var app = await StartMcpServerAsync();
|
||||
await using var transport = CreateOAuthTransport();
|
||||
|
||||
var ex = await Assert.ThrowsAsync<McpException>(() => McpClient.CreateAsync(
|
||||
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken));
|
||||
|
||||
Assert.Contains("does not match the expected issuer", ex.Message);
|
||||
Assert.Single(TestOAuthServer.MetadataRequests);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CannotAuthenticate_WhenAuthorizationServerMetadataOmitsIssuer()
|
||||
{
|
||||
TestOAuthServer.IncludeIssuerInMetadata = false;
|
||||
|
||||
await using var app = await StartMcpServerAsync();
|
||||
await using var transport = CreateOAuthTransport();
|
||||
|
||||
var ex = await Assert.ThrowsAsync<McpException>(() => McpClient.CreateAsync(
|
||||
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken));
|
||||
|
||||
Assert.Contains("did not provide the required issuer", ex.Message);
|
||||
Assert.Single(TestOAuthServer.MetadataRequests);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanAuthenticate_WhenAuthorizationResponseIssuerMatches()
|
||||
{
|
||||
TestOAuthServer.AuthorizationResponseIssParameterSupported = true;
|
||||
TestOAuthServer.AuthorizationResponseIssuer = OAuthServerUrl;
|
||||
|
||||
await using var app = await StartMcpServerAsync();
|
||||
await using var transport = CreateOAuthTransport();
|
||||
await using var client = await McpClient.CreateAsync(
|
||||
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true, "https://attacker.example", "does not match expected issuer")]
|
||||
[InlineData(true, null, "advertises RFC 9207 iss parameter support but none was received")]
|
||||
[InlineData(false, "https://attacker.example", "does not match expected issuer")]
|
||||
public async Task CannotAuthenticate_WhenAuthorizationResponseIssuerIsInvalid(
|
||||
bool authorizationResponseIssParameterSupported,
|
||||
string? authorizationResponseIssuer,
|
||||
string expectedMessage)
|
||||
{
|
||||
TestOAuthServer.AuthorizationResponseIssParameterSupported = authorizationResponseIssParameterSupported;
|
||||
TestOAuthServer.AuthorizationResponseIssuer = authorizationResponseIssuer;
|
||||
|
||||
await using var app = await StartMcpServerAsync();
|
||||
await using var transport = CreateOAuthTransport();
|
||||
|
||||
var ex = await Assert.ThrowsAsync<McpException>(() => McpClient.CreateAsync(
|
||||
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken));
|
||||
|
||||
Assert.Contains(expectedMessage, ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanAuthenticate_WithResourceMetadataPathFallbacks()
|
||||
{
|
||||
|
|
@ -1284,7 +1475,7 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
|
|
@ -1339,7 +1530,7 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
|
|
@ -1386,7 +1577,7 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
|
|
@ -1428,7 +1619,7 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
|
|
@ -1474,7 +1665,7 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
|
|
@ -1521,7 +1712,7 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
|
|
@ -1673,7 +1864,7 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
|
|
@ -1790,7 +1981,7 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
|
|
@ -1892,7 +2083,7 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
|
|
@ -1963,7 +2154,7 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
|
|
@ -1991,11 +2182,11 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = (uri, redirect, ct) =>
|
||||
AuthorizationCallbackHandler = (context, ct) =>
|
||||
{
|
||||
var query = QueryHelpers.ParseQuery(uri.Query);
|
||||
var query = QueryHelpers.ParseQuery(context.AuthorizationUri.Query);
|
||||
requestedScope = query["scope"].ToString();
|
||||
return HandleAuthorizationUrlAsync(uri, redirect, ct);
|
||||
return HandleAuthorizationUrlAsync(context, ct);
|
||||
},
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
|
@ -2023,11 +2214,11 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = (uri, redirect, ct) =>
|
||||
AuthorizationCallbackHandler = (context, ct) =>
|
||||
{
|
||||
var query = QueryHelpers.ParseQuery(uri.Query);
|
||||
var query = QueryHelpers.ParseQuery(context.AuthorizationUri.Query);
|
||||
requestedScope = query["scope"].ToString();
|
||||
return HandleAuthorizationUrlAsync(uri, redirect, ct);
|
||||
return HandleAuthorizationUrlAsync(context, ct);
|
||||
},
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
|
@ -2062,11 +2253,11 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = (uri, redirect, ct) =>
|
||||
AuthorizationCallbackHandler = (context, ct) =>
|
||||
{
|
||||
var query = QueryHelpers.ParseQuery(uri.Query);
|
||||
var query = QueryHelpers.ParseQuery(context.AuthorizationUri.Query);
|
||||
requestedScope = query["scope"].ToString();
|
||||
return HandleAuthorizationUrlAsync(uri, redirect, ct);
|
||||
return HandleAuthorizationUrlAsync(context, ct);
|
||||
},
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
|
@ -2099,11 +2290,11 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = (uri, redirect, ct) =>
|
||||
AuthorizationCallbackHandler = (context, ct) =>
|
||||
{
|
||||
var query = QueryHelpers.ParseQuery(uri.Query);
|
||||
var query = QueryHelpers.ParseQuery(context.AuthorizationUri.Query);
|
||||
requestedScope = query["scope"].ToString();
|
||||
return HandleAuthorizationUrlAsync(uri, redirect, ct);
|
||||
return HandleAuthorizationUrlAsync(context, ct);
|
||||
},
|
||||
ScopeSelector = scopes => scopes?.Where(s => s == "mcp:tools"),
|
||||
},
|
||||
|
|
@ -2130,11 +2321,11 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = (uri, redirect, ct) =>
|
||||
AuthorizationCallbackHandler = (context, ct) =>
|
||||
{
|
||||
var query = QueryHelpers.ParseQuery(uri.Query);
|
||||
var query = QueryHelpers.ParseQuery(context.AuthorizationUri.Query);
|
||||
requestedScope = query["scope"].ToString();
|
||||
return HandleAuthorizationUrlAsync(uri, redirect, ct);
|
||||
return HandleAuthorizationUrlAsync(context, ct);
|
||||
},
|
||||
ScopeSelector = scopes => scopes?.Append("custom:scope") ?? ["custom:scope"],
|
||||
},
|
||||
|
|
@ -2168,7 +2359,7 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
ScopeSelector = scopes =>
|
||||
{
|
||||
capturedInput = scopes;
|
||||
|
|
@ -2198,10 +2389,10 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = (uri, redirect, ct) =>
|
||||
AuthorizationCallbackHandler = (context, ct) =>
|
||||
{
|
||||
scopePresent = QueryHelpers.ParseQuery(uri.Query).ContainsKey("scope");
|
||||
return HandleAuthorizationUrlAsync(uri, redirect, ct);
|
||||
scopePresent = QueryHelpers.ParseQuery(context.AuthorizationUri.Query).ContainsKey("scope");
|
||||
return HandleAuthorizationUrlAsync(context, ct);
|
||||
},
|
||||
ScopeSelector = _ => null,
|
||||
},
|
||||
|
|
@ -2228,10 +2419,10 @@ public class AuthTests : OAuthTestBase
|
|||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = (uri, redirect, ct) =>
|
||||
AuthorizationCallbackHandler = (context, ct) =>
|
||||
{
|
||||
scopePresent = QueryHelpers.ParseQuery(uri.Query).ContainsKey("scope");
|
||||
return HandleAuthorizationUrlAsync(uri, redirect, ct);
|
||||
scopePresent = QueryHelpers.ParseQuery(context.AuthorizationUri.Query).ContainsKey("scope");
|
||||
return HandleAuthorizationUrlAsync(context, ct);
|
||||
},
|
||||
ScopeSelector = _ => [],
|
||||
},
|
||||
|
|
@ -2243,6 +2434,19 @@ public class AuthTests : OAuthTestBase
|
|||
Assert.False(scopePresent);
|
||||
}
|
||||
|
||||
private HttpClientTransport CreateOAuthTransport() =>
|
||||
new(new()
|
||||
{
|
||||
Endpoint = new(McpServerUrl),
|
||||
OAuth = new()
|
||||
{
|
||||
ClientId = "demo-client",
|
||||
ClientSecret = "demo-secret",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
[Fact]
|
||||
public async Task DynamicClientRegistration_ScopeSelector_AppliesToDcrScope()
|
||||
{
|
||||
|
|
@ -2259,7 +2463,7 @@ public class AuthTests : OAuthTestBase
|
|||
OAuth = new ClientOAuthOptions()
|
||||
{
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
|
||||
DynamicClientRegistration = new() { ClientName = "Test MCP Client" },
|
||||
ScopeSelector = scopes => scopes?.Where(s => s == "mcp:tools"),
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue