From 54965900e0126a3115a4b1ee5ba1a8eba90a40f6 Mon Sep 17 00:00:00 2001 From: Steven Luiten Date: Thu, 10 Jul 2025 15:16:38 +0200 Subject: [PATCH 01/10] Add a delegate for handling the dynamic client registration response. --- .../Authentication/ClientOAuthOptions.cs | 13 +++++++++++++ .../Authentication/ClientOAuthProvider.cs | 9 +++++++++ .../DynamicClientRegistrationDelegate.cs | 13 +++++++++++++ .../DynamicClientRegistrationResponse.cs | 2 +- 4 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 src/ModelContextProtocol.Core/Authentication/DynamicClientRegistrationDelegate.cs diff --git a/src/ModelContextProtocol.Core/Authentication/ClientOAuthOptions.cs b/src/ModelContextProtocol.Core/Authentication/ClientOAuthOptions.cs index 686316f5..13cb70ce 100644 --- a/src/ModelContextProtocol.Core/Authentication/ClientOAuthOptions.cs +++ b/src/ModelContextProtocol.Core/Authentication/ClientOAuthOptions.cs @@ -52,6 +52,19 @@ public sealed class ClientOAuthOptions /// public AuthorizationRedirectDelegate? AuthorizationRedirectDelegate { 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 DynamicClientRegistrationDelegate? DynamicClientRegistrationDelegate { get; set; } + /// /// Gets or sets the authorization server selector function. /// diff --git a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs index 96356028..2d307344 100644 --- a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs +++ b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs @@ -27,6 +27,7 @@ internal sealed partial class ClientOAuthProvider private readonly IDictionary _additionalAuthorizationParameters; private readonly Func, Uri?> _authServerSelector; private readonly AuthorizationRedirectDelegate _authorizationRedirectDelegate; + private readonly DynamicClientRegistrationDelegate? _dynamicClientRegistrationDelegate; // _clientName and _client URI is used for dynamic client registration (RFC 7591) private readonly string? _clientName; @@ -77,6 +78,9 @@ public ClientOAuthProvider( // Set up authorization URL handler (use default if not provided) _authorizationRedirectDelegate = options.AuthorizationRedirectDelegate ?? DefaultAuthorizationUrlHandler; + + // Set up dynamic client registration delegate + _dynamicClientRegistrationDelegate = options.DynamicClientRegistrationDelegate; } /// @@ -483,6 +487,11 @@ private async Task PerformDynamicClientRegistrationAsync( } LogDynamicClientRegistrationSuccessful(_clientId!); + + if (_dynamicClientRegistrationDelegate is not null) + { + await _dynamicClientRegistrationDelegate(registrationResponse, cancellationToken).ConfigureAwait(false); + } } /// diff --git a/src/ModelContextProtocol.Core/Authentication/DynamicClientRegistrationDelegate.cs b/src/ModelContextProtocol.Core/Authentication/DynamicClientRegistrationDelegate.cs new file mode 100644 index 00000000..a19ffb5d --- /dev/null +++ b/src/ModelContextProtocol.Core/Authentication/DynamicClientRegistrationDelegate.cs @@ -0,0 +1,13 @@ + +namespace ModelContextProtocol.Authentication; + +/// +/// Represents a method that handles the dynamic client registration response. +/// +/// The dynamic client registration response containing the client credentials. +/// The cancellation token. +/// A task that represents the asynchronous operation. +/// +/// The implementation should save the client credentials securely for future use. +/// +public delegate Task DynamicClientRegistrationDelegate(DynamicClientRegistrationResponse response, CancellationToken cancellationToken); \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Authentication/DynamicClientRegistrationResponse.cs b/src/ModelContextProtocol.Core/Authentication/DynamicClientRegistrationResponse.cs index dcd51d68..1dfe1229 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. From febcbf12203c96076db5187a36915220ef878cfd Mon Sep 17 00:00:00 2001 From: Steven Luiten Date: Thu, 10 Jul 2025 15:20:25 +0200 Subject: [PATCH 02/10] Support creating a public client in dynamic client registration. --- .../Authentication/ClientOAuthOptions.cs | 14 ++++++++++++++ .../Authentication/ClientOAuthProvider.cs | 6 ++++-- .../Authentication/OAuthClientType.cs | 17 +++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 src/ModelContextProtocol.Core/Authentication/OAuthClientType.cs diff --git a/src/ModelContextProtocol.Core/Authentication/ClientOAuthOptions.cs b/src/ModelContextProtocol.Core/Authentication/ClientOAuthOptions.cs index 13cb70ce..9524ba7c 100644 --- a/src/ModelContextProtocol.Core/Authentication/ClientOAuthOptions.cs +++ b/src/ModelContextProtocol.Core/Authentication/ClientOAuthOptions.cs @@ -98,6 +98,20 @@ public sealed class ClientOAuthOptions /// public Uri? ClientUri { get; set; } + /// + /// Gets or sets the client type to use during dynamic client registration. + /// + /// + /// + /// This indicates whether the client is confidential (requires a client secret) or public (does not require a client secret). + /// Only used when a is not specified. + /// + /// + /// When not specified, the client type will default to . + /// + /// + public OAuthClientType? ClientType { get; set; } + /// /// Gets or sets additional parameters to include in the query string of the OAuth authorization request /// providing extra information or fulfilling specific requirements of the OAuth provider. diff --git a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs index 2d307344..669840f6 100644 --- a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs +++ b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs @@ -29,9 +29,10 @@ internal sealed partial class ClientOAuthProvider private readonly AuthorizationRedirectDelegate _authorizationRedirectDelegate; private readonly DynamicClientRegistrationDelegate? _dynamicClientRegistrationDelegate; - // _clientName and _client URI is used for dynamic client registration (RFC 7591) + // _clientName, _clientUri, and _clientType is used for dynamic client registration (RFC 7591) private readonly string? _clientName; private readonly Uri? _clientUri; + private readonly OAuthClientType _clientType; private readonly HttpClient _httpClient; private readonly ILogger _logger; @@ -70,6 +71,7 @@ public ClientOAuthProvider( _redirectUri = options.RedirectUri ?? throw new ArgumentException("ClientOAuthOptions.RedirectUri must configured."); _clientName = options.ClientName; _clientUri = options.ClientUri; + _clientType = options.ClientType ?? OAuthClientType.Confidential; _scopes = options.Scopes?.ToArray(); _additionalAuthorizationParameters = options.AdditionalAuthorizationParameters; @@ -446,7 +448,7 @@ private async Task PerformDynamicClientRegistrationAsync( RedirectUris = [_redirectUri.ToString()], GrantTypes = ["authorization_code", "refresh_token"], ResponseTypes = ["code"], - TokenEndpointAuthMethod = "client_secret_post", + TokenEndpointAuthMethod = _clientType == OAuthClientType.Confidential ? "client_secret_post" : "none", ClientName = _clientName, ClientUri = _clientUri?.ToString(), Scope = _scopes is not null ? string.Join(" ", _scopes) : null diff --git a/src/ModelContextProtocol.Core/Authentication/OAuthClientType.cs b/src/ModelContextProtocol.Core/Authentication/OAuthClientType.cs new file mode 100644 index 00000000..60615b78 --- /dev/null +++ b/src/ModelContextProtocol.Core/Authentication/OAuthClientType.cs @@ -0,0 +1,17 @@ +namespace ModelContextProtocol.Authentication; + +/// +/// Represents the type of OAuth client. +/// +public enum OAuthClientType +{ + /// + /// A confidential client, typically a server-side application that can securely store credentials. + /// + Confidential, + + /// + /// A public client, typically a client-side application that cannot securely store credentials. + /// + Public, +} \ No newline at end of file From 5700cbc66c512b65ca2c8896e1fe1f3ed957aca2 Mon Sep 17 00:00:00 2001 From: Steven Luiten Date: Thu, 10 Jul 2025 15:30:26 +0200 Subject: [PATCH 03/10] Support passing an initial access token for dynamic client registration. --- .../AuthenticatingMcpHttpClient.cs | 24 ++----------- .../Authentication/ClientOAuthOptions.cs | 14 ++++++++ .../Authentication/ClientOAuthProvider.cs | 36 +++++++++++++++++++ 3 files changed, 52 insertions(+), 22 deletions(-) diff --git a/src/ModelContextProtocol.Core/Authentication/AuthenticatingMcpHttpClient.cs b/src/ModelContextProtocol.Core/Authentication/AuthenticatingMcpHttpClient.cs index 1cc08189..ba8af9b5 100644 --- a/src/ModelContextProtocol.Core/Authentication/AuthenticatingMcpHttpClient.cs +++ b/src/ModelContextProtocol.Core/Authentication/AuthenticatingMcpHttpClient.cs @@ -1,6 +1,5 @@ using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; -using System.Net.Http.Headers; namespace ModelContextProtocol.Authentication; @@ -20,7 +19,7 @@ internal override async Task SendAsync(HttpRequestMessage r { if (request.Headers.Authorization == null) { - await AddAuthorizationHeaderAsync(request, _currentScheme, cancellationToken).ConfigureAwait(false); + await credentialProvider.AddAuthorizationHeaderAsync(request, _currentScheme, cancellationToken).ConfigureAwait(false); } var response = await base.SendAsync(request, message, cancellationToken).ConfigureAwait(false); @@ -78,7 +77,7 @@ private async Task HandleUnauthorizedResponseAsync( } } - await AddAuthorizationHeaderAsync(retryRequest, _currentScheme, cancellationToken).ConfigureAwait(false); + await credentialProvider.AddAuthorizationHeaderAsync(retryRequest, _currentScheme, cancellationToken).ConfigureAwait(false); return await base.SendAsync(retryRequest, originalJsonRpcMessage, cancellationToken).ConfigureAwait(false); } @@ -96,23 +95,4 @@ private static HashSet ExtractServerSupportedSchemes(HttpResponseMessage return serverSchemes; } - - /// - /// Adds an authorization header to the request. - /// - private async Task AddAuthorizationHeaderAsync(HttpRequestMessage request, string scheme, CancellationToken cancellationToken) - { - if (request.RequestUri is null) - { - return; - } - - var token = await credentialProvider.GetCredentialAsync(scheme, request.RequestUri, cancellationToken).ConfigureAwait(false); - if (string.IsNullOrEmpty(token)) - { - return; - } - - request.Headers.Authorization = new AuthenticationHeaderValue(scheme, token); - } } \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Authentication/ClientOAuthOptions.cs b/src/ModelContextProtocol.Core/Authentication/ClientOAuthOptions.cs index 9524ba7c..421d2006 100644 --- a/src/ModelContextProtocol.Core/Authentication/ClientOAuthOptions.cs +++ b/src/ModelContextProtocol.Core/Authentication/ClientOAuthOptions.cs @@ -112,6 +112,20 @@ public sealed class ClientOAuthOptions /// public OAuthClientType? ClientType { 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. + /// Only used when a is not specified. + /// + /// + /// This is required if the authorization server does not allow anonymous client registration. + /// + /// + public string? InitialAccessToken { get; set; } + /// /// Gets or sets additional parameters to include in the query string of the OAuth authorization request /// providing extra information or fulfilling specific requirements of the OAuth provider. diff --git a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs index 669840f6..5aba1511 100644 --- a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs +++ b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs @@ -2,6 +2,7 @@ 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; @@ -83,6 +84,17 @@ public ClientOAuthProvider( // Set up dynamic client registration delegate _dynamicClientRegistrationDelegate = options.DynamicClientRegistrationDelegate; + + if (options.InitialAccessToken is not null) + { + _token = new() + { + AccessToken = options.InitialAccessToken, + ExpiresIn = 900, + TokenType = BearerScheme, + ObtainedAt = DateTimeOffset.UtcNow, + }; + } } /// @@ -181,6 +193,25 @@ public async Task HandleUnauthorizedResponseAsync( await PerformOAuthAuthorizationAsync(response, cancellationToken).ConfigureAwait(false); } + /// + /// Adds an authorization header to the request. + /// + internal async Task AddAuthorizationHeaderAsync(HttpRequestMessage request, string scheme, CancellationToken cancellationToken) + { + if (request.RequestUri is null) + { + return; + } + + var token = await GetCredentialAsync(scheme, request.RequestUri, cancellationToken).ConfigureAwait(false); + if (string.IsNullOrEmpty(token)) + { + return; + } + + request.Headers.Authorization = new AuthenticationHeaderValue(scheme, token); + } + /// /// Performs OAuth authorization by selecting an appropriate authorization server and completing the OAuth flow. /// @@ -462,6 +493,11 @@ private async Task PerformDynamicClientRegistrationAsync( Content = requestContent }; + if (_token is not null) + { + await AddAuthorizationHeaderAsync(request, _token.TokenType, cancellationToken).ConfigureAwait(false); + } + using var httpResponse = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); if (!httpResponse.IsSuccessStatusCode) From 0ec0e6152f12401357efcada4d80a593187f15dd Mon Sep 17 00:00:00 2001 From: Steven Luiten Date: Fri, 11 Jul 2025 11:33:10 +0200 Subject: [PATCH 04/10] Do not re-use _token for DCR's initial access token. --- .../AuthenticatingMcpHttpClient.cs | 24 +++++++++++- .../Authentication/ClientOAuthProvider.cs | 38 +++---------------- 2 files changed, 27 insertions(+), 35 deletions(-) diff --git a/src/ModelContextProtocol.Core/Authentication/AuthenticatingMcpHttpClient.cs b/src/ModelContextProtocol.Core/Authentication/AuthenticatingMcpHttpClient.cs index ba8af9b5..b809af19 100644 --- a/src/ModelContextProtocol.Core/Authentication/AuthenticatingMcpHttpClient.cs +++ b/src/ModelContextProtocol.Core/Authentication/AuthenticatingMcpHttpClient.cs @@ -1,3 +1,4 @@ +using System.Net.Http.Headers; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; @@ -19,7 +20,7 @@ internal override async Task SendAsync(HttpRequestMessage r { if (request.Headers.Authorization == null) { - await credentialProvider.AddAuthorizationHeaderAsync(request, _currentScheme, cancellationToken).ConfigureAwait(false); + await AddAuthorizationHeaderAsync(request, _currentScheme, cancellationToken).ConfigureAwait(false); } var response = await base.SendAsync(request, message, cancellationToken).ConfigureAwait(false); @@ -77,7 +78,7 @@ private async Task HandleUnauthorizedResponseAsync( } } - await credentialProvider.AddAuthorizationHeaderAsync(retryRequest, _currentScheme, cancellationToken).ConfigureAwait(false); + await AddAuthorizationHeaderAsync(retryRequest, _currentScheme, cancellationToken).ConfigureAwait(false); return await base.SendAsync(retryRequest, originalJsonRpcMessage, cancellationToken).ConfigureAwait(false); } @@ -95,4 +96,23 @@ private static HashSet ExtractServerSupportedSchemes(HttpResponseMessage return serverSchemes; } + + /// + /// Adds an authorization header to the request. + /// + private async Task AddAuthorizationHeaderAsync(HttpRequestMessage request, string scheme, CancellationToken cancellationToken) + { + if (request.RequestUri is null) + { + return; + } + + var token = await credentialProvider.GetCredentialAsync(scheme, request.RequestUri, cancellationToken).ConfigureAwait(false); + if (string.IsNullOrEmpty(token)) + { + return; + } + + request.Headers.Authorization = new AuthenticationHeaderValue(scheme, token); + } } \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs index 5aba1511..43cd31e5 100644 --- a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs +++ b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs @@ -30,10 +30,11 @@ internal sealed partial class ClientOAuthProvider private readonly AuthorizationRedirectDelegate _authorizationRedirectDelegate; private readonly DynamicClientRegistrationDelegate? _dynamicClientRegistrationDelegate; - // _clientName, _clientUri, and _clientType is used for dynamic client registration (RFC 7591) + // _clientName, _clientUri, _clientType, and _initialAccessToken is used for dynamic client registration (RFC 7591) private readonly string? _clientName; private readonly Uri? _clientUri; private readonly OAuthClientType _clientType; + private readonly string? _initialAccessToken; private readonly HttpClient _httpClient; private readonly ILogger _logger; @@ -73,6 +74,7 @@ public ClientOAuthProvider( _clientName = options.ClientName; _clientUri = options.ClientUri; _clientType = options.ClientType ?? OAuthClientType.Confidential; + _initialAccessToken = options.InitialAccessToken; _scopes = options.Scopes?.ToArray(); _additionalAuthorizationParameters = options.AdditionalAuthorizationParameters; @@ -84,17 +86,6 @@ public ClientOAuthProvider( // Set up dynamic client registration delegate _dynamicClientRegistrationDelegate = options.DynamicClientRegistrationDelegate; - - if (options.InitialAccessToken is not null) - { - _token = new() - { - AccessToken = options.InitialAccessToken, - ExpiresIn = 900, - TokenType = BearerScheme, - ObtainedAt = DateTimeOffset.UtcNow, - }; - } } /// @@ -193,25 +184,6 @@ public async Task HandleUnauthorizedResponseAsync( await PerformOAuthAuthorizationAsync(response, cancellationToken).ConfigureAwait(false); } - /// - /// Adds an authorization header to the request. - /// - internal async Task AddAuthorizationHeaderAsync(HttpRequestMessage request, string scheme, CancellationToken cancellationToken) - { - if (request.RequestUri is null) - { - return; - } - - var token = await GetCredentialAsync(scheme, request.RequestUri, cancellationToken).ConfigureAwait(false); - if (string.IsNullOrEmpty(token)) - { - return; - } - - request.Headers.Authorization = new AuthenticationHeaderValue(scheme, token); - } - /// /// Performs OAuth authorization by selecting an appropriate authorization server and completing the OAuth flow. /// @@ -493,9 +465,9 @@ private async Task PerformDynamicClientRegistrationAsync( Content = requestContent }; - if (_token is not null) + if (!string.IsNullOrEmpty(_initialAccessToken)) { - await AddAuthorizationHeaderAsync(request, _token.TokenType, cancellationToken).ConfigureAwait(false); + request.Headers.Authorization = new AuthenticationHeaderValue(BearerScheme, _initialAccessToken); } using var httpResponse = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); From 210b6f218e844338f60b9e3a40b0b367b1b2b288 Mon Sep 17 00:00:00 2001 From: Steven Luiten Date: Fri, 11 Jul 2025 11:36:14 +0200 Subject: [PATCH 05/10] Rename. --- .../Authentication/ClientOAuthProvider.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs index 43cd31e5..ef76c6c3 100644 --- a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs +++ b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs @@ -33,7 +33,7 @@ internal sealed partial class ClientOAuthProvider // _clientName, _clientUri, _clientType, and _initialAccessToken is used for dynamic client registration (RFC 7591) private readonly string? _clientName; private readonly Uri? _clientUri; - private readonly OAuthClientType _clientType; + private readonly OAuthClientType _dcrRequestedAuthMethod; private readonly string? _initialAccessToken; private readonly HttpClient _httpClient; @@ -73,7 +73,7 @@ public ClientOAuthProvider( _redirectUri = options.RedirectUri ?? throw new ArgumentException("ClientOAuthOptions.RedirectUri must configured."); _clientName = options.ClientName; _clientUri = options.ClientUri; - _clientType = options.ClientType ?? OAuthClientType.Confidential; + _dcrRequestedAuthMethod = options.ClientType ?? OAuthClientType.Confidential; _initialAccessToken = options.InitialAccessToken; _scopes = options.Scopes?.ToArray(); _additionalAuthorizationParameters = options.AdditionalAuthorizationParameters; @@ -451,7 +451,7 @@ private async Task PerformDynamicClientRegistrationAsync( RedirectUris = [_redirectUri.ToString()], GrantTypes = ["authorization_code", "refresh_token"], ResponseTypes = ["code"], - TokenEndpointAuthMethod = _clientType == OAuthClientType.Confidential ? "client_secret_post" : "none", + TokenEndpointAuthMethod = _dcrRequestedAuthMethod == OAuthClientType.Confidential ? "client_secret_post" : "none", ClientName = _clientName, ClientUri = _clientUri?.ToString(), Scope = _scopes is not null ? string.Join(" ", _scopes) : null From cf152125bede2834f6529938c2e043e2230ef22d Mon Sep 17 00:00:00 2001 From: Steven Luiten Date: Mon, 14 Jul 2025 09:36:02 +0200 Subject: [PATCH 06/10] Move DCR-related properties to new class DynamicClientRegistrationOptions. --- samples/ProtectedMCPClient/Program.cs | 5 +- .../Authentication/ClientOAuthOptions.cs | 55 +--------------- .../Authentication/ClientOAuthProvider.cs | 21 +++++-- .../DynamicClientRegistrationOptions.cs | 62 +++++++++++++++++++ .../AuthEventTests.cs | 7 ++- .../AuthTests.cs | 9 ++- 6 files changed, 94 insertions(+), 65 deletions(-) create mode 100644 src/ModelContextProtocol.Core/Authentication/DynamicClientRegistrationOptions.cs diff --git a/samples/ProtectedMCPClient/Program.cs b/samples/ProtectedMCPClient/Program.cs index 516227b3..5871284a 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 421d2006..cc6a8952 100644 --- a/src/ModelContextProtocol.Core/Authentication/ClientOAuthOptions.cs +++ b/src/ModelContextProtocol.Core/Authentication/ClientOAuthOptions.cs @@ -52,19 +52,6 @@ public sealed class ClientOAuthOptions /// public AuthorizationRedirectDelegate? AuthorizationRedirectDelegate { 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 DynamicClientRegistrationDelegate? DynamicClientRegistrationDelegate { get; set; } - /// /// Gets or sets the authorization server selector function. /// @@ -81,50 +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; } - - /// - /// Gets or sets the client type to use during dynamic client registration. - /// - /// - /// - /// This indicates whether the client is confidential (requires a client secret) or public (does not require a client secret). - /// Only used when a is not specified. - /// - /// - /// When not specified, the client type will default to . - /// - /// - public OAuthClientType? ClientType { 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. - /// Only used when a is not specified. - /// - /// - /// This is required if the authorization server does not allow anonymous client registration. - /// - /// - public string? InitialAccessToken { 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 ef76c6c3..4d6f4855 100644 --- a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs +++ b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs @@ -71,10 +71,6 @@ public ClientOAuthProvider( _clientId = options.ClientId; _clientSecret = options.ClientSecret; _redirectUri = options.RedirectUri ?? throw new ArgumentException("ClientOAuthOptions.RedirectUri must configured."); - _clientName = options.ClientName; - _clientUri = options.ClientUri; - _dcrRequestedAuthMethod = options.ClientType ?? OAuthClientType.Confidential; - _initialAccessToken = options.InitialAccessToken; _scopes = options.Scopes?.ToArray(); _additionalAuthorizationParameters = options.AdditionalAuthorizationParameters; @@ -84,8 +80,21 @@ public ClientOAuthProvider( // Set up authorization URL handler (use default if not provided) _authorizationRedirectDelegate = options.AuthorizationRedirectDelegate ?? DefaultAuthorizationUrlHandler; - // Set up dynamic client registration delegate - _dynamicClientRegistrationDelegate = options.DynamicClientRegistrationDelegate; + if (string.IsNullOrEmpty(_clientId)) + { + if (options.DynamicClientRegistration is null) + { + throw new ArgumentException("ClientOAuthOptions.DynamicClientRegistration must be configured when ClientId is not set."); + } + + _clientName = options.DynamicClientRegistration.ClientName; + _clientUri = options.DynamicClientRegistration.ClientUri; + _dcrRequestedAuthMethod = options.DynamicClientRegistration.ClientType; + _initialAccessToken = options.DynamicClientRegistration.InitialAccessToken; + + // Set up dynamic client registration delegate + _dynamicClientRegistrationDelegate = options.DynamicClientRegistration.DynamicClientRegistrationDelegate; + } } /// diff --git a/src/ModelContextProtocol.Core/Authentication/DynamicClientRegistrationOptions.cs b/src/ModelContextProtocol.Core/Authentication/DynamicClientRegistrationOptions.cs new file mode 100644 index 00000000..bb5e5685 --- /dev/null +++ b/src/ModelContextProtocol.Core/Authentication/DynamicClientRegistrationOptions.cs @@ -0,0 +1,62 @@ +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 required 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 client type to use during dynamic client registration. + /// + /// + /// + /// This indicates whether the client is confidential (requires a client secret) or public (does not require a client secret). + /// + /// + /// The default value is . + /// + /// + public OAuthClientType ClientType { get; set; } = OAuthClientType.Confidential; + + /// + /// 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 DynamicClientRegistrationDelegate? DynamicClientRegistrationDelegate { get; set; } +} \ No newline at end of file diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/AuthEventTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/AuthEventTests.cs index 6a48c21d..e562550d 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/AuthEventTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/AuthEventTests.cs @@ -148,9 +148,12 @@ 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"), + }, }, }, HttpClient, diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/AuthTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/AuthTests.cs index 2252b1b7..b480934a 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); From b84a95419511aff18bc5420b483272ed99b0fe81 Mon Sep 17 00:00:00 2001 From: Steven Luiten Date: Mon, 14 Jul 2025 09:41:59 +0200 Subject: [PATCH 07/10] Remove OAuthClientType. --- .../Authentication/ClientOAuthProvider.cs | 6 ++---- .../DynamicClientRegistrationOptions.cs | 13 ------------- .../Authentication/OAuthClientType.cs | 17 ----------------- 3 files changed, 2 insertions(+), 34 deletions(-) delete mode 100644 src/ModelContextProtocol.Core/Authentication/OAuthClientType.cs diff --git a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs index 4d6f4855..1daf1340 100644 --- a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs +++ b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs @@ -30,10 +30,9 @@ internal sealed partial class ClientOAuthProvider private readonly AuthorizationRedirectDelegate _authorizationRedirectDelegate; private readonly DynamicClientRegistrationDelegate? _dynamicClientRegistrationDelegate; - // _clientName, _clientUri, _clientType, and _initialAccessToken is used for dynamic client registration (RFC 7591) + // _clientName, _clientUri, and _initialAccessToken is used for dynamic client registration (RFC 7591) private readonly string? _clientName; private readonly Uri? _clientUri; - private readonly OAuthClientType _dcrRequestedAuthMethod; private readonly string? _initialAccessToken; private readonly HttpClient _httpClient; @@ -89,7 +88,6 @@ public ClientOAuthProvider( _clientName = options.DynamicClientRegistration.ClientName; _clientUri = options.DynamicClientRegistration.ClientUri; - _dcrRequestedAuthMethod = options.DynamicClientRegistration.ClientType; _initialAccessToken = options.DynamicClientRegistration.InitialAccessToken; // Set up dynamic client registration delegate @@ -460,7 +458,7 @@ private async Task PerformDynamicClientRegistrationAsync( RedirectUris = [_redirectUri.ToString()], GrantTypes = ["authorization_code", "refresh_token"], ResponseTypes = ["code"], - TokenEndpointAuthMethod = _dcrRequestedAuthMethod == OAuthClientType.Confidential ? "client_secret_post" : "none", + TokenEndpointAuthMethod = "client_secret_post", ClientName = _clientName, ClientUri = _clientUri?.ToString(), Scope = _scopes is not null ? string.Join(" ", _scopes) : null diff --git a/src/ModelContextProtocol.Core/Authentication/DynamicClientRegistrationOptions.cs b/src/ModelContextProtocol.Core/Authentication/DynamicClientRegistrationOptions.cs index bb5e5685..ca687f36 100644 --- a/src/ModelContextProtocol.Core/Authentication/DynamicClientRegistrationOptions.cs +++ b/src/ModelContextProtocol.Core/Authentication/DynamicClientRegistrationOptions.cs @@ -21,19 +21,6 @@ public sealed class DynamicClientRegistrationOptions /// public Uri? ClientUri { get; set; } - /// - /// Gets or sets the client type to use during dynamic client registration. - /// - /// - /// - /// This indicates whether the client is confidential (requires a client secret) or public (does not require a client secret). - /// - /// - /// The default value is . - /// - /// - public OAuthClientType ClientType { get; set; } = OAuthClientType.Confidential; - /// /// Gets or sets the initial access token to use during dynamic client registration. /// diff --git a/src/ModelContextProtocol.Core/Authentication/OAuthClientType.cs b/src/ModelContextProtocol.Core/Authentication/OAuthClientType.cs deleted file mode 100644 index 60615b78..00000000 --- a/src/ModelContextProtocol.Core/Authentication/OAuthClientType.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace ModelContextProtocol.Authentication; - -/// -/// Represents the type of OAuth client. -/// -public enum OAuthClientType -{ - /// - /// A confidential client, typically a server-side application that can securely store credentials. - /// - Confidential, - - /// - /// A public client, typically a client-side application that cannot securely store credentials. - /// - Public, -} \ No newline at end of file From 6a301fe4ff68291f41f6690d7129e1e109fc5d52 Mon Sep 17 00:00:00 2001 From: Steven Luiten Date: Mon, 14 Jul 2025 09:48:57 +0200 Subject: [PATCH 08/10] Revert usings order. --- .../Authentication/AuthenticatingMcpHttpClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ModelContextProtocol.Core/Authentication/AuthenticatingMcpHttpClient.cs b/src/ModelContextProtocol.Core/Authentication/AuthenticatingMcpHttpClient.cs index b809af19..1cc08189 100644 --- a/src/ModelContextProtocol.Core/Authentication/AuthenticatingMcpHttpClient.cs +++ b/src/ModelContextProtocol.Core/Authentication/AuthenticatingMcpHttpClient.cs @@ -1,6 +1,6 @@ -using System.Net.Http.Headers; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; +using System.Net.Http.Headers; namespace ModelContextProtocol.Authentication; From 5816a68c592fbe9c2da1770db250265338ff87b4 Mon Sep 17 00:00:00 2001 From: Steven Luiten Date: Mon, 14 Jul 2025 10:09:05 +0200 Subject: [PATCH 09/10] Test DynamicClientRegistrationDelegate. --- .../AuthEventTests.cs | 11 +++++++++++ .../AuthTests.cs | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/AuthEventTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/AuthEventTests.cs index e562550d..782b70db 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() { @@ -153,6 +155,11 @@ public async Task CanAuthenticate_WithDynamicClientRegistration_FromEvent() { ClientName = "Test MCP Client", ClientUri = new Uri("https://example.com"), + DynamicClientRegistrationDelegate = (response, cancellationToken) => + { + dcrResponse = response; + return Task.CompletedTask; + }, }, }, }, @@ -165,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 b480934a..f91904c6 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/AuthTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/AuthTests.cs @@ -174,6 +174,8 @@ public async Task CanAuthenticate_WithDynamicClientRegistration() await app.StartAsync(TestContext.Current.CancellationToken); + DynamicClientRegistrationResponse? dcrResponse = null; + await using var transport = new SseClientTransport(new() { Endpoint = new(McpServerUrl), @@ -186,12 +188,21 @@ public async Task CanAuthenticate_WithDynamicClientRegistration() { ClientName = "Test MCP Client", ClientUri = new Uri("https://example.com"), + DynamicClientRegistrationDelegate = (response, cancellationToken) => + { + dcrResponse = response; + return Task.CompletedTask; + }, }, }, }, HttpClient, LoggerFactory); await using var client = await McpClientFactory.CreateAsync( transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken); + + Assert.NotNull(dcrResponse); + Assert.False(string.IsNullOrEmpty(dcrResponse.ClientId)); + Assert.False(string.IsNullOrEmpty(dcrResponse.ClientSecret)); } [Fact] From d9fb5cb97c859bfa5b0d09fc19f4447bd40a4056 Mon Sep 17 00:00:00 2001 From: Steven Luiten Date: Mon, 14 Jul 2025 10:09:29 +0200 Subject: [PATCH 10/10] Add paramName to exceptions. --- .../Authentication/ClientOAuthProvider.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs index 1daf1340..46b5e215 100644 --- a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs +++ b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs @@ -69,7 +69,7 @@ public ClientOAuthProvider( _clientId = options.ClientId; _clientSecret = options.ClientSecret; - _redirectUri = options.RedirectUri ?? throw new ArgumentException("ClientOAuthOptions.RedirectUri must configured."); + _redirectUri = options.RedirectUri ?? throw new ArgumentException("ClientOAuthOptions.RedirectUri must configured.", nameof(options)); _scopes = options.Scopes?.ToArray(); _additionalAuthorizationParameters = options.AdditionalAuthorizationParameters; @@ -83,7 +83,7 @@ public ClientOAuthProvider( { if (options.DynamicClientRegistration is null) { - throw new ArgumentException("ClientOAuthOptions.DynamicClientRegistration must be configured when ClientId is not set."); + throw new ArgumentException("ClientOAuthOptions.DynamicClientRegistration must be configured when ClientId is not set.", nameof(options)); } _clientName = options.DynamicClientRegistration.ClientName;