Improvements to dynamic client registration (#609)
* Add a delegate for handling the dynamic client registration response. * Support passing an initial access token for dynamic client registration.
This commit is contained in:
parent
2fa658e439
commit
f9129f5b28
7 changed files with 102 additions and 28 deletions
|
|
@ -31,9 +31,12 @@ var transport = new SseClientTransport(new()
|
|||
Name = "Secure Weather Client",
|
||||
OAuth = new()
|
||||
{
|
||||
ClientName = "ProtectedMcpClient",
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
DynamicClientRegistration = new()
|
||||
{
|
||||
ClientName = "ProtectedMcpClient",
|
||||
},
|
||||
}
|
||||
}, httpClient, consoleLoggerFactory);
|
||||
|
||||
|
|
|
|||
|
|
@ -68,22 +68,12 @@ public sealed class ClientOAuthOptions
|
|||
public Func<IReadOnlyList<Uri>, Uri?>? AuthServerSelector { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the client name to use during dynamic client registration.
|
||||
/// Gets or sets the options to use during dynamic client registration.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is a human-readable name for the client that may be displayed to users during authorization.
|
||||
/// Only used when a <see cref="ClientId"/> is not specified.
|
||||
/// </remarks>
|
||||
public string? ClientName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the client URI to use during dynamic client registration.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This should be a URL pointing to the client's home page or information page.
|
||||
/// Only used when a <see cref="ClientId"/> is not specified.
|
||||
/// </remarks>
|
||||
public Uri? ClientUri { get; set; }
|
||||
public DynamicClientRegistrationOptions? DynamicClientRegistration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets additional parameters to include in the query string of the OAuth authorization request
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
|
@ -28,9 +28,11 @@ internal sealed partial class ClientOAuthProvider
|
|||
private readonly Func<IReadOnlyList<Uri>, Uri?> _authServerSelector;
|
||||
private readonly AuthorizationRedirectDelegate _authorizationRedirectDelegate;
|
||||
|
||||
// _clientName and _client URI is used for dynamic client registration (RFC 7591)
|
||||
private readonly string? _clientName;
|
||||
private readonly Uri? _clientUri;
|
||||
// _dcrClientName, _dcrClientUri, _dcrInitialAccessToken and _dcrResponseDelegate are used for dynamic client registration (RFC 7591)
|
||||
private readonly string? _dcrClientName;
|
||||
private readonly Uri? _dcrClientUri;
|
||||
private readonly string? _dcrInitialAccessToken;
|
||||
private readonly Func<DynamicClientRegistrationResponse, CancellationToken, Task>? _dcrResponseDelegate;
|
||||
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger _logger;
|
||||
|
|
@ -66,9 +68,7 @@ internal sealed partial class ClientOAuthProvider
|
|||
|
||||
_clientId = options.ClientId;
|
||||
_clientSecret = options.ClientSecret;
|
||||
_redirectUri = options.RedirectUri ?? throw new ArgumentException("ClientOAuthOptions.RedirectUri must configured.");
|
||||
_clientName = options.ClientName;
|
||||
_clientUri = options.ClientUri;
|
||||
_redirectUri = options.RedirectUri ?? throw new ArgumentException("ClientOAuthOptions.RedirectUri must configured.", nameof(options));
|
||||
_scopes = options.Scopes?.ToArray();
|
||||
_additionalAuthorizationParameters = options.AdditionalAuthorizationParameters;
|
||||
|
||||
|
|
@ -77,6 +77,11 @@ internal sealed partial class ClientOAuthProvider
|
|||
|
||||
// Set up authorization URL handler (use default if not provided)
|
||||
_authorizationRedirectDelegate = options.AuthorizationRedirectDelegate ?? DefaultAuthorizationUrlHandler;
|
||||
|
||||
_dcrClientName = options.DynamicClientRegistration?.ClientName;
|
||||
_dcrClientUri = options.DynamicClientRegistration?.ClientUri;
|
||||
_dcrInitialAccessToken = options.DynamicClientRegistration?.InitialAccessToken;
|
||||
_dcrResponseDelegate = options.DynamicClientRegistration?.ResponseDelegate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -447,8 +452,8 @@ internal sealed partial class ClientOAuthProvider
|
|||
GrantTypes = ["authorization_code", "refresh_token"],
|
||||
ResponseTypes = ["code"],
|
||||
TokenEndpointAuthMethod = "client_secret_post",
|
||||
ClientName = _clientName,
|
||||
ClientUri = _clientUri?.ToString(),
|
||||
ClientName = _dcrClientName,
|
||||
ClientUri = _dcrClientUri?.ToString(),
|
||||
Scope = _scopes is not null ? string.Join(" ", _scopes) : null
|
||||
};
|
||||
|
||||
|
|
@ -460,6 +465,11 @@ internal sealed partial class ClientOAuthProvider
|
|||
Content = requestContent
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(_dcrInitialAccessToken))
|
||||
{
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue(BearerScheme, _dcrInitialAccessToken);
|
||||
}
|
||||
|
||||
using var httpResponse = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!httpResponse.IsSuccessStatusCode)
|
||||
|
|
@ -487,6 +497,11 @@ internal sealed partial class ClientOAuthProvider
|
|||
}
|
||||
|
||||
LogDynamicClientRegistrationSuccessful(_clientId!);
|
||||
|
||||
if (_dcrResponseDelegate is not null)
|
||||
{
|
||||
await _dcrResponseDelegate(registrationResponse, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
namespace ModelContextProtocol.Authentication;
|
||||
|
||||
/// <summary>
|
||||
/// Provides configuration options for the <see cref="ClientOAuthProvider"/> related to dynamic client registration (RFC 7591).
|
||||
/// </summary>
|
||||
public sealed class DynamicClientRegistrationOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the client name to use during dynamic client registration.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is a human-readable name for the client that may be displayed to users during authorization.
|
||||
/// </remarks>
|
||||
public string? ClientName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the client URI to use during dynamic client registration.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This should be a URL pointing to the client's home page or information page.
|
||||
/// </remarks>
|
||||
public Uri? ClientUri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the initial access token to use during dynamic client registration.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This token is used to authenticate the client during the registration process.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This is required if the authorization server does not allow anonymous client registration.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public string? InitialAccessToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the delegate used for handling the dynamic client registration response.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This delegate is responsible for processing the response from the dynamic client registration endpoint.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The implementation should save the client credentials securely for future use.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public Func<DynamicClientRegistrationResponse, CancellationToken, Task>? ResponseDelegate { get; set; }
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ namespace ModelContextProtocol.Authentication;
|
|||
/// <summary>
|
||||
/// Represents a client registration response for OAuth 2.0 Dynamic Client Registration (RFC 7591).
|
||||
/// </summary>
|
||||
internal sealed class DynamicClientRegistrationResponse
|
||||
public sealed class DynamicClientRegistrationResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the client identifier.
|
||||
|
|
|
|||
|
|
@ -140,6 +140,8 @@ public class AuthEventTests : KestrelInMemoryTest, IAsyncDisposable
|
|||
|
||||
await app.StartAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
DynamicClientRegistrationResponse? dcrResponse = null;
|
||||
|
||||
await using var transport = new SseClientTransport(
|
||||
new()
|
||||
{
|
||||
|
|
@ -148,9 +150,17 @@ public class AuthEventTests : KestrelInMemoryTest, IAsyncDisposable
|
|||
{
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
ClientName = "Test MCP Client",
|
||||
ClientUri = new Uri("https://example.com"),
|
||||
Scopes = ["mcp:tools"],
|
||||
DynamicClientRegistration = new()
|
||||
{
|
||||
ClientName = "Test MCP Client",
|
||||
ClientUri = new Uri("https://example.com"),
|
||||
ResponseDelegate = (response, cancellationToken) =>
|
||||
{
|
||||
dcrResponse = response;
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
HttpClient,
|
||||
|
|
@ -162,6 +172,10 @@ public class AuthEventTests : KestrelInMemoryTest, IAsyncDisposable
|
|||
loggerFactory: LoggerFactory,
|
||||
cancellationToken: TestContext.Current.CancellationToken
|
||||
);
|
||||
|
||||
Assert.NotNull(dcrResponse);
|
||||
Assert.False(string.IsNullOrEmpty(dcrResponse.ClientId));
|
||||
Assert.False(string.IsNullOrEmpty(dcrResponse.ClientSecret));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -181,9 +181,12 @@ public class AuthTests : KestrelInMemoryTest, IAsyncDisposable
|
|||
{
|
||||
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||
ClientName = "Test MCP Client",
|
||||
ClientUri = new Uri("https://example.com"),
|
||||
Scopes = ["mcp:tools"]
|
||||
Scopes = ["mcp:tools"],
|
||||
DynamicClientRegistration = new()
|
||||
{
|
||||
ClientName = "Test MCP Client",
|
||||
ClientUri = new Uri("https://example.com"),
|
||||
},
|
||||
},
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue