diff --git a/samples/ProtectedMcpClient/Program.cs b/samples/ProtectedMcpClient/Program.cs index 516227b37..5871284a7 100644 --- a/samples/ProtectedMcpClient/Program.cs +++ b/samples/ProtectedMcpClient/Program.cs @@ -31,9 +31,12 @@ Name = "Secure Weather Client", OAuth = new() { - ClientName = "ProtectedMcpClient", RedirectUri = new Uri("http://localhost:1179/callback"), AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + DynamicClientRegistration = new() + { + ClientName = "ProtectedMcpClient", + }, } }, httpClient, consoleLoggerFactory); diff --git a/src/ModelContextProtocol.Core/Authentication/ClientOAuthOptions.cs b/src/ModelContextProtocol.Core/Authentication/ClientOAuthOptions.cs index 686316f55..cc6a8952e 100644 --- a/src/ModelContextProtocol.Core/Authentication/ClientOAuthOptions.cs +++ b/src/ModelContextProtocol.Core/Authentication/ClientOAuthOptions.cs @@ -68,22 +68,12 @@ public sealed class ClientOAuthOptions public Func, Uri?>? AuthServerSelector { get; set; } /// - /// Gets or sets the client name to use during dynamic client registration. + /// Gets or sets the options to use during dynamic client registration. /// /// - /// This is a human-readable name for the client that may be displayed to users during authorization. /// Only used when a is not specified. /// - public string? ClientName { get; set; } - - /// - /// Gets or sets the client URI to use during dynamic client registration. - /// - /// - /// This should be a URL pointing to the client's home page or information page. - /// Only used when a is not specified. - /// - public Uri? ClientUri { get; set; } + public DynamicClientRegistrationOptions? DynamicClientRegistration { get; set; } /// /// Gets or sets additional parameters to include in the query string of the OAuth authorization request diff --git a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs index 686d749ff..b72f775c4 100644 --- a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs +++ b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs @@ -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, 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? _dcrResponseDelegate; private readonly HttpClient _httpClient; private readonly ILogger _logger; @@ -66,9 +68,7 @@ public 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 @@ public 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; } /// @@ -447,8 +452,8 @@ private async Task PerformDynamicClientRegistrationAsync( 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 @@ private async Task PerformDynamicClientRegistrationAsync( 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 @@ private async Task PerformDynamicClientRegistrationAsync( } LogDynamicClientRegistrationSuccessful(_clientId!); + + if (_dcrResponseDelegate is not null) + { + await _dcrResponseDelegate(registrationResponse, cancellationToken).ConfigureAwait(false); + } } /// diff --git a/src/ModelContextProtocol.Core/Authentication/DynamicClientRegistrationOptions.cs b/src/ModelContextProtocol.Core/Authentication/DynamicClientRegistrationOptions.cs new file mode 100644 index 000000000..c7337122e --- /dev/null +++ b/src/ModelContextProtocol.Core/Authentication/DynamicClientRegistrationOptions.cs @@ -0,0 +1,49 @@ +namespace ModelContextProtocol.Authentication; + +/// +/// Provides configuration options for the related to dynamic client registration (RFC 7591). +/// +public sealed class DynamicClientRegistrationOptions +{ + /// + /// Gets or sets the client name to use during dynamic client registration. + /// + /// + /// This is a human-readable name for the client that may be displayed to users during authorization. + /// + public string? ClientName { get; set; } + + /// + /// Gets or sets the client URI to use during dynamic client registration. + /// + /// + /// This should be a URL pointing to the client's home page or information page. + /// + public Uri? ClientUri { get; set; } + + /// + /// Gets or sets the initial access token to use during dynamic client registration. + /// + /// + /// + /// This token is used to authenticate the client during the registration process. + /// + /// + /// This is required if the authorization server does not allow anonymous client registration. + /// + /// + public string? InitialAccessToken { get; set; } + + /// + /// Gets or sets the delegate used for handling the dynamic client registration response. + /// + /// + /// + /// This delegate is responsible for processing the response from the dynamic client registration endpoint. + /// + /// + /// The implementation should save the client credentials securely for future use. + /// + /// + public Func? ResponseDelegate { get; set; } +} \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Authentication/DynamicClientRegistrationResponse.cs b/src/ModelContextProtocol.Core/Authentication/DynamicClientRegistrationResponse.cs index dcd51d68a..1dfe12294 100644 --- a/src/ModelContextProtocol.Core/Authentication/DynamicClientRegistrationResponse.cs +++ b/src/ModelContextProtocol.Core/Authentication/DynamicClientRegistrationResponse.cs @@ -5,7 +5,7 @@ namespace ModelContextProtocol.Authentication; /// /// Represents a client registration response for OAuth 2.0 Dynamic Client Registration (RFC 7591). /// -internal sealed class DynamicClientRegistrationResponse +public sealed class DynamicClientRegistrationResponse { /// /// Gets or sets the client identifier. diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/AuthEventTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/AuthEventTests.cs index c993edd4c..efff68c84 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/AuthEventTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/AuthEventTests.cs @@ -140,6 +140,8 @@ public async Task CanAuthenticate_WithDynamicClientRegistration_FromEvent() await app.StartAsync(TestContext.Current.CancellationToken); + DynamicClientRegistrationResponse? dcrResponse = null; + await using var transport = new SseClientTransport( new() { @@ -148,9 +150,17 @@ public async Task CanAuthenticate_WithDynamicClientRegistration_FromEvent() { 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 async Task CanAuthenticate_WithDynamicClientRegistration_FromEvent() loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken ); + + Assert.NotNull(dcrResponse); + Assert.False(string.IsNullOrEmpty(dcrResponse.ClientId)); + Assert.False(string.IsNullOrEmpty(dcrResponse.ClientSecret)); } [Fact] diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/AuthTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/AuthTests.cs index 2252b1b7c..b480934ac 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/AuthTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/AuthTests.cs @@ -181,9 +181,12 @@ public async Task CanAuthenticate_WithDynamicClientRegistration() { 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);