/* * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) * See https://github.com/openiddict/openiddict-core for more information concerning * the license and the contributors participating to this project. */ using System.Diagnostics; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Tokens; namespace OpenIddict.Client; /// /// Contains the methods required to ensure that the OpenIddict client configuration is valid. /// public class OpenIddictClientConfiguration : IPostConfigureOptions { private readonly OpenIddictClientService _service; public OpenIddictClientConfiguration(OpenIddictClientService service) => _service = service ?? throw new ArgumentNullException(nameof(service)); /// /// Populates the default OpenIddict client options and ensures /// that the configuration is in a consistent and valid state. /// /// The authentication scheme associated with the handler instance. /// The options instance to initialize. public void PostConfigure(string name, OpenIddictClientOptions options) { if (options is null) { throw new ArgumentNullException(nameof(options)); } if (options.JsonWebTokenHandler is null) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0075)); } foreach (var registration in options.Registrations) { if (registration.ConfigurationManager is null) { if (registration.Configuration is not null) { registration.Configuration.Issuer = registration.Issuer; registration.ConfigurationManager = new StaticConfigurationManager(registration.Configuration); } else { if (!options.Handlers.Any(descriptor => descriptor.ContextType == typeof(ApplyConfigurationRequestContext)) || !options.Handlers.Any(descriptor => descriptor.ContextType == typeof(ApplyCryptographyRequestContext))) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0313)); } if (registration.MetadataAddress is null) { registration.MetadataAddress = new Uri(".well-known/openid-configuration", UriKind.Relative); } if (!registration.MetadataAddress.IsAbsoluteUri) { var issuer = registration.Issuer; if (issuer is not { IsAbsoluteUri: true }) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0136)); } if (!string.IsNullOrEmpty(issuer.Fragment) || !string.IsNullOrEmpty(issuer.Query)) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0137)); } if (!issuer.OriginalString.EndsWith("/", StringComparison.Ordinal)) { issuer = new Uri(issuer.OriginalString + "/", UriKind.Absolute); } if (registration.MetadataAddress.OriginalString.StartsWith("/", StringComparison.Ordinal)) { registration.MetadataAddress = new Uri(registration.MetadataAddress.OriginalString.Substring( 1, registration.MetadataAddress.OriginalString.Length - 1), UriKind.Relative); } registration.MetadataAddress = new Uri(issuer, registration.MetadataAddress); } registration.ConfigurationManager = new ConfigurationManager( registration.MetadataAddress.AbsoluteUri, new OpenIddictClientRetriever(_service)) { AutomaticRefreshInterval = ConfigurationManager.DefaultAutomaticRefreshInterval, RefreshInterval = ConfigurationManager.DefaultRefreshInterval }; } } } // Sort the handlers collection using the order associated with each handler. options.Handlers.Sort((left, right) => left.Order.CompareTo(right.Order)); // Sort the encryption and signing credentials. options.EncryptionCredentials.Sort((left, right) => Compare(left.Key, right.Key)); options.SigningCredentials.Sort((left, right) => Compare(left.Key, right.Key)); // Generate a key identifier for the encryption/signing keys that don't already have one. foreach (var key in options.EncryptionCredentials.Select(credentials => credentials.Key) .Concat(options.SigningCredentials.Select(credentials => credentials.Key)) .Where(key => string.IsNullOrEmpty(key.KeyId))) { key.KeyId = GetKeyIdentifier(key); } // Attach the signing credentials to the token validation parameters. options.TokenValidationParameters.IssuerSigningKeys = from credentials in options.SigningCredentials select credentials.Key; // Attach the encryption credentials to the token validation parameters. options.TokenValidationParameters.TokenDecryptionKeys = from credentials in options.EncryptionCredentials select credentials.Key; static int Compare(SecurityKey left, SecurityKey right) => (left, right) switch { // If the two keys refer to the same instances, return 0. (SecurityKey first, SecurityKey second) when ReferenceEquals(first, second) => 0, // If one of the keys is a symmetric key, prefer it to the other one. (SymmetricSecurityKey, SymmetricSecurityKey) => 0, (SymmetricSecurityKey, SecurityKey) => -1, (SecurityKey, SymmetricSecurityKey) => 1, // If one of the keys is backed by a X.509 certificate, don't prefer it if it's not valid yet. (X509SecurityKey first, SecurityKey) when first.Certificate.NotBefore > DateTime.Now => 1, (SecurityKey, X509SecurityKey second) when second.Certificate.NotBefore > DateTime.Now => 1, // If the two keys are backed by a X.509 certificate, prefer the one with the furthest expiration date. (X509SecurityKey first, X509SecurityKey second) => -first.Certificate.NotAfter.CompareTo(second.Certificate.NotAfter), // If one of the keys is backed by a X.509 certificate, prefer the X.509 security key. (X509SecurityKey, SecurityKey) => -1, (SecurityKey, X509SecurityKey) => 1, // If the two keys are not backed by a X.509 certificate, none should be preferred to the other. (SecurityKey, SecurityKey) => 0 }; static string? GetKeyIdentifier(SecurityKey key) { // When no key identifier can be retrieved from the security keys, a value is automatically // inferred from the hexadecimal representation of the certificate thumbprint (SHA-1) // when the key is bound to a X.509 certificate or from the public part of the signing key. if (key is X509SecurityKey x509SecurityKey) { return x509SecurityKey.Certificate.Thumbprint; } if (key is RsaSecurityKey rsaSecurityKey) { // Note: if the RSA parameters are not attached to the signing key, // extract them by calling ExportParameters on the RSA instance. var parameters = rsaSecurityKey.Parameters; if (parameters.Modulus is null) { parameters = rsaSecurityKey.Rsa.ExportParameters(includePrivateParameters: false); Debug.Assert(parameters.Modulus is not null, SR.GetResourceString(SR.ID4003)); } // Only use the 40 first chars of the base64url-encoded modulus. var identifier = Base64UrlEncoder.Encode(parameters.Modulus); return identifier.Substring(0, Math.Min(identifier.Length, 40)).ToUpperInvariant(); } #if SUPPORTS_ECDSA if (key is ECDsaSecurityKey ecsdaSecurityKey) { // Extract the ECDSA parameters from the signing credentials. var parameters = ecsdaSecurityKey.ECDsa.ExportParameters(includePrivateParameters: false); Debug.Assert(parameters.Q.X is not null, SR.GetResourceString(SR.ID4004)); // Only use the 40 first chars of the base64url-encoded X coordinate. var identifier = Base64UrlEncoder.Encode(parameters.Q.X); return identifier.Substring(0, Math.Min(identifier.Length, 40)).ToUpperInvariant(); } #endif return null; } } }