From 83f0bb5c8f434bd2a320ac0e25a50698cbd1de01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Sun, 31 May 2026 15:16:32 +0200 Subject: [PATCH] Move the options validation logic to IValidateOptions implementations --- OpenIddict.slnx | 24 +- ...OpenIddictClientWebIntegrationGenerator.cs | 173 +++++--- .../OpenIddictResources.resx | 26 +- ...OpenIddictClientAspNetCoreConfiguration.cs | 115 ++++-- .../OpenIddictClientAspNetCoreExtensions.cs | 10 +- ...IddictClientDataProtectionConfiguration.cs | 10 +- .../OpenIddictClientOwinConfiguration.cs | 42 +- .../OpenIddictClientOwinExtensions.cs | 3 + ...ictClientSystemIntegrationConfiguration.cs | 134 +++--- ...IddictClientSystemIntegrationExtensions.cs | 3 + ...nIddictClientSystemNetHttpConfiguration.cs | 2 - ...OpenIddictClientSystemNetHttpExtensions.cs | 4 +- ...IddictClientWebIntegrationConfiguration.cs | 33 +- ...penIddictClientWebIntegrationExtensions.cs | 14 +- ...penIddictClientWebIntegrationProviders.xml | 2 +- .../OpenIddictClientConfiguration.cs | 271 ++++++------ .../OpenIddictClientExtensions.cs | 5 +- ...OpenIddictServerAspNetCoreConfiguration.cs | 88 ++-- .../OpenIddictServerAspNetCoreExtensions.cs | 9 +- ...IddictServerDataProtectionConfiguration.cs | 10 +- .../OpenIddictServerOwinConfiguration.cs | 10 +- .../OpenIddictServerOwinExtensions.cs | 4 +- .../OpenIddictServerConfiguration.cs | 391 +++++++++--------- .../OpenIddictServerExtensions.cs | 5 +- ...IddictValidationAspNetCoreConfiguration.cs | 38 +- ...penIddictValidationAspNetCoreExtensions.cs | 5 +- ...ctValidationDataProtectionConfiguration.cs | 10 +- ...alidationServerIntegrationConfiguration.cs | 39 +- ...ctValidationServerIntegrationExtensions.cs | 2 +- ...ictValidationSystemNetHttpConfiguration.cs | 2 - ...IddictValidationSystemNetHttpExtensions.cs | 4 +- .../OpenIddictValidationConfiguration.cs | 196 +++++---- .../OpenIddictValidationExtensions.cs | 5 +- .../OpenIddict.Client.AspNetCore.Tests.csproj | 23 ++ ...nIddict.Client.DataProtection.Tests.csproj | 22 + .../OpenIddict.Client.Owin.Tests.csproj | 26 ++ ...dict.Client.SystemIntegration.Tests.csproj | 34 ++ .../OpenIddict.Client.Tests.csproj | 26 ++ ...nIddict.Client.WebIntegration.Tests.csproj | 34 ++ .../OpenIddict.Server.AspNetCore.Tests.csproj | 23 ++ ...nIddict.Server.DataProtection.Tests.csproj | 6 +- .../OpenIddict.Server.IntegrationTests.csproj | 10 +- ...nIddictServerIntegrationTests.Discovery.cs | 17 +- .../OpenIddict.Server.Owin.Tests.csproj | 26 ++ .../OpenIddict.Server.Tests.csproj | 12 +- ...nIddict.Validation.AspNetCore.Tests.csproj | 23 ++ ...ict.Validation.DataProtection.Tests.csproj | 22 + ...nIddict.Validation.IntegrationTests.csproj | 8 +- .../OpenIddictValidationIntegrationTests.cs | 5 + .../OpenIddict.Validation.Owin.Tests.csproj | 26 ++ .../OpenIddict.Validation.Tests.csproj | 26 ++ 51 files changed, 1345 insertions(+), 713 deletions(-) create mode 100644 test/OpenIddict.Client.AspNetCore.Tests/OpenIddict.Client.AspNetCore.Tests.csproj create mode 100644 test/OpenIddict.Client.DataProtection.Tests/OpenIddict.Client.DataProtection.Tests.csproj create mode 100644 test/OpenIddict.Client.Owin.Tests/OpenIddict.Client.Owin.Tests.csproj create mode 100644 test/OpenIddict.Client.SystemIntegration.Tests/OpenIddict.Client.SystemIntegration.Tests.csproj create mode 100644 test/OpenIddict.Client.Tests/OpenIddict.Client.Tests.csproj create mode 100644 test/OpenIddict.Client.WebIntegration.Tests/OpenIddict.Client.WebIntegration.Tests.csproj create mode 100644 test/OpenIddict.Server.AspNetCore.Tests/OpenIddict.Server.AspNetCore.Tests.csproj create mode 100644 test/OpenIddict.Server.Owin.Tests/OpenIddict.Server.Owin.Tests.csproj create mode 100644 test/OpenIddict.Validation.AspNetCore.Tests/OpenIddict.Validation.AspNetCore.Tests.csproj create mode 100644 test/OpenIddict.Validation.DataProtection.Tests/OpenIddict.Validation.DataProtection.Tests.csproj create mode 100644 test/OpenIddict.Validation.Owin.Tests/OpenIddict.Validation.Owin.Tests.csproj create mode 100644 test/OpenIddict.Validation.Tests/OpenIddict.Validation.Tests.csproj diff --git a/OpenIddict.slnx b/OpenIddict.slnx index bd18239e..0e76237a 100644 --- a/OpenIddict.slnx +++ b/OpenIddict.slnx @@ -49,55 +49,67 @@ - - + - - + + - - + + + + + + + + + + + + + + + diff --git a/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs b/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs index 81c869c3..7b62d5ae 100644 --- a/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs +++ b/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs @@ -908,6 +908,7 @@ public static partial class OpenIddictClientWebIntegrationConstants var template = Template.Parse(@"#nullable enable #pragma warning disable CS0618 +using System.ComponentModel.DataAnnotations; using System.Text.RegularExpressions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -926,7 +927,7 @@ public sealed partial class OpenIddictClientWebIntegrationConfiguration ArgumentNullException.ThrowIfNull(registration); {{~ for provider in providers ~}} - {{~ if for.index == 0 ~}} + {{~ if for.first ~}} if (registration.ProviderType is ProviderTypes.{{ provider.name }}) {{~ else ~}} else if (registration.ProviderType is ProviderTypes.{{ provider.name }}) @@ -934,7 +935,7 @@ public sealed partial class OpenIddictClientWebIntegrationConfiguration { if (registration.ProviderSettings is not OpenIddictClientWebIntegrationSettings.{{ provider.name }} settings) { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0406)); + return; } {{~ for setting in provider.settings ~}} @@ -947,7 +948,7 @@ public sealed partial class OpenIddictClientWebIntegrationConfiguration {{~ else if setting.type == 'Uri' ~}} if (settings.{{ setting.property_name }} is null) { - settings.{{ setting.property_name }} = new Uri(""{{ setting.default_value }}"", UriKind.RelativeOrAbsolute); + settings.{{ setting.property_name }} = CreateUri(""{{ setting.default_value }}"", UriKind.RelativeOrAbsolute); } {{~ else if setting.type == 'Boolean' ~}} if (settings.{{ setting.property_name }} is null) @@ -995,26 +996,6 @@ public sealed partial class OpenIddictClientWebIntegrationConfiguration } {{~ end ~}} - {{~ for setting in provider.settings ~}} - {{~ if setting.required ~}} - {{~ if setting.type == 'String' ~}} - if (string.IsNullOrEmpty(settings.{{ setting.property_name }})) - {{~ else ~}} - if (settings.{{ setting.property_name }} is null) - {{~ end ~}} - { - throw new InvalidOperationException(SR.FormatID0332(nameof(settings.{{ setting.property_name }}), Providers.{{ provider.name }})); - } - {{~ end ~}} - - {{~ if setting.type == 'Uri' ~}} - if (!settings.{{ setting.property_name }}.IsAbsoluteUri || OpenIddictHelpers.IsImplicitFileUri(settings.{{ setting.property_name }})) - { - throw new InvalidOperationException(SR.FormatID0350(nameof(settings.{{ setting.property_name }}), Providers.{{ provider.name }})); - } - {{~ end ~}} - {{~ end ~}} - registration.ProviderName ??= Providers.{{ provider.name }}; registration.ProviderDisplayName ??= ""{{ provider.display_name }}""; @@ -1022,10 +1003,10 @@ public sealed partial class OpenIddictClientWebIntegrationConfiguration { {{~ for environment in provider.environments ~}} OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.{{ environment.name }} - => new Uri($""{{ environment.issuer | string.replace '\'' '""' }}"", UriKind.Absolute), + => CreateUri($""{{ environment.issuer | string.replace '\'' '""' }}"", UriKind.Absolute), {{~ end ~}} - _ => throw new InvalidOperationException(SR.FormatID0194(nameof(settings.Environment))) + _ => null }; registration.ConfigurationEndpoint ??= settings.Environment switch @@ -1033,13 +1014,13 @@ public sealed partial class OpenIddictClientWebIntegrationConfiguration {{~ for environment in provider.environments ~}} OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.{{ environment.name }} {{~ if environment.configuration_endpoint ~}} - => new Uri($""{{ environment.configuration_endpoint | string.replace '\'' '""' }}"", UriKind.Absolute), + => CreateUri($""{{ environment.configuration_endpoint | string.replace '\'' '""' }}"", UriKind.Absolute), {{~ else ~}} => null, {{~ end ~}} {{~ end ~}} - _ => throw new InvalidOperationException(SR.FormatID0194(nameof(settings.Environment))) + _ => null }; registration.Configuration ??= settings.Environment switch @@ -1049,27 +1030,27 @@ public sealed partial class OpenIddictClientWebIntegrationConfiguration OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.{{ environment.name }} => new OpenIddictConfiguration { {{~ if environment.configuration.authorization_endpoint ~}} - AuthorizationEndpoint = new Uri($""{{ environment.configuration.authorization_endpoint | string.replace '\'' '""' }}"", UriKind.Absolute), + AuthorizationEndpoint = CreateUri($""{{ environment.configuration.authorization_endpoint | string.replace '\'' '""' }}"", UriKind.Absolute), {{~ end ~}} {{~ if environment.configuration.device_authorization_endpoint ~}} - DeviceAuthorizationEndpoint = new Uri($""{{ environment.configuration.device_authorization_endpoint | string.replace '\'' '""' }}"", UriKind.Absolute), + DeviceAuthorizationEndpoint = CreateUri($""{{ environment.configuration.device_authorization_endpoint | string.replace '\'' '""' }}"", UriKind.Absolute), {{~ end ~}} {{~ if environment.configuration.introspection_endpoint ~}} - IntrospectionEndpoint = new Uri($""{{ environment.configuration.introspection_endpoint | string.replace '\'' '""' }}"", UriKind.Absolute), + IntrospectionEndpoint = CreateUri($""{{ environment.configuration.introspection_endpoint | string.replace '\'' '""' }}"", UriKind.Absolute), {{~ end ~}} {{~ if environment.configuration.revocation_endpoint ~}} - RevocationEndpoint = new Uri($""{{ environment.configuration.revocation_endpoint | string.replace '\'' '""' }}"", UriKind.Absolute), + RevocationEndpoint = CreateUri($""{{ environment.configuration.revocation_endpoint | string.replace '\'' '""' }}"", UriKind.Absolute), {{~ end ~}} {{~ if environment.configuration.token_endpoint ~}} - TokenEndpoint = new Uri($""{{ environment.configuration.token_endpoint | string.replace '\'' '""' }}"", UriKind.Absolute), + TokenEndpoint = CreateUri($""{{ environment.configuration.token_endpoint | string.replace '\'' '""' }}"", UriKind.Absolute), {{~ end ~}} {{~ if environment.configuration.user_info_endpoint ~}} - UserInfoEndpoint = new Uri($""{{ environment.configuration.user_info_endpoint | string.replace '\'' '""' }}"", UriKind.Absolute), + UserInfoEndpoint = CreateUri($""{{ environment.configuration.user_info_endpoint | string.replace '\'' '""' }}"", UriKind.Absolute), {{~ end ~}} CodeChallengeMethodsSupported = @@ -1140,7 +1121,7 @@ public sealed partial class OpenIddictClientWebIntegrationConfiguration {{~ end ~}} {{~ end ~}} - _ => throw new InvalidOperationException(SR.FormatID0194(nameof(settings.Environment))) + _ => null }; {{~ for setting in provider.settings ~}} @@ -1182,22 +1163,11 @@ public sealed partial class OpenIddictClientWebIntegrationConfiguration { registration.SigningCredentials.Add(new SigningCredentials(key, SecurityAlgorithms.EcdsaSha512)); } - - else - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0068)); - } } {{~ end ~}} {{~ if setting.type == 'SigningKey' ~}} - if (settings.{{ setting.property_name }} is not null) + if (settings.{{ setting.property_name }} is not null and not AsymmetricSecurityKey { PrivateKeyStatus: PrivateKeyStatus.DoesNotExist }) { - // If the signing key is an asymmetric security key, ensure it has a private key. - if (settings.{{ setting.property_name }} is AsymmetricSecurityKey { PrivateKeyStatus: PrivateKeyStatus.DoesNotExist }) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0067)); - } - {{~ if setting.signing_algorithm ~}} registration.SigningCredentials.Add(new SigningCredentials(settings.{{ setting.property_name }}, ""{{ setting.signing_algorithm }}"")); {{~ else ~}} @@ -1226,11 +1196,6 @@ public sealed partial class OpenIddictClientWebIntegrationConfiguration { registration.SigningCredentials.Add(new SigningCredentials(settings.{{ setting.property_name }}, SecurityAlgorithms.EcdsaSha512)); } - - else - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0068)); - } {{~ end ~}} } {{~ end ~}} @@ -1239,9 +1204,111 @@ public sealed partial class OpenIddictClientWebIntegrationConfiguration {{~ end ~}} - else + static Uri? CreateUri(string? value, UriKind kind) => Uri.TryCreate(value, kind, out Uri? uri) ? uri : null; + } + + public static partial IEnumerable ValidateProvider(OpenIddictClientRegistration registration) + { + ArgumentNullException.ThrowIfNull(registration); + + return Execute(); + + IEnumerable Execute() { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0407)); + {{~ for provider in providers ~}} + {{~ if for.index == 0 ~}} + if (registration.ProviderType is ProviderTypes.{{ provider.name }}) + {{~ else ~}} + else if (registration.ProviderType is ProviderTypes.{{ provider.name }}) + {{~ end ~}} + { + if (registration.ProviderSettings is not OpenIddictClientWebIntegrationSettings.{{ provider.name }} settings) + { + yield return new ValidationResult(SR.GetResourceString(SR.ID0406)); + + // Note: if the settings instance is missing or of the wrong type, abort any further validation. + yield break; + } + + // Ensure the specified environment is valid. + if (settings.Environment is not ( + {{~ for environment in provider.environments ~}} + {{~ if !for.last ~}} + OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.{{ environment.name }} or + {{~ else ~}} + OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.{{ environment.name }} + {{~ end ~}} + {{~ end ~}} + )) + { + yield return new ValidationResult(SR.FormatID0194(nameof(settings.Environment))); + } + + {{~ for setting in provider.settings ~}} + {{~ if setting.required ~}} + {{~ if setting.type == 'String' ~}} + if (string.IsNullOrEmpty(settings.{{ setting.property_name }})) + {{~ else ~}} + if (settings.{{ setting.property_name }} is null) + {{~ end ~}} + { + yield return new ValidationResult(SR.FormatID0332(nameof(settings.{{ setting.property_name }}), Providers.{{ provider.name }})); + } + {{~ end ~}} + + {{~ if setting.type == 'Uri' ~}} + else if (!settings.{{ setting.property_name }}.IsAbsoluteUri || OpenIddictHelpers.IsImplicitFileUri(settings.{{ setting.property_name }})) + { + yield return new ValidationResult(SR.FormatID0350(nameof(settings.{{ setting.property_name }}), Providers.{{ provider.name }})); + } + {{~ end ~}} + {{~ end ~}} + + {{~ for setting in provider.settings ~}} + {{~ if setting.type == 'SigningCertificate' ~}} + if (settings.{{ setting.property_name }} is not null) + { + var key = new X509SecurityKey(settings.{{ setting.property_name }}); + if (!key.IsSupportedAlgorithm(SecurityAlgorithms.RsaSha256) && + !key.IsSupportedAlgorithm(SecurityAlgorithms.HmacSha256) && + !key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha256) && + !key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha384) && + !key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha512)) + { + yield return new ValidationResult(SR.GetResourceString(SR.ID0068)); + } + } + {{~ end ~}} + {{~ if setting.type == 'SigningKey' ~}} + if (settings.{{ setting.property_name }} is not null) + { + // If the signing key is an asymmetric security key, ensure it has a private key. + if (settings.{{ setting.property_name }} is AsymmetricSecurityKey { PrivateKeyStatus: PrivateKeyStatus.DoesNotExist }) + { + yield return new ValidationResult(SR.GetResourceString(SR.ID0067)); + } + + {{~ if !setting.signing_algorithm ~}} + if (!settings.{{ setting.property_name }}.IsSupportedAlgorithm(SecurityAlgorithms.RsaSha256) && + !settings.{{ setting.property_name }}.IsSupportedAlgorithm(SecurityAlgorithms.HmacSha256) && + !settings.{{ setting.property_name }}.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha256) && + !settings.{{ setting.property_name }}.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha384) && + !settings.{{ setting.property_name }}.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha512)) + { + yield return new ValidationResult(SR.GetResourceString(SR.ID0068)); + } + {{~ end ~}} + } + {{~ end ~}} + {{~ end ~}} + } + + {{~ end ~}} + + else + { + yield return new ValidationResult(SR.GetResourceString(SR.ID0407)); + } } } } diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx index eb44e11f..3d1d310d 100644 --- a/src/OpenIddict.Abstractions/OpenIddictResources.resx +++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx @@ -472,11 +472,10 @@ This may indicate that it was not properly registered in the dependency injectio The realm cannot be null or empty. - The OpenIddict ASP.NET Core server handler cannot be registered as an authentication scheme. -This may indicate that an instance of another handler was registered with the same scheme. + The '{0}' authentication scheme already exists and cannot be registered as an authentication scheme by the OpenIddict ASP.NET Core server integration. Consider removing the conflicting authentication handler. - The OpenIddict ASP.NET Core server handler cannot be used as the default scheme handler. + The OpenIddict ASP.NET Core server handler cannot be used as a default authentication scheme handler. Make sure that neither DefaultAuthenticateScheme, DefaultChallengeScheme, DefaultForbidScheme, DefaultSignInScheme, DefaultSignOutScheme nor DefaultScheme point to an instance of the OpenIddict ASP.NET Core server handler. @@ -685,11 +684,10 @@ To register the OpenIddict core services, reference the 'OpenIddict.Core' packag Error URI: {2} - The OpenIddict ASP.NET Core validation handler cannot be registered as an authentication scheme. -This may indicate that an instance of another handler was registered with the same scheme. + The '{0}' authentication scheme already exists and cannot be registered as an authentication scheme by the OpenIddict ASP.NET Core validation integration. Consider removing the conflicting authentication handler. - The OpenIddict ASP.NET Core validation handler cannot be used as the default sign-in/sign-out handler. + The OpenIddict ASP.NET Core validation handler cannot be used as a default authentication scheme handler. Make sure that neither DefaultSignInScheme nor DefaultSignOutScheme point to an instance of the OpenIddict ASP.NET Core validation handler. @@ -1016,11 +1014,10 @@ To register the OpenIddict core services, reference the 'OpenIddict.Core' packag The payload of this authentication ticket was serialized using an unsupported formatter version. - The OpenIddict ASP.NET Core client handler cannot be registered as an authentication scheme. -This may indicate that an instance of another handler was registered with the same scheme. + The '{0}' authentication scheme already exists and cannot be registered as an authentication scheme by the OpenIddict ASP.NET Core client integration. Consider removing the conflicting authentication handler. - The OpenIddict ASP.NET Core client handler cannot be used as the default authentication/sign-in/sign-out handler. + The OpenIddict ASP.NET Core client handler cannot be used as a default authentication scheme handler. Make sure that neither DefaultAuthenticateScheme, DefaultSignInScheme, DefaultSignOutScheme nor DefaultScheme point to an instance of the OpenIddict ASP.NET Core client handler. @@ -1347,7 +1344,7 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId The default system browser couldn't be started. If the application executes inside a sandbox, make sure it is allowed to launch URIs or spawn new processes. - An application discriminator must be manually set in the OpenIddict client system integration options when no application name is provided by the .NET generic host. To set the application discriminator, call 'services.AddOpenIddict().AddClient().UseSystemIntegration().SetApplicationDiscriminator()'. + A pipe name must be manually set in the OpenIddict client system integration options when enabling activation redirection or the built-in pipe server. To set an application discriminator from which the pipe name will be derived, call 'services.AddOpenIddict().AddClient().UseSystemIntegration().SetApplicationDiscriminator()'. Alternatively, you can directly set the pipe name using 'services.AddOpenIddict().AddClient().UseSystemIntegration().SetPipeName()'. The type extracted from the inter-process notification ({0}) is unknown or invalid, which may indicate that different versions of the OpenIddict client are used for the same application. @@ -1878,6 +1875,15 @@ To use a custom policy relying on the system store, set 'OpenIddictServerOptions The output length used for derivation of client secrets cannot be lower than {0} or higher than {1} bits. + + Client registrations must be assigned a unique registration identifier. + + + Client registrations must be assigned a configuration manager. + + + A configuration manager must be attached to the validation options. + The security token is missing. diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConfiguration.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConfiguration.cs index 961b84c4..88124285 100644 --- a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConfiguration.cs +++ b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConfiguration.cs @@ -17,7 +17,9 @@ namespace OpenIddict.Client.AspNetCore; public sealed class OpenIddictClientAspNetCoreConfiguration : IConfigureOptions, IConfigureOptions, IPostConfigureOptions, - IPostConfigureOptions + IPostConfigureOptions, + IValidateOptions, + IValidateOptions { private readonly IServiceProvider _provider; @@ -33,28 +35,22 @@ public sealed class OpenIddictClientAspNetCoreConfiguration : IConfigureOptions< { ArgumentNullException.ThrowIfNull(options); - // If a handler was already registered and the type doesn't correspond to the OpenIddict handler, throw an exception. - if (options.SchemeMap.TryGetValue(OpenIddictClientAspNetCoreDefaults.AuthenticationScheme, out var builder) && - builder.HandlerType != typeof(OpenIddictClientAspNetCoreHandler)) + // Register the authentication scheme handler used by the OpenIddict ASP.NET Core client integration. + if (!options.SchemeMap.ContainsKey(OpenIddictClientAspNetCoreDefaults.AuthenticationScheme)) { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0288)); + options.AddScheme( + OpenIddictClientAspNetCoreDefaults.AuthenticationScheme, displayName: null); } - options.AddScheme( - OpenIddictClientAspNetCoreDefaults.AuthenticationScheme, displayName: null); - // Resolve the forwarded authentication schemes managed by the OpenIddict ASP.NET Core // client host and add an entry for each scheme in the ASP.NET Core authentication options. foreach (var scheme in _provider.GetRequiredService>() .CurrentValue.ForwardedAuthenticationSchemes) { - if (options.SchemeMap.TryGetValue(scheme.Name, out builder) && - builder.HandlerType != typeof(OpenIddictClientAspNetCoreForwarder)) + if (!options.SchemeMap.ContainsKey(scheme.Name)) { - throw new InvalidOperationException(SR.FormatID0414(scheme.Name)); + options.AddScheme(scheme.Name, scheme.DisplayName); } - - options.AddScheme(scheme.Name, scheme.DisplayName); } } @@ -67,9 +63,9 @@ public sealed class OpenIddictClientAspNetCoreConfiguration : IConfigureOptions< { foreach (var (provider, registrations) in _provider.GetRequiredService>() .CurrentValue.Registrations - .Where(registration => !string.IsNullOrEmpty(registration.ProviderName)) - .GroupBy(registration => registration.ProviderName) - .Select(group => (ProviderName: group.Key, Registrations: group.ToList()))) + .Where(static registration => !string.IsNullOrEmpty(registration.ProviderName)) + .GroupBy(static registration => registration.ProviderName) + .Select(static group => (ProviderName: group.Key, Registrations: group.ToList()))) { // If an explicit mapping was already added, don't overwrite it. if (options.ForwardedAuthenticationSchemes.Exists(scheme => @@ -78,11 +74,9 @@ public sealed class OpenIddictClientAspNetCoreConfiguration : IConfigureOptions< continue; } - // Ensure multiple client registrations don't share the same provider - // name when automatic authentication scheme forwarding is enabled. if (registrations is not [OpenIddictClientRegistration registration]) { - throw new InvalidOperationException(SR.FormatID0415(provider)); + continue; } options.ForwardedAuthenticationSchemes.Add(new AuthenticationScheme( @@ -107,14 +101,6 @@ public sealed class OpenIddictClientAspNetCoreConfiguration : IConfigureOptions< { ArgumentNullException.ThrowIfNull(options); - if (!TryValidate(options.SchemeMap, options.DefaultAuthenticateScheme) || - !TryValidate(options.SchemeMap, options.DefaultScheme) || - !TryValidate(options.SchemeMap, options.DefaultSignInScheme) || - !TryValidate(options.SchemeMap, options.DefaultSignOutScheme)) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0289)); - } - // Starting in ASP.NET 7.0, the authentication stack integrates a fallback // mechanism to select the default scheme to use when no value is set, but // only if a single handler has been registered in the authentication options. @@ -131,8 +117,44 @@ public sealed class OpenIddictClientAspNetCoreConfiguration : IConfigureOptions< { options.AddScheme(Guid.NewGuid().ToString(), displayName: null); } + } + + /// + public ValidateOptionsResult Validate(string? name, AuthenticationOptions options) + { + ArgumentNullException.ThrowIfNull(options); + + var builder = new ValidateOptionsResultBuilder(); - static bool TryValidate(IDictionary map, string? scheme) + // Ensure the default schemes are not mapped to the OpenIddict client handler or forwarder. + if (!ValidateDefaultScheme(options.SchemeMap, options.DefaultAuthenticateScheme) || + !ValidateDefaultScheme(options.SchemeMap, options.DefaultScheme) || + !ValidateDefaultScheme(options.SchemeMap, options.DefaultSignInScheme) || + !ValidateDefaultScheme(options.SchemeMap, options.DefaultSignOutScheme)) + { + builder.AddError(SR.GetResourceString(SR.ID0289)); + } + + // Ensure the main authentication scheme was not hijacked by another component + // and that the handler type corresponds to the OpenIddict client handler. + if (!ValidateHandlerType( + options.SchemeMap, OpenIddictClientAspNetCoreDefaults.AuthenticationScheme)) + { + builder.AddError(SR.GetResourceString(SR.ID0288)); + } + + // Ensure the forwarded authentication schemes are mapped to the OpenIddict client forwarder. + foreach (var group in _provider.GetRequiredService>() + .CurrentValue.ForwardedAuthenticationSchemes + .GroupBy(static scheme => scheme.Name) + .Where(group => !ValidateHandlerType(options.SchemeMap, group.Key))) + { + builder.AddError(SR.FormatID0414(group.Key)); + } + + return builder.Build(); + + static bool ValidateDefaultScheme(IDictionary map, string? scheme) { // If the scheme was not set or if it cannot be found in the map, return true. if (string.IsNullOrEmpty(scheme) || !map.TryGetValue(scheme, out var builder)) @@ -143,5 +165,42 @@ public sealed class OpenIddictClientAspNetCoreConfiguration : IConfigureOptions< return builder.HandlerType != typeof(OpenIddictClientAspNetCoreHandler) && builder.HandlerType != typeof(OpenIddictClientAspNetCoreForwarder); } + + static bool ValidateHandlerType(IDictionary map, string? scheme) + where THandler : IAuthenticationHandler + { + // If the scheme was not set or if it cannot be found in the map, return true. + if (string.IsNullOrEmpty(scheme) || !map.TryGetValue(scheme, out var builder)) + { + return true; + } + + return builder.HandlerType == typeof(THandler); + } + } + + /// + public ValidateOptionsResult Validate(string? name, OpenIddictClientAspNetCoreOptions options) + { + ArgumentNullException.ThrowIfNull(options); + + var builder = new ValidateOptionsResultBuilder(); + + // Ensure multiple client registrations don't share the same provider + // name when automatic authentication scheme forwarding is enabled. + if (!options.DisableAutomaticAuthenticationSchemeForwarding) + { + foreach (var (provider, registrations) in _provider.GetRequiredService>() + .CurrentValue.Registrations + .Where(static registration => !string.IsNullOrEmpty(registration.ProviderName)) + .GroupBy(static registration => registration.ProviderName) + .Select(static group => (ProviderName: group.Key, Registrations: group.ToList())) + .Where(static group => group.Registrations.Count is > 1)) + { + builder.AddError(SR.FormatID0415(provider)); + } + } + + return builder.Build(); } } diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreExtensions.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreExtensions.cs index cac9e143..6305d59c 100644 --- a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreExtensions.cs +++ b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreExtensions.cs @@ -49,14 +49,20 @@ public static class OpenIddictClientAspNetCoreExtensions IConfigureOptions, OpenIddictClientAspNetCoreConfiguration>()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< - IPostConfigureOptions, OpenIddictClientAspNetCoreConfiguration>()); + IConfigureOptions, OpenIddictClientAspNetCoreConfiguration>()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< - IConfigureOptions, OpenIddictClientAspNetCoreConfiguration>()); + IPostConfigureOptions, OpenIddictClientAspNetCoreConfiguration>()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< IPostConfigureOptions, OpenIddictClientAspNetCoreConfiguration>()); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< + IValidateOptions, OpenIddictClientAspNetCoreConfiguration>()); + + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< + IValidateOptions, OpenIddictClientAspNetCoreConfiguration>()); + return new OpenIddictClientAspNetCoreBuilder(builder.Services); } diff --git a/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionConfiguration.cs b/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionConfiguration.cs index 14b6dd7b..4fb20d10 100644 --- a/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionConfiguration.cs +++ b/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionConfiguration.cs @@ -17,14 +17,14 @@ namespace OpenIddict.Client.DataProtection; public sealed class OpenIddictClientDataProtectionConfiguration : IConfigureOptions, IPostConfigureOptions { - private readonly IDataProtectionProvider _dataProtectionProvider; + private readonly IServiceProvider _provider; /// /// Creates a new instance of the class. /// - /// The ASP.NET Core Data Protection provider. - public OpenIddictClientDataProtectionConfiguration(IDataProtectionProvider dataProtectionProvider) - => _dataProtectionProvider = dataProtectionProvider; + /// The service provider. + public OpenIddictClientDataProtectionConfiguration(IServiceProvider provider) + => _provider = provider ?? throw new ArgumentNullException(nameof(provider)); /// public void Configure(OpenIddictClientOptions options) @@ -40,6 +40,6 @@ public sealed class OpenIddictClientDataProtectionConfiguration : IConfigureOpti { ArgumentNullException.ThrowIfNull(options); - options.DataProtectionProvider ??= _dataProtectionProvider; + options.DataProtectionProvider ??= _provider.GetDataProtectionProvider(); } } diff --git a/src/OpenIddict.Client.Owin/OpenIddictClientOwinConfiguration.cs b/src/OpenIddict.Client.Owin/OpenIddictClientOwinConfiguration.cs index dd09d6d8..5670226a 100644 --- a/src/OpenIddict.Client.Owin/OpenIddictClientOwinConfiguration.cs +++ b/src/OpenIddict.Client.Owin/OpenIddictClientOwinConfiguration.cs @@ -16,7 +16,8 @@ namespace OpenIddict.Client.Owin; /// [EditorBrowsable(EditorBrowsableState.Advanced)] public sealed class OpenIddictClientOwinConfiguration : IConfigureOptions, - IPostConfigureOptions + IPostConfigureOptions, + IValidateOptions { private readonly IServiceProvider _provider; @@ -53,11 +54,6 @@ public sealed class OpenIddictClientOwinConfiguration : IConfigureOptions new CookieManager() }; - if (options.AuthenticationMode is AuthenticationMode.Active) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0314)); - } - if (!options.DisableAutomaticAuthenticationTypeForwarding) { foreach (var (provider, registrations) in _provider.GetRequiredService>() @@ -73,11 +69,9 @@ public sealed class OpenIddictClientOwinConfiguration : IConfigureOptions + public ValidateOptionsResult Validate(string? name, OpenIddictClientOwinOptions options) + { + ArgumentNullException.ThrowIfNull(options); + + var builder = new ValidateOptionsResultBuilder(); + + if (options.AuthenticationMode is AuthenticationMode.Active) + { + builder.AddError(SR.GetResourceString(SR.ID0314)); + } + + // Ensure multiple client registrations don't share the same provider + // name when automatic authentication type forwarding is enabled. + if (!options.DisableAutomaticAuthenticationTypeForwarding) + { + foreach (var (provider, registrations) in _provider.GetRequiredService>() + .CurrentValue.Registrations + .Where(static registration => !string.IsNullOrEmpty(registration.ProviderName)) + .GroupBy(static registration => registration.ProviderName) + .Select(static group => (ProviderName: group.Key, Registrations: group.ToList())) + .Where(static group => group.Registrations.Count is > 1)) + { + builder.AddError(SR.FormatID0416(provider)); + } + } + + return builder.Build(); + } } diff --git a/src/OpenIddict.Client.Owin/OpenIddictClientOwinExtensions.cs b/src/OpenIddict.Client.Owin/OpenIddictClientOwinExtensions.cs index 0c1992b7..73ed9c7a 100644 --- a/src/OpenIddict.Client.Owin/OpenIddictClientOwinExtensions.cs +++ b/src/OpenIddict.Client.Owin/OpenIddictClientOwinExtensions.cs @@ -52,6 +52,9 @@ public static class OpenIddictClientOwinExtensions builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< IPostConfigureOptions, OpenIddictClientOwinConfiguration>()); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< + IValidateOptions, OpenIddictClientOwinConfiguration>()); + return new OpenIddictClientOwinBuilder(builder.Services); } diff --git a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationConfiguration.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationConfiguration.cs index 74e7184b..06aa54f7 100644 --- a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationConfiguration.cs +++ b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationConfiguration.cs @@ -11,6 +11,7 @@ using System.Security.AccessControl; using System.Security.Cryptography; using System.Security.Principal; using System.Text; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; @@ -24,16 +25,17 @@ namespace OpenIddict.Client.SystemIntegration; [EditorBrowsable(EditorBrowsableState.Advanced)] public sealed class OpenIddictClientSystemIntegrationConfiguration : IConfigureOptions, IPostConfigureOptions, - IPostConfigureOptions + IPostConfigureOptions, + IValidateOptions { - private readonly IHostEnvironment _environment; + private readonly IServiceProvider _provider; /// /// Creates a new instance of the class. /// - /// The host environment. - public OpenIddictClientSystemIntegrationConfiguration(IHostEnvironment environment) - => _environment = environment ?? throw new ArgumentNullException(nameof(environment)); + /// The service provider. + public OpenIddictClientSystemIntegrationConfiguration(IServiceProvider provider) + => _provider = provider ?? throw new ArgumentNullException(nameof(provider)); /// public void Configure(OpenIddictClientOptions options) @@ -62,50 +64,6 @@ public sealed class OpenIddictClientSystemIntegrationConfiguration : IConfigureO { ArgumentNullException.ThrowIfNull(options); - // Ensure the operating system version is supported. - if (!OperatingSystem.IsAndroidVersionAtLeast(21) && !OperatingSystem.IsIOSVersionAtLeast(12) && - !OperatingSystem.IsLinux() && !OperatingSystem.IsMacCatalystVersionAtLeast(13, 1) && - !OperatingSystem.IsMacOSVersionAtLeast(10, 15) && !OperatingSystem.IsWindowsVersionAtLeast(7)) - { - throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0389)); - } - -#if !ANDROID - // When running on Android, iOS or Mac Catalyst, ensure the version compiled for these platforms - // is used to prevent the generic/non-OS specific TFM from being used as launching the system - // browser cannot be done using Process.Start() and requires using OS-specific APIs that are - // not available on the portable version of the OpenIddict.Client.SystemIntegration package. - if (OperatingSystem.IsAndroid()) - { - throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0449)); - } -#endif - -#if !IOS && !MACCATALYST - if (OperatingSystem.IsIOS() || OperatingSystem.IsMacCatalyst()) - { - throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0449)); - } -#endif - -#pragma warning disable CA1416 - // If explicitly set, ensure the specified authentication mode is supported. - if (options.AuthenticationMode is ASWebAuthenticationSession && !IsASWebAuthenticationSessionSupported()) - { - throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0446)); - } - - else if (options.AuthenticationMode is CustomTabsIntent && !IsCustomTabsIntentSupported()) - { - throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0452)); - } - - else if (options.AuthenticationMode is WebAuthenticationBroker && !IsWebAuthenticationBrokerSupported()) - { - throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0392)); - } -#pragma warning restore CA1416 - // When possible, always prefer OS-managed modes. Otherwise, fall back to the system browser. options.AuthenticationMode ??= IsASWebAuthenticationSessionSupported() ? ASWebAuthenticationSession : @@ -128,16 +86,12 @@ public sealed class OpenIddictClientSystemIntegrationConfiguration : IConfigureO options.EnableEmbeddedWebServer ??= false; } - // If no explicit application discriminator was specified, compute the SHA-256 hash - // of the application name resolved from the host and use it as a unique identifier. - if (string.IsNullOrEmpty(options.ApplicationDiscriminator)) + // If no explicit application discriminator was specified, compute the SHA-256 hash of the application + // name resolved from the host environment (if available) and use it as a unique identifier. + if (string.IsNullOrEmpty(options.ApplicationDiscriminator) && + _provider.GetService() is IHostEnvironment { ApplicationName.Length: > 0 } environment) { - if (string.IsNullOrEmpty(_environment.ApplicationName)) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0386)); - } - - var digest = SHA256.HashData(Encoding.UTF8.GetBytes(_environment.ApplicationName)); + var digest = SHA256.HashData(Encoding.UTF8.GetBytes(environment.ApplicationName)); // Note: only the left-most half of the hash is used to limit the length of the resulting discriminator, // which is required on platforms like macOS, where the name of pipes is always prefixed with a static part @@ -152,7 +106,7 @@ public sealed class OpenIddictClientSystemIntegrationConfiguration : IConfigureO } // If no explicit pipe name was specified, build one using the application discriminator. - if (string.IsNullOrEmpty(options.PipeName)) + if (string.IsNullOrEmpty(options.PipeName) && !string.IsNullOrEmpty(options.ApplicationDiscriminator)) { // Note: on Windows, the name is deliberately prefixed with "LOCAL\" to support // partial trust/sandboxed applications that are executed in an AppContainer @@ -195,4 +149,66 @@ public sealed class OpenIddictClientSystemIntegrationConfiguration : IConfigureO } } } + + /// + public ValidateOptionsResult Validate(string? name, OpenIddictClientSystemIntegrationOptions options) + { + ArgumentNullException.ThrowIfNull(options); + + var builder = new ValidateOptionsResultBuilder(); + + // Ensure the operating system version is supported. + if (!OperatingSystem.IsAndroidVersionAtLeast(21) && !OperatingSystem.IsIOSVersionAtLeast(12) && + !OperatingSystem.IsLinux() && !OperatingSystem.IsMacCatalystVersionAtLeast(13, 1) && + !OperatingSystem.IsMacOSVersionAtLeast(10, 15) && !OperatingSystem.IsWindowsVersionAtLeast(7)) + { + builder.AddError(SR.GetResourceString(SR.ID0389)); + } + +#if !ANDROID + // When running on Android, iOS or Mac Catalyst, ensure the version compiled for these platforms + // is used to prevent the generic/non-OS specific TFM from being used as launching the system + // browser cannot be done using Process.Start() and requires using OS-specific APIs that are + // not available on the portable version of the OpenIddict.Client.SystemIntegration package. + if (OperatingSystem.IsAndroid()) + { + builder.AddError(SR.GetResourceString(SR.ID0449)); + } +#endif + +#if !IOS && !MACCATALYST + else if (OperatingSystem.IsIOS() || OperatingSystem.IsMacCatalyst()) + { + builder.AddError(SR.GetResourceString(SR.ID0449)); + } +#endif + +#pragma warning disable CA1416 + // If explicitly set, ensure the specified authentication mode is supported. + if (options.AuthenticationMode is ASWebAuthenticationSession && !IsASWebAuthenticationSessionSupported()) + { + builder.AddError(SR.GetResourceString(SR.ID0446)); + } + + else if (options.AuthenticationMode is CustomTabsIntent && !IsCustomTabsIntentSupported()) + { + builder.AddError(SR.GetResourceString(SR.ID0452)); + } + + else if (options.AuthenticationMode is WebAuthenticationBroker && !IsWebAuthenticationBrokerSupported()) + { + builder.AddError(SR.GetResourceString(SR.ID0392)); + } +#pragma warning restore CA1416 + + // If activation redirection or the pipe server is enabled, ensure a pipe name + // was specified or could be inferred from the application discriminator. + if (string.IsNullOrEmpty(options.PipeName) && + (options.EnableActivationRedirection is true || options.EnablePipeServer is true)) + { + builder.AddError(SR.GetResourceString(SR.ID0386)); + } + + return builder.Build(); + } } diff --git a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationExtensions.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationExtensions.cs index dcf96278..b5a898f5 100644 --- a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationExtensions.cs +++ b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationExtensions.cs @@ -104,6 +104,9 @@ public static class OpenIddictClientSystemIntegrationExtensions builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< IPostConfigureOptions, OpenIddictClientSystemIntegrationConfiguration>()); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< + IValidateOptions, OpenIddictClientSystemIntegrationConfiguration>()); + return new OpenIddictClientSystemIntegrationBuilder(builder.Services); } diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs index 616f95bb..bf38b80d 100644 --- a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs +++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs @@ -112,9 +112,7 @@ public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptio #if NET else if (options.CurrentValue.HttpResiliencePipeline is ResiliencePipeline pipeline) { -#pragma warning disable EXTEXP0001 builder.AdditionalHandlers.Add(new ResilienceHandler(pipeline)); -#pragma warning restore EXTEXP0001 } #endif if (builder.PrimaryHandler is not HttpClientHandler handler) diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpExtensions.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpExtensions.cs index c4c5f58f..4bd7d626 100644 --- a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpExtensions.cs +++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpExtensions.cs @@ -38,10 +38,10 @@ public static class OpenIddictClientSystemNetHttpExtensions // Note: TryAddEnumerable() is used here to ensure the initializers are registered only once. builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< - IConfigureOptions, OpenIddictClientSystemNetHttpConfiguration>()); + IConfigureOptions, OpenIddictClientSystemNetHttpConfiguration>()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< - IConfigureOptions, OpenIddictClientSystemNetHttpConfiguration>()); + IConfigureOptions, OpenIddictClientSystemNetHttpConfiguration>()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< IPostConfigureOptions, OpenIddictClientSystemNetHttpConfiguration>()); diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationConfiguration.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationConfiguration.cs index 3fcd53f5..0bb5bf60 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationConfiguration.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationConfiguration.cs @@ -5,6 +5,7 @@ */ using System.ComponentModel; +using System.ComponentModel.DataAnnotations; using Microsoft.Extensions.Options; namespace OpenIddict.Client.WebIntegration; @@ -14,7 +15,8 @@ namespace OpenIddict.Client.WebIntegration; /// [EditorBrowsable(EditorBrowsableState.Advanced)] public sealed partial class OpenIddictClientWebIntegrationConfiguration : IConfigureOptions, - IPostConfigureOptions + IPostConfigureOptions, + IValidateOptions { /// public void Configure(OpenIddictClientOptions options) @@ -32,8 +34,6 @@ public sealed partial class OpenIddictClientWebIntegrationConfiguration : IConfi foreach (var registration in options.Registrations) { - // If the client registration has a provider type attached, apply - // the configuration logic corresponding to the specified provider. if (!string.IsNullOrEmpty(registration.ProviderType)) { ConfigureProvider(registration); @@ -41,6 +41,24 @@ public sealed partial class OpenIddictClientWebIntegrationConfiguration : IConfi } } + /// + public ValidateOptionsResult Validate(string? name, OpenIddictClientOptions options) + { + ArgumentNullException.ThrowIfNull(options); + + var builder = new ValidateOptionsResultBuilder(); + + foreach (var registration in options.Registrations) + { + if (!string.IsNullOrEmpty(registration.ProviderType)) + { + builder.AddResults(ValidateProvider(registration)); + } + } + + return builder.Build(); + } + /// /// Amends the registration with the provider-specific configuration logic. /// @@ -48,4 +66,13 @@ public sealed partial class OpenIddictClientWebIntegrationConfiguration : IConfi // Note: the implementation of this method is automatically generated by the source generator. [EditorBrowsable(EditorBrowsableState.Never)] public static partial void ConfigureProvider(OpenIddictClientRegistration registration); + + /// + /// Validates the registration using the provider-specific configuration logic. + /// + /// The client registration. + /// The validation results. + // Note: the implementation of this method is automatically generated by the source generator. + [EditorBrowsable(EditorBrowsableState.Never)] + public static partial IEnumerable ValidateProvider(OpenIddictClientRegistration registration); } diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationExtensions.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationExtensions.cs index 250baf0a..6c9fa75d 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationExtensions.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationExtensions.cs @@ -38,9 +38,9 @@ public static partial class OpenIddictClientWebIntegrationExtensions builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< IConfigureOptions, OpenIddictClientWebIntegrationConfiguration>()); - // Note: the IPostConfigureOptions service responsible for populating - // the client registrations MUST be registered before OpenIddictClientConfiguration to ensure - // the registrations are correctly populated before being validated. + // Note: the IPostConfigureOptions and IValidateOptions + // services responsible for populating and validating the client registrations MUST be registered before + // OpenIddictClientConfiguration to ensure the registrations are correctly populated and validated. if (!builder.Services.Any(static descriptor => descriptor.ServiceType == typeof(IPostConfigureOptions) && descriptor.ImplementationType == typeof(OpenIddictClientWebIntegrationConfiguration))) @@ -49,6 +49,14 @@ public static partial class OpenIddictClientWebIntegrationExtensions IPostConfigureOptions, OpenIddictClientWebIntegrationConfiguration>()); } + if (!builder.Services.Any(static descriptor => + descriptor.ServiceType == typeof(IValidateOptions) && + descriptor.ImplementationType == typeof(OpenIddictClientWebIntegrationConfiguration))) + { + builder.Services.Insert(0, ServiceDescriptor.Singleton< + IValidateOptions, OpenIddictClientWebIntegrationConfiguration>()); + } + return new OpenIddictClientWebIntegrationBuilder(builder.Services); } diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml index f56442fb..5259939a 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml @@ -2,7 +2,7 @@ xsi:noNamespaceSchemaLocation="OpenIddictClientWebIntegrationProviders.xsd">