Skip to content

Commit f9129f5

Browse files
authored
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.
1 parent 2fa658e commit f9129f5

File tree

7 files changed

+102
-28
lines changed

7 files changed

+102
-28
lines changed

samples/ProtectedMcpClient/Program.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,12 @@
3131
Name = "Secure Weather Client",
3232
OAuth = new()
3333
{
34-
ClientName = "ProtectedMcpClient",
3534
RedirectUri = new Uri("http://localhost:1179/callback"),
3635
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
36+
DynamicClientRegistration = new()
37+
{
38+
ClientName = "ProtectedMcpClient",
39+
},
3740
}
3841
}, httpClient, consoleLoggerFactory);
3942

src/ModelContextProtocol.Core/Authentication/ClientOAuthOptions.cs

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -68,22 +68,12 @@ public sealed class ClientOAuthOptions
6868
public Func<IReadOnlyList<Uri>, Uri?>? AuthServerSelector { get; set; }
6969

7070
/// <summary>
71-
/// Gets or sets the client name to use during dynamic client registration.
71+
/// Gets or sets the options to use during dynamic client registration.
7272
/// </summary>
7373
/// <remarks>
74-
/// This is a human-readable name for the client that may be displayed to users during authorization.
7574
/// Only used when a <see cref="ClientId"/> is not specified.
7675
/// </remarks>
77-
public string? ClientName { get; set; }
78-
79-
/// <summary>
80-
/// Gets or sets the client URI to use during dynamic client registration.
81-
/// </summary>
82-
/// <remarks>
83-
/// This should be a URL pointing to the client's home page or information page.
84-
/// Only used when a <see cref="ClientId"/> is not specified.
85-
/// </remarks>
86-
public Uri? ClientUri { get; set; }
76+
public DynamicClientRegistrationOptions? DynamicClientRegistration { get; set; }
8777

8878
/// <summary>
8979
/// Gets or sets additional parameters to include in the query string of the OAuth authorization request

src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
using Microsoft.Extensions.Logging;
22
using Microsoft.Extensions.Logging.Abstractions;
3-
using System.Collections.Specialized;
43
using System.Diagnostics.CodeAnalysis;
4+
using System.Net.Http.Headers;
55
using System.Security.Cryptography;
66
using System.Text;
77
using System.Text.Json;
@@ -28,9 +28,11 @@ internal sealed partial class ClientOAuthProvider
2828
private readonly Func<IReadOnlyList<Uri>, Uri?> _authServerSelector;
2929
private readonly AuthorizationRedirectDelegate _authorizationRedirectDelegate;
3030

31-
// _clientName and _client URI is used for dynamic client registration (RFC 7591)
32-
private readonly string? _clientName;
33-
private readonly Uri? _clientUri;
31+
// _dcrClientName, _dcrClientUri, _dcrInitialAccessToken and _dcrResponseDelegate are used for dynamic client registration (RFC 7591)
32+
private readonly string? _dcrClientName;
33+
private readonly Uri? _dcrClientUri;
34+
private readonly string? _dcrInitialAccessToken;
35+
private readonly Func<DynamicClientRegistrationResponse, CancellationToken, Task>? _dcrResponseDelegate;
3436

3537
private readonly HttpClient _httpClient;
3638
private readonly ILogger _logger;
@@ -66,9 +68,7 @@ public ClientOAuthProvider(
6668

6769
_clientId = options.ClientId;
6870
_clientSecret = options.ClientSecret;
69-
_redirectUri = options.RedirectUri ?? throw new ArgumentException("ClientOAuthOptions.RedirectUri must configured.");
70-
_clientName = options.ClientName;
71-
_clientUri = options.ClientUri;
71+
_redirectUri = options.RedirectUri ?? throw new ArgumentException("ClientOAuthOptions.RedirectUri must configured.", nameof(options));
7272
_scopes = options.Scopes?.ToArray();
7373
_additionalAuthorizationParameters = options.AdditionalAuthorizationParameters;
7474

@@ -77,6 +77,11 @@ public ClientOAuthProvider(
7777

7878
// Set up authorization URL handler (use default if not provided)
7979
_authorizationRedirectDelegate = options.AuthorizationRedirectDelegate ?? DefaultAuthorizationUrlHandler;
80+
81+
_dcrClientName = options.DynamicClientRegistration?.ClientName;
82+
_dcrClientUri = options.DynamicClientRegistration?.ClientUri;
83+
_dcrInitialAccessToken = options.DynamicClientRegistration?.InitialAccessToken;
84+
_dcrResponseDelegate = options.DynamicClientRegistration?.ResponseDelegate;
8085
}
8186

8287
/// <summary>
@@ -447,8 +452,8 @@ private async Task PerformDynamicClientRegistrationAsync(
447452
GrantTypes = ["authorization_code", "refresh_token"],
448453
ResponseTypes = ["code"],
449454
TokenEndpointAuthMethod = "client_secret_post",
450-
ClientName = _clientName,
451-
ClientUri = _clientUri?.ToString(),
455+
ClientName = _dcrClientName,
456+
ClientUri = _dcrClientUri?.ToString(),
452457
Scope = _scopes is not null ? string.Join(" ", _scopes) : null
453458
};
454459

@@ -460,6 +465,11 @@ private async Task PerformDynamicClientRegistrationAsync(
460465
Content = requestContent
461466
};
462467

468+
if (!string.IsNullOrEmpty(_dcrInitialAccessToken))
469+
{
470+
request.Headers.Authorization = new AuthenticationHeaderValue(BearerScheme, _dcrInitialAccessToken);
471+
}
472+
463473
using var httpResponse = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
464474

465475
if (!httpResponse.IsSuccessStatusCode)
@@ -487,6 +497,11 @@ private async Task PerformDynamicClientRegistrationAsync(
487497
}
488498

489499
LogDynamicClientRegistrationSuccessful(_clientId!);
500+
501+
if (_dcrResponseDelegate is not null)
502+
{
503+
await _dcrResponseDelegate(registrationResponse, cancellationToken).ConfigureAwait(false);
504+
}
490505
}
491506

492507
/// <summary>
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
namespace ModelContextProtocol.Authentication;
2+
3+
/// <summary>
4+
/// Provides configuration options for the <see cref="ClientOAuthProvider"/> related to dynamic client registration (RFC 7591).
5+
/// </summary>
6+
public sealed class DynamicClientRegistrationOptions
7+
{
8+
/// <summary>
9+
/// Gets or sets the client name to use during dynamic client registration.
10+
/// </summary>
11+
/// <remarks>
12+
/// This is a human-readable name for the client that may be displayed to users during authorization.
13+
/// </remarks>
14+
public string? ClientName { get; set; }
15+
16+
/// <summary>
17+
/// Gets or sets the client URI to use during dynamic client registration.
18+
/// </summary>
19+
/// <remarks>
20+
/// This should be a URL pointing to the client's home page or information page.
21+
/// </remarks>
22+
public Uri? ClientUri { get; set; }
23+
24+
/// <summary>
25+
/// Gets or sets the initial access token to use during dynamic client registration.
26+
/// </summary>
27+
/// <remarks>
28+
/// <para>
29+
/// This token is used to authenticate the client during the registration process.
30+
/// </para>
31+
/// <para>
32+
/// This is required if the authorization server does not allow anonymous client registration.
33+
/// </para>
34+
/// </remarks>
35+
public string? InitialAccessToken { get; set; }
36+
37+
/// <summary>
38+
/// Gets or sets the delegate used for handling the dynamic client registration response.
39+
/// </summary>
40+
/// <remarks>
41+
/// <para>
42+
/// This delegate is responsible for processing the response from the dynamic client registration endpoint.
43+
/// </para>
44+
/// <para>
45+
/// The implementation should save the client credentials securely for future use.
46+
/// </para>
47+
/// </remarks>
48+
public Func<DynamicClientRegistrationResponse, CancellationToken, Task>? ResponseDelegate { get; set; }
49+
}

src/ModelContextProtocol.Core/Authentication/DynamicClientRegistrationResponse.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace ModelContextProtocol.Authentication;
55
/// <summary>
66
/// Represents a client registration response for OAuth 2.0 Dynamic Client Registration (RFC 7591).
77
/// </summary>
8-
internal sealed class DynamicClientRegistrationResponse
8+
public sealed class DynamicClientRegistrationResponse
99
{
1010
/// <summary>
1111
/// Gets or sets the client identifier.

tests/ModelContextProtocol.AspNetCore.Tests/AuthEventTests.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ public async Task CanAuthenticate_WithDynamicClientRegistration_FromEvent()
140140

141141
await app.StartAsync(TestContext.Current.CancellationToken);
142142

143+
DynamicClientRegistrationResponse? dcrResponse = null;
144+
143145
await using var transport = new SseClientTransport(
144146
new()
145147
{
@@ -148,9 +150,17 @@ public async Task CanAuthenticate_WithDynamicClientRegistration_FromEvent()
148150
{
149151
RedirectUri = new Uri("http://localhost:1179/callback"),
150152
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
151-
ClientName = "Test MCP Client",
152-
ClientUri = new Uri("https://example.com"),
153153
Scopes = ["mcp:tools"],
154+
DynamicClientRegistration = new()
155+
{
156+
ClientName = "Test MCP Client",
157+
ClientUri = new Uri("https://example.com"),
158+
ResponseDelegate = (response, cancellationToken) =>
159+
{
160+
dcrResponse = response;
161+
return Task.CompletedTask;
162+
},
163+
},
154164
},
155165
},
156166
HttpClient,
@@ -162,6 +172,10 @@ public async Task CanAuthenticate_WithDynamicClientRegistration_FromEvent()
162172
loggerFactory: LoggerFactory,
163173
cancellationToken: TestContext.Current.CancellationToken
164174
);
175+
176+
Assert.NotNull(dcrResponse);
177+
Assert.False(string.IsNullOrEmpty(dcrResponse.ClientId));
178+
Assert.False(string.IsNullOrEmpty(dcrResponse.ClientSecret));
165179
}
166180

167181
[Fact]

tests/ModelContextProtocol.AspNetCore.Tests/AuthTests.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,12 @@ public async Task CanAuthenticate_WithDynamicClientRegistration()
181181
{
182182
RedirectUri = new Uri("http://localhost:1179/callback"),
183183
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
184-
ClientName = "Test MCP Client",
185-
ClientUri = new Uri("https://example.com"),
186-
Scopes = ["mcp:tools"]
184+
Scopes = ["mcp:tools"],
185+
DynamicClientRegistration = new()
186+
{
187+
ClientName = "Test MCP Client",
188+
ClientUri = new Uri("https://example.com"),
189+
},
187190
},
188191
}, HttpClient, LoggerFactory);
189192

0 commit comments

Comments
 (0)