Add support for Client ID Metadata Documents to enable URL-based client registration (#1023)
This commit is contained in:
parent
7f25ae9d2a
commit
5a65af8ed7
8 changed files with 202 additions and 11 deletions
|
|
@ -66,4 +66,10 @@ internal sealed class AuthorizationServerMetadata
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("scopes_supported")]
|
[JsonPropertyName("scopes_supported")]
|
||||||
public List<string>? ScopesSupported { get; set; }
|
public List<string>? ScopesSupported { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates if the server supports OAuth Client ID Metadata Documents.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("client_id_metadata_document_supported")]
|
||||||
|
public bool ClientIdMetadataDocumentSupported { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,17 @@ public sealed class ClientOAuthOptions
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public string? ClientSecret { get; set; }
|
public string? ClientSecret { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the HTTPS URL pointing to this client's metadata document.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// When specified, and when the authorization server metadata reports
|
||||||
|
/// <c>client_id_metadata_document_supported = true</c>, the OAuth client will respond to
|
||||||
|
/// challenges by sending this URL as the client identifier instead of performing dynamic
|
||||||
|
/// client registration.
|
||||||
|
/// </remarks>
|
||||||
|
public Uri? ClientMetadataDocumentUri { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the OAuth scopes to request.
|
/// Gets or sets the OAuth scopes to request.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ internal sealed partial class ClientOAuthProvider
|
||||||
private readonly IDictionary<string, string> _additionalAuthorizationParameters;
|
private readonly IDictionary<string, string> _additionalAuthorizationParameters;
|
||||||
private readonly Func<IReadOnlyList<Uri>, Uri?> _authServerSelector;
|
private readonly Func<IReadOnlyList<Uri>, Uri?> _authServerSelector;
|
||||||
private readonly AuthorizationRedirectDelegate _authorizationRedirectDelegate;
|
private readonly AuthorizationRedirectDelegate _authorizationRedirectDelegate;
|
||||||
|
private readonly Uri? _clientMetadataDocumentUri;
|
||||||
|
|
||||||
// _dcrClientName, _dcrClientUri, _dcrInitialAccessToken and _dcrResponseDelegate are used for dynamic client registration (RFC 7591)
|
// _dcrClientName, _dcrClientUri, _dcrInitialAccessToken and _dcrResponseDelegate are used for dynamic client registration (RFC 7591)
|
||||||
private readonly string? _dcrClientName;
|
private readonly string? _dcrClientName;
|
||||||
|
|
@ -74,6 +75,7 @@ internal sealed partial class ClientOAuthProvider
|
||||||
_redirectUri = options.RedirectUri ?? throw new ArgumentException("ClientOAuthOptions.RedirectUri must configured.", nameof(options));
|
_redirectUri = options.RedirectUri ?? throw new ArgumentException("ClientOAuthOptions.RedirectUri must configured.", nameof(options));
|
||||||
_scopes = options.Scopes?.ToArray();
|
_scopes = options.Scopes?.ToArray();
|
||||||
_additionalAuthorizationParameters = options.AdditionalAuthorizationParameters;
|
_additionalAuthorizationParameters = options.AdditionalAuthorizationParameters;
|
||||||
|
_clientMetadataDocumentUri = options.ClientMetadataDocumentUri;
|
||||||
|
|
||||||
// Set up authorization server selection strategy
|
// Set up authorization server selection strategy
|
||||||
_authServerSelector = options.AuthServerSelector ?? DefaultAuthServerSelector;
|
_authServerSelector = options.AuthServerSelector ?? DefaultAuthServerSelector;
|
||||||
|
|
@ -221,7 +223,7 @@ internal sealed partial class ClientOAuthProvider
|
||||||
_authServerMetadata = authServerMetadata;
|
_authServerMetadata = authServerMetadata;
|
||||||
|
|
||||||
// The existing access token must be invalid to have resulted in a 401 response, but refresh might still work.
|
// The existing access token must be invalid to have resulted in a 401 response, but refresh might still work.
|
||||||
if (await _tokenCache.GetTokensAsync(cancellationToken).ConfigureAwait(false) is { RefreshToken: {} refreshToken })
|
if (await _tokenCache.GetTokensAsync(cancellationToken).ConfigureAwait(false) is { RefreshToken: { } refreshToken })
|
||||||
{
|
{
|
||||||
var refreshedTokens = await RefreshTokenAsync(refreshToken, protectedResourceMetadata.Resource, authServerMetadata, cancellationToken).ConfigureAwait(false);
|
var refreshedTokens = await RefreshTokenAsync(refreshToken, protectedResourceMetadata.Resource, authServerMetadata, cancellationToken).ConfigureAwait(false);
|
||||||
if (refreshedTokens is not null)
|
if (refreshedTokens is not null)
|
||||||
|
|
@ -231,10 +233,18 @@ internal sealed partial class ClientOAuthProvider
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform dynamic client registration if needed
|
// Assign a client ID if necessary
|
||||||
if (string.IsNullOrEmpty(_clientId))
|
if (string.IsNullOrEmpty(_clientId))
|
||||||
{
|
{
|
||||||
await PerformDynamicClientRegistrationAsync(authServerMetadata, cancellationToken).ConfigureAwait(false);
|
// Try using a client metadata document before falling back to dynamic client registration
|
||||||
|
if (authServerMetadata.ClientIdMetadataDocumentSupported && _clientMetadataDocumentUri is not null)
|
||||||
|
{
|
||||||
|
ApplyClientIdMetadataDocument(_clientMetadataDocumentUri);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await PerformDynamicClientRegistrationAsync(authServerMetadata, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform the OAuth flow
|
// Perform the OAuth flow
|
||||||
|
|
@ -243,6 +253,23 @@ internal sealed partial class ClientOAuthProvider
|
||||||
LogOAuthAuthorizationCompleted();
|
LogOAuthAuthorizationCompleted();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ApplyClientIdMetadataDocument(Uri metadataUri)
|
||||||
|
{
|
||||||
|
if (!IsValidClientMetadataDocumentUri(metadataUri))
|
||||||
|
{
|
||||||
|
ThrowFailedToHandleUnauthorizedResponse(
|
||||||
|
$"{nameof(ClientOAuthOptions.ClientMetadataDocumentUri)} must be an HTTPS URL with a non-root absolute path. Value: '{metadataUri}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_clientId = metadataUri.AbsoluteUri;
|
||||||
|
|
||||||
|
// See: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-client-id-metadata-document-00#section-3
|
||||||
|
static bool IsValidClientMetadataDocumentUri(Uri uri)
|
||||||
|
=> uri.IsAbsoluteUri
|
||||||
|
&& string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& uri.AbsolutePath.Length > 1; // AbsolutePath always starts with "/"
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<AuthorizationServerMetadata> GetAuthServerMetadataAsync(Uri authServerUri, CancellationToken cancellationToken)
|
private async Task<AuthorizationServerMetadata> GetAuthServerMetadataAsync(Uri authServerUri, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (authServerUri.OriginalString.Length == 0 ||
|
if (authServerUri.OriginalString.Length == 0 ||
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,10 @@ namespace ModelContextProtocol.AspNetCore.Tests.OAuth;
|
||||||
|
|
||||||
public class AuthTests : OAuthTestBase
|
public class AuthTests : OAuthTestBase
|
||||||
{
|
{
|
||||||
public AuthTests(ITestOutputHelper outputHelper)
|
private const string ClientMetadataDocumentUrl = $"{OAuthServerUrl}/client-metadata/cimd-client.json";
|
||||||
: base(outputHelper)
|
|
||||||
|
public AuthTests(ITestOutputHelper outputHelper)
|
||||||
|
: base(outputHelper)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -98,6 +100,105 @@ public class AuthTests : OAuthTestBase
|
||||||
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
|
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CanAuthenticate_WithClientMetadataDocument()
|
||||||
|
{
|
||||||
|
await using var app = await StartMcpServerAsync();
|
||||||
|
|
||||||
|
await using var transport = new HttpClientTransport(new()
|
||||||
|
{
|
||||||
|
Endpoint = new(McpServerUrl),
|
||||||
|
OAuth = new ClientOAuthOptions()
|
||||||
|
{
|
||||||
|
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||||
|
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||||
|
ClientMetadataDocumentUri = new Uri(ClientMetadataDocumentUrl)
|
||||||
|
},
|
||||||
|
}, HttpClient, LoggerFactory);
|
||||||
|
|
||||||
|
await using var client = await McpClient.CreateAsync(
|
||||||
|
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UsesDynamicClientRegistration_WhenCimdNotSupported()
|
||||||
|
{
|
||||||
|
// Disable CIMD support on the test OAuth server so the client
|
||||||
|
// falls back to dynamic registration even if a CIMD URL is provided.
|
||||||
|
TestOAuthServer.ClientIdMetadataDocumentSupported = false;
|
||||||
|
|
||||||
|
await using var app = await StartMcpServerAsync();
|
||||||
|
|
||||||
|
// Provide an invalid CIMD URL; if CIMD were used, auth would fail.
|
||||||
|
await using var transport = new HttpClientTransport(new()
|
||||||
|
{
|
||||||
|
Endpoint = new(McpServerUrl),
|
||||||
|
OAuth = new ClientOAuthOptions()
|
||||||
|
{
|
||||||
|
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||||
|
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||||
|
ClientMetadataDocumentUri = new Uri("http://invalid-cimd.example.com"),
|
||||||
|
Scopes = ["mcp:tools"],
|
||||||
|
DynamicClientRegistration = new()
|
||||||
|
{
|
||||||
|
ClientName = "Test MCP Client (No CIMD)",
|
||||||
|
ClientUri = new Uri("https://example.com/no-cimd"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, HttpClient, LoggerFactory);
|
||||||
|
|
||||||
|
// Should succeed via dynamic client registration.
|
||||||
|
await using var client = await McpClient.CreateAsync(
|
||||||
|
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DoesNotUseClientMetadataDocument_WhenClientIdIsSpecified()
|
||||||
|
{
|
||||||
|
await using var app = await StartMcpServerAsync();
|
||||||
|
|
||||||
|
// Provide an invalid CIMD URL; if CIMD were used, auth would fail.
|
||||||
|
await using var transport = new HttpClientTransport(new()
|
||||||
|
{
|
||||||
|
Endpoint = new(McpServerUrl),
|
||||||
|
OAuth = new ClientOAuthOptions()
|
||||||
|
{
|
||||||
|
ClientId = "demo-client",
|
||||||
|
ClientSecret = "demo-secret",
|
||||||
|
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||||
|
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||||
|
ClientMetadataDocumentUri = new Uri("http://invalid-cimd.example.com"),
|
||||||
|
},
|
||||||
|
}, HttpClient, LoggerFactory);
|
||||||
|
|
||||||
|
await using var client = await McpClient.CreateAsync(
|
||||||
|
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("http://localhost:7029/client-metadata/cimd-client.json")] // Non-HTTPS Scheme
|
||||||
|
[InlineData("http://localhost:7029")] // Missing path
|
||||||
|
public async Task CannotAuthenticate_WithInvalidClientMetadataDocument(string uri)
|
||||||
|
{
|
||||||
|
await using var app = await StartMcpServerAsync();
|
||||||
|
|
||||||
|
await using var transport = new HttpClientTransport(new()
|
||||||
|
{
|
||||||
|
Endpoint = new(McpServerUrl),
|
||||||
|
OAuth = new ClientOAuthOptions()
|
||||||
|
{
|
||||||
|
RedirectUri = new Uri("http://localhost:1179/callback"),
|
||||||
|
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
|
||||||
|
ClientMetadataDocumentUri = new Uri(uri),
|
||||||
|
},
|
||||||
|
}, HttpClient, LoggerFactory);
|
||||||
|
|
||||||
|
var ex = await Assert.ThrowsAsync<McpException>(() => McpClient.CreateAsync(
|
||||||
|
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken));
|
||||||
|
|
||||||
|
Assert.StartsWith("Failed to handle unauthorized response", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CanAuthenticate_WithTokenRefresh()
|
public async Task CanAuthenticate_WithTokenRefresh()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -60,4 +60,10 @@ internal sealed class AuthorizationServerMetadata
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("scopes_supported")]
|
[JsonPropertyName("scopes_supported")]
|
||||||
public List<string>? ScopesSupported { get; init; }
|
public List<string>? ScopesSupported { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether CIMD client IDs are supported.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("client_id_metadata_document_supported")]
|
||||||
|
public bool ClientIdMetadataDocumentSupported { get; init; }
|
||||||
}
|
}
|
||||||
|
|
@ -12,10 +12,15 @@ internal sealed class ClientInfo
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required string ClientId { get; init; }
|
public required string ClientId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether a client secret is required.
|
||||||
|
/// </summary>
|
||||||
|
public required bool RequiresClientSecret { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the client secret.
|
/// Gets or sets the client secret.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required string ClientSecret { get; init; }
|
public string? ClientSecret { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the list of redirect URIs allowed for this client.
|
/// Gets or sets the list of redirect URIs allowed for this client.
|
||||||
|
|
|
||||||
|
|
@ -171,4 +171,11 @@ internal sealed class OAuthServerMetadata
|
||||||
[JsonPropertyName("claims_supported")]
|
[JsonPropertyName("claims_supported")]
|
||||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
public List<string>? ClaimsSupported { get; init; }
|
public List<string>? ClaimsSupported { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether CIMD client IDs are supported.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("client_id_metadata_document_supported")]
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public bool? ClientIdMetadataDocumentSupported { get; init; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ public sealed class Program
|
||||||
{
|
{
|
||||||
private const int _port = 7029;
|
private const int _port = 7029;
|
||||||
private static readonly string _url = $"https://localhost:{_port}";
|
private static readonly string _url = $"https://localhost:{_port}";
|
||||||
|
private static readonly string _clientMetadataDocumentUrl = $"{_url}/client-metadata/cimd-client.json";
|
||||||
|
|
||||||
// Port 5000 is used by tests and port 7071 is used by the ProtectedMcpServer sample
|
// Port 5000 is used by tests and port 7071 is used by the ProtectedMcpServer sample
|
||||||
private static readonly string[] ValidResources = ["http://localhost:5000/", "http://localhost:7071/"];
|
private static readonly string[] ValidResources = ["http://localhost:5000/", "http://localhost:7071/"];
|
||||||
|
|
@ -44,6 +45,16 @@ public sealed class Program
|
||||||
public bool HasIssuedExpiredToken { get; set; }
|
public bool HasIssuedExpiredToken { get; set; }
|
||||||
public bool HasRefreshedToken { get; set; }
|
public bool HasRefreshedToken { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether the authorization server
|
||||||
|
/// advertises support for client ID metadata documents in its discovery
|
||||||
|
/// document. This is used by tests to toggle CIMD support.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The default value is <c>true</c>.
|
||||||
|
/// </remarks>
|
||||||
|
public bool ClientIdMetadataDocumentSupported { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Entry point for the application.
|
/// Entry point for the application.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -102,6 +113,7 @@ public sealed class Program
|
||||||
_clients[clientId] = new ClientInfo
|
_clients[clientId] = new ClientInfo
|
||||||
{
|
{
|
||||||
ClientId = clientId,
|
ClientId = clientId,
|
||||||
|
RequiresClientSecret = true,
|
||||||
ClientSecret = clientSecret,
|
ClientSecret = clientSecret,
|
||||||
RedirectUris = ["http://localhost:1179/callback"],
|
RedirectUris = ["http://localhost:1179/callback"],
|
||||||
};
|
};
|
||||||
|
|
@ -111,10 +123,24 @@ public sealed class Program
|
||||||
_clients["test-refresh-client"] = new ClientInfo
|
_clients["test-refresh-client"] = new ClientInfo
|
||||||
{
|
{
|
||||||
ClientId = "test-refresh-client",
|
ClientId = "test-refresh-client",
|
||||||
|
RequiresClientSecret = true,
|
||||||
ClientSecret = "test-refresh-secret",
|
ClientSecret = "test-refresh-secret",
|
||||||
RedirectUris = ["http://localhost:1179/callback"],
|
RedirectUris = ["http://localhost:1179/callback"],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// This client is pre-registered to support testing Client ID Metadata Documents (CIMD).
|
||||||
|
// A non-test OAuth server implementation would fetch the metadata document from the client-specified
|
||||||
|
// URL during authorization, but we just register the client here to keep the test implementation simple.
|
||||||
|
// We also set 'RequiresClientSecret' to 'false' here because client secrets are disallowed when using CIMD.
|
||||||
|
// See https://datatracker.ietf.org/doc/html/draft-ietf-oauth-client-id-metadata-document-00#section-4.1
|
||||||
|
_clients[_clientMetadataDocumentUrl] = new ClientInfo
|
||||||
|
{
|
||||||
|
ClientId = _clientMetadataDocumentUrl,
|
||||||
|
|
||||||
|
RequiresClientSecret = false,
|
||||||
|
RedirectUris = ["http://localhost:1179/callback"],
|
||||||
|
};
|
||||||
|
|
||||||
// The MCP spec tells the client to use /.well-known/oauth-authorization-server but AddJwtBearer looks for
|
// The MCP spec tells the client to use /.well-known/oauth-authorization-server but AddJwtBearer looks for
|
||||||
// /.well-known/openid-configuration by default. To make things easier, we support both with the same response
|
// /.well-known/openid-configuration by default. To make things easier, we support both with the same response
|
||||||
// which seems to be common. Ex. https://github.com/keycloak/keycloak/pull/29628
|
// which seems to be common. Ex. https://github.com/keycloak/keycloak/pull/29628
|
||||||
|
|
@ -144,7 +170,8 @@ public sealed class Program
|
||||||
CodeChallengeMethodsSupported = ["S256"],
|
CodeChallengeMethodsSupported = ["S256"],
|
||||||
GrantTypesSupported = ["authorization_code", "refresh_token"],
|
GrantTypesSupported = ["authorization_code", "refresh_token"],
|
||||||
IntrospectionEndpoint = $"{_url}/introspect",
|
IntrospectionEndpoint = $"{_url}/introspect",
|
||||||
RegistrationEndpoint = $"{_url}/register"
|
RegistrationEndpoint = $"{_url}/register",
|
||||||
|
ClientIdMetadataDocumentSupported = ClientIdMetadataDocumentSupported,
|
||||||
};
|
};
|
||||||
|
|
||||||
return Results.Ok(metadata);
|
return Results.Ok(metadata);
|
||||||
|
|
@ -468,6 +495,7 @@ public sealed class Program
|
||||||
_clients[clientId] = new ClientInfo
|
_clients[clientId] = new ClientInfo
|
||||||
{
|
{
|
||||||
ClientId = clientId,
|
ClientId = clientId,
|
||||||
|
RequiresClientSecret = true,
|
||||||
ClientSecret = clientSecret,
|
ClientSecret = clientSecret,
|
||||||
RedirectUris = registrationRequest.RedirectUris,
|
RedirectUris = registrationRequest.RedirectUris,
|
||||||
};
|
};
|
||||||
|
|
@ -508,17 +536,17 @@ public sealed class Program
|
||||||
var clientId = form["client_id"].ToString();
|
var clientId = form["client_id"].ToString();
|
||||||
var clientSecret = form["client_secret"].ToString();
|
var clientSecret = form["client_secret"].ToString();
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(clientSecret))
|
if (string.IsNullOrEmpty(clientId) || !_clients.TryGetValue(clientId, out var client))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_clients.TryGetValue(clientId, out var client) && client.ClientSecret == clientSecret)
|
if (client.RequiresClientSecret && client.ClientSecret != clientSecret)
|
||||||
{
|
{
|
||||||
return client;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue