Skip to content
Merged
5 changes: 4 additions & 1 deletion samples/ProtectedMcpClient/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
14 changes: 2 additions & 12 deletions src/ModelContextProtocol.Core/Authentication/ClientOAuthOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;

Expand All @@ -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;
}

/// <summary>
Expand Down Expand Up @@ -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
};

Expand All @@ -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)
Expand Down Expand Up @@ -487,6 +497,11 @@ private async Task PerformDynamicClientRegistrationAsync(
}

LogDynamicClientRegistrationSuccessful(_clientId!);

if (_dcrResponseDelegate is not null)
{
await _dcrResponseDelegate(registrationResponse, cancellationToken).ConfigureAwait(false);
}
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -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; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
18 changes: 16 additions & 2 deletions tests/ModelContextProtocol.AspNetCore.Tests/AuthEventTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand All @@ -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,
Expand All @@ -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]
Expand Down
9 changes: 6 additions & 3 deletions tests/ModelContextProtocol.AspNetCore.Tests/AuthTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down