Add support for Client ID Metadata Documents to enable URL-based client registration (#1023)
This commit is contained in:
parent
7f25ae9d2a
commit
5a65af8ed7
3 changed files with 202 additions and 11 deletions
|
|
@ -66,4 +66,10 @@ internal sealed class AuthorizationServerMetadata
|
|||
/// </summary>
|
||||
[JsonPropertyName("scopes_supported")]
|
||||
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>
|
||||
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>
|
||||
/// Gets or sets the OAuth scopes to request.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ internal sealed partial class ClientOAuthProvider
|
|||
private readonly IDictionary<string, string> _additionalAuthorizationParameters;
|
||||
private readonly Func<IReadOnlyList<Uri>, Uri?> _authServerSelector;
|
||||
private readonly AuthorizationRedirectDelegate _authorizationRedirectDelegate;
|
||||
private readonly Uri? _clientMetadataDocumentUri;
|
||||
|
||||
// _dcrClientName, _dcrClientUri, _dcrInitialAccessToken and _dcrResponseDelegate are used for dynamic client registration (RFC 7591)
|
||||
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));
|
||||
_scopes = options.Scopes?.ToArray();
|
||||
_additionalAuthorizationParameters = options.AdditionalAuthorizationParameters;
|
||||
_clientMetadataDocumentUri = options.ClientMetadataDocumentUri;
|
||||
|
||||
// Set up authorization server selection strategy
|
||||
_authServerSelector = options.AuthServerSelector ?? DefaultAuthServerSelector;
|
||||
|
|
@ -221,7 +223,7 @@ internal sealed partial class ClientOAuthProvider
|
|||
_authServerMetadata = authServerMetadata;
|
||||
|
||||
// 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);
|
||||
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))
|
||||
{
|
||||
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
|
||||
|
|
@ -243,6 +253,23 @@ internal sealed partial class ClientOAuthProvider
|
|||
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)
|
||||
{
|
||||
if (authServerUri.OriginalString.Length == 0 ||
|
||||
|
|
|
|||
Loading…
Reference in a new issue