diff --git a/Directory.Packages.props b/Directory.Packages.props index 12cdd018..28e0fdc7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -189,11 +189,11 @@ - + - + diff --git a/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs b/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs index 799381b8..6f5ec274 100644 --- a/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs +++ b/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs @@ -43,6 +43,10 @@ namespace OpenIddict.Client.WebIntegration.Generators "OpenIddictClientWebIntegrationOptions.generated.cs", SourceText.From(GenerateOptions(document), Encoding.UTF8)); + context.AddSource( + "OpenIddictClientWebIntegrationSettings.generated.cs", + SourceText.From(GenerateSettings(document), Encoding.UTF8)); + static string GenerateBuilderMethods(XDocument document) { var template = Template.Parse(@"#nullable enable @@ -75,18 +79,29 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder /// /// This extension can be safely called multiple times. /// The instance. + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete($""This method was replaced by {nameof(Add{{ provider.name }})} and will be removed in a future version."")] public OpenIddictClientWebIntegrationBuilder.{{ provider.name }} Use{{ provider.name }}() + => Add{{ provider.name }}(); + + /// + /// Adds a new {{ provider.display_name }} client registration. + {{~ if provider.documentation ~}} + /// For more information, read the documentation. + {{~ end ~}} + /// + /// The instance. + public OpenIddictClientWebIntegrationBuilder.{{ provider.name }} Add{{ provider.name }}() { - // Note: TryAddEnumerable() is used here to ensure the initializers are registered only once. - Services.TryAddEnumerable(new[] + var registration = new OpenIddictClientRegistration { - ServiceDescriptor.Singleton< - IConfigureOptions, OpenIddictClientWebIntegrationConfiguration.{{ provider.name }}>(), - ServiceDescriptor.Singleton< - IPostConfigureOptions, OpenIddictClientWebIntegrationConfiguration.{{ provider.name }}>() - }); + ProviderSettings = new OpenIddictClientWebIntegrationSettings.{{ provider.name }}(), + ProviderType = ProviderTypes.{{ provider.name }} + }; + + Services.Configure(options => options.Registrations.Add(registration)); - return new OpenIddictClientWebIntegrationBuilder.{{ provider.name }}(Services); + return new OpenIddictClientWebIntegrationBuilder.{{ provider.name }}(registration); } /// @@ -98,14 +113,27 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder /// This extension can be safely called multiple times. /// The delegate used to configure the OpenIddict/{{ provider.display_name }} options. /// The instance. + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete($""This method was replaced by {nameof(Add{{ provider.name }})} and will be removed in a future version."")] public OpenIddictClientWebIntegrationBuilder Use{{ provider.name }}(Action configuration) + => Add{{ provider.name }}(configuration); + + /// + /// Adds a new {{ provider.display_name }} client registration. + {{~ if provider.documentation ~}} + /// For more information, read the documentation. + {{~ end ~}} + /// + /// The delegate used to configure the OpenIddict/{{ provider.display_name }} options. + /// The instance. + public OpenIddictClientWebIntegrationBuilder Add{{ provider.name }}(Action configuration) { if (configuration is null) { throw new ArgumentNullException(nameof(configuration)); } - configuration(Use{{ provider.name }}()); + configuration(Add{{ provider.name }}()); return this; } @@ -121,14 +149,28 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder /// Initializes a new instance of . /// /// The services collection. + [Obsolete(""This constructor is no longer supported and will be removed in a future version."", error: true)] public {{ provider.name }}(IServiceCollection services) - => Services = services ?? throw new ArgumentNullException(nameof(services)); + => throw new NotSupportedException(SR.GetResourceString(SR.ID0403)); + + /// + /// Initializes a new instance of . + /// + /// The client registration. + public {{ provider.name }}(OpenIddictClientRegistration registration) + => Registration = registration ?? throw new ArgumentNullException(nameof(registration)); + + /// + /// Gets the client registration. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public OpenIddictClientRegistration Registration { get; } /// /// Gets the services collection. /// [EditorBrowsable(EditorBrowsableState.Never)] - public IServiceCollection Services { get; } + public IServiceCollection Services => throw new NotSupportedException(SR.GetResourceString(SR.ID0403)); /// /// Amends the default OpenIddict client {{ provider.display_name }} configuration. @@ -136,16 +178,38 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder /// The delegate used to configure the OpenIddict options. /// This extension can be safely called multiple times. /// The instance. + [Obsolete(""This method is no longer supported and will be removed in a future version."", error: true)] public {{ provider.name }} Configure(Action configuration) + => throw new NotSupportedException(SR.GetResourceString(SR.ID0403)); + + /// + /// Sets the provider name. + /// + /// The provider name. + /// The instance. + public {{ provider.name }} SetProviderName(string name) { - if (configuration is null) + if (string.IsNullOrEmpty(name)) { - throw new ArgumentNullException(nameof(configuration)); + throw new ArgumentException(SR.GetResourceString(SR.ID0124), nameof(name)); } - Services.Configure(configuration); + return Set(registration => registration.ProviderName = name); + } - return this; + /// + /// Sets the registration identifier. + /// + /// The registration identifier. + /// The instance. + public {{ provider.name }} SetRegistrationId(string identifier) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0124), nameof(identifier)); + } + + return Set(registration => registration.RegistrationId = identifier); } /// @@ -160,7 +224,7 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder throw new ArgumentException(SR.GetResourceString(SR.ID0124), nameof(identifier)); } - return Configure(options => options.ClientId = identifier); + return Set(registration => registration.ClientId = identifier); } /// @@ -175,7 +239,7 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder throw new ArgumentException(SR.GetResourceString(SR.ID0125), nameof(secret)); } - return Configure(options => options.ClientSecret = secret); + return Set(registration => registration.ClientSecret = secret); } /// @@ -194,7 +258,7 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder throw new ArgumentNullException(nameof(uri)); } - return Configure(options => options.PostLogoutRedirectUri = uri); + return Set(registration => registration.PostLogoutRedirectUri = uri); } /// @@ -232,7 +296,7 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder throw new ArgumentNullException(nameof(uri)); } - return Configure(options => options.RedirectUri = uri); + return Set(registration => registration.RedirectUri = uri); } /// @@ -266,7 +330,7 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder throw new ArgumentNullException(nameof(scopes)); } - return Configure(options => options.Scopes.UnionWith(scopes)); + return Set(registration => registration.Scopes.UnionWith(scopes)); } {{~ for environment in provider.environments ~}} @@ -275,7 +339,7 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder /// /// The instance. public {{ provider.name }} Use{{ environment.name }}Environment() - => Configure(options => options.Environment = OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.{{ environment.name }}); + => Set(registration => registration.Get{{ provider.name }}Settings().Environment = OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.{{ environment.name }}); {{~ end ~}} {{~ for setting in provider.settings ~}} @@ -295,7 +359,7 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder throw new ArgumentNullException(nameof({{ setting.parameter_name }})); } - return Configure(options => options.{{ setting.property_name }}.UnionWith({{ setting.parameter_name }})); + return Set(registration => registration.Get{{ provider.name }}Settings().{{ setting.property_name }}.UnionWith({{ setting.parameter_name }})); } {{~ else if setting.clr_type == 'ECDsaSecurityKey' ~}} /// @@ -318,7 +382,7 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder throw new ArgumentException(SR.GetResourceString(SR.ID0055), nameof({{ setting.parameter_name }})); } - return Configure(options => options.{{ setting.property_name }} = {{ setting.parameter_name }}); + return Set(registration => registration.Get{{ provider.name }}Settings().{{ setting.property_name }} = {{ setting.parameter_name }}); } #if SUPPORTS_PEM_ENCODED_KEY_IMPORT @@ -403,7 +467,7 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder throw new ArgumentException(SR.GetResourceString(SR.ID0144), nameof({{ setting.parameter_name }})); } - return Configure(options => options.{{ setting.property_name }} = {{ setting.parameter_name }}); + return Set(registration => registration.Get{{ provider.name }}Settings().{{ setting.property_name }} = {{ setting.parameter_name }}); } /// @@ -444,7 +508,7 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder throw new ArgumentException(SR.GetResourceString(SR.ID0061), nameof({{ setting.parameter_name }})); } - return Configure(options => options.{{ setting.property_name }} = {{ setting.parameter_name }}); + return Set(registration => registration.Get{{ provider.name }}Settings().{{ setting.property_name }} = {{ setting.parameter_name }}); } /// @@ -615,7 +679,7 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder throw new ArgumentNullException(nameof({{ setting.parameter_name }})); } - return Configure(options => options.{{ setting.property_name }} = {{ setting.parameter_name }}); + return Set(registration => registration.Get{{ provider.name }}Settings().{{ setting.property_name }} = {{ setting.parameter_name }}); } {{~ end ~}} @@ -632,6 +696,24 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder /// [EditorBrowsable(EditorBrowsableState.Never)] public override string? ToString() => base.ToString(); + + /// + /// Amends the client registration created by the {{ provider.display_name }} integration. + /// + /// The delegate used to configure the {{ provider.display_name }} client registration. + /// This extension can be safely called multiple times. + /// The instance. + private {{ provider.name }} Set(Action configuration) + { + if (configuration is null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + configuration(Registration); + + return this; + } } {{~ end ~}} } @@ -713,6 +795,13 @@ public static partial class OpenIddictClientWebIntegrationConstants public const string {{ provider.name }} = ""{{ provider.name }}""; {{~ end ~}} } + + public static class ProviderTypes + { + {{~ for provider in providers ~}} + public const string {{ provider.name }} = ""{{ provider.id }}""; + {{~ end ~}} + } } "); return template.Render(new @@ -721,6 +810,7 @@ public static partial class OpenIddictClientWebIntegrationConstants .Select(provider => new { Name = (string) provider.Attribute("Name"), + Id = (string) provider.Attribute("Id"), Environments = provider.Elements("Environment").Select(environment => new { @@ -751,36 +841,58 @@ public sealed partial class OpenIddictClientWebIntegrationConfiguration /// /// Contains the methods required to register the {{ provider.display_name }} integration in the OpenIddict client options. /// + [Obsolete(""This class is no longer supported and will be removed in a future version."", error: true)] public sealed class {{ provider.name }} : IConfigureOptions, IPostConfigureOptions { - private readonly IServiceProvider _provider; - /// /// Creates a new instance of the class. /// /// The service provider. - /// is null. + [Obsolete(""This constructor is no longer supported and will be removed in a future version."", error: true)] public {{ provider.name }}(IServiceProvider provider) - => _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + => throw new NotSupportedException(SR.GetResourceString(SR.ID0403)); /// + [Obsolete(""This method is no longer supported and will be removed in a future version."", error: true)] public void PostConfigure(string? name, OpenIddictClientWebIntegrationOptions.{{ provider.name }} options) + => throw new NotSupportedException(SR.GetResourceString(SR.ID0403)); + + /// + [Obsolete(""This method is no longer supported and will be removed in a future version."", error: true)] + public void Configure(OpenIddictClientOptions options) + => throw new NotSupportedException(SR.GetResourceString(SR.ID0403)); + } + {{~ end ~}} + + static partial void ConfigureProvider(OpenIddictClientRegistration registration) + { + {{~ 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) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0406)); + } + {{~ for setting in provider.settings ~}} {{~ if setting.default_value && setting.type == 'String' ~}} - if (string.IsNullOrEmpty(options.{{ setting.property_name }})) + if (string.IsNullOrEmpty(settings.{{ setting.property_name }})) { - options.{{ setting.property_name }} = ""{{ setting.default_value }}""; + settings.{{ setting.property_name }} = ""{{ setting.default_value }}""; } {{~ end ~}} {{~ if setting.collection ~}} - if (options.{{ setting.property_name }}.Count is 0) + if (settings.{{ setting.property_name }}.Count is 0) { {{~ for item in setting.items ~}} {{~ if item.default && !item.required ~}} - options.{{ setting.property_name }}.Add(""{{ item.value }}""); + settings.{{ setting.property_name }}.Add(""{{ item.value }}""); {{~ end ~}} {{~ end ~}} } @@ -788,202 +900,175 @@ public sealed partial class OpenIddictClientWebIntegrationConfiguration {{~ for item in setting.items ~}} {{~ if item.required ~}} - options.{{ setting.property_name }}.Add(""{{ item.value }}""); + settings.{{ setting.property_name }}.Add(""{{ item.value }}""); {{~ end ~}} {{~ end ~}} {{~ end ~}} {{~ for environment in provider.environments ~}} - if (options.Environment is OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.{{ environment.name }}) + if (settings.Environment is OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.{{ environment.name }}) { - if (options.Scopes.Count is 0) + if (registration.Scopes.Count is 0) { {{~ for scope in environment.scopes ~}} {{~ if scope.default && !scope.required ~}} - options.Scopes.Add(""{{ scope.name }}""); + registration.Scopes.Add(""{{ scope.name }}""); {{~ end ~}} {{~ end ~}} } {{~ for scope in environment.scopes ~}} {{~ if scope.required ~}} - options.Scopes.Add(""{{ scope.name }}""); + registration.Scopes.Add(""{{ scope.name }}""); {{~ end ~}} {{~ end ~}} } {{~ end ~}} - if (string.IsNullOrEmpty(options.ClientId)) - { - throw new InvalidOperationException(SR.FormatID0332(nameof(options.ClientId), Providers.{{ provider.name }})); - } - {{~ for setting in provider.settings ~}} {{~ if setting.required ~}} {{~ if setting.type == 'String' ~}} - if (string.IsNullOrEmpty(options.{{ setting.property_name }})) + if (string.IsNullOrEmpty(settings.{{ setting.property_name }})) {{~ else ~}} - if (options.{{ setting.property_name }} is null) + if (settings.{{ setting.property_name }} is null) {{~ end ~}} { - throw new InvalidOperationException(SR.FormatID0332(nameof(options.{{ setting.property_name }}), Providers.{{ provider.name }})); + throw new InvalidOperationException(SR.FormatID0332(nameof(settings.{{ setting.property_name }}), Providers.{{ provider.name }})); } {{~ end ~}} {{~ if setting.type == 'Uri' ~}} - if (!options.{{ setting.property_name }}.IsAbsoluteUri || !options.{{ setting.property_name }}.IsWellFormedOriginalString()) + if (!settings.{{ setting.property_name }}.IsAbsoluteUri || !settings.{{ setting.property_name }}.IsWellFormedOriginalString()) { - throw new InvalidOperationException(SR.FormatID0350(nameof(options.{{ setting.property_name }}), Providers.{{ provider.name }})); + throw new InvalidOperationException(SR.FormatID0350(nameof(settings.{{ setting.property_name }}), Providers.{{ provider.name }})); } {{~ end ~}} {{~ end ~}} - } - /// - public void Configure(OpenIddictClientOptions options) - { - // Resolve the provider options from the service provider and create a registration based on the specified settings. - var settings = _provider.GetRequiredService>().CurrentValue; + registration.ProviderName ??= Providers.{{ provider.name }}; - var registration = new OpenIddictClientRegistration + registration.Issuer ??= settings.Environment switch { - ProviderName = Providers.{{ provider.name }}, - ProviderOptions = settings, + {{~ for environment in provider.environments ~}} + OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.{{ environment.name }} + => new Uri($""{{ environment.issuer | string.replace '\'' '""' }}"", UriKind.Absolute), + {{~ end ~}} - Issuer = settings.Environment switch - { - {{~ for environment in provider.environments ~}} - OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.{{ environment.name }} - => new Uri($""{{ environment.issuer | string.replace '\'' '""' }}"", UriKind.Absolute), - {{~ end ~}} + _ => throw new InvalidOperationException(SR.FormatID0194(nameof(settings.Environment))) + }; - _ => throw new InvalidOperationException(SR.FormatID0194(nameof(settings.Environment))) - }, + registration.ConfigurationEndpoint ??= settings.Environment switch + { + {{~ for environment in provider.environments ~}} + OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.{{ environment.name }} + {{~ if environment.configuration_endpoint ~}} + => new Uri($""{{ environment.configuration_endpoint | string.replace '\'' '""' }}"", UriKind.Absolute), + {{~ else ~}} + => null, + {{~ end ~}} + {{~ end ~}} + + _ => throw new InvalidOperationException(SR.FormatID0194(nameof(settings.Environment))) + }; - ConfigurationEndpoint = settings.Environment switch + registration.Configuration ??= settings.Environment switch + { + {{~ for environment in provider.environments ~}} + {{~ if environment.configuration ~}} + OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.{{ environment.name }} => new OpenIddictConfiguration { - {{~ for environment in provider.environments ~}} - OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.{{ environment.name }} - {{~ if environment.configuration_endpoint ~}} - => new Uri($""{{ environment.configuration_endpoint | string.replace '\'' '""' }}"", UriKind.Absolute), - {{~ else ~}} - => null, - {{~ end ~}} + {{~ if environment.configuration.authorization_endpoint ~}} + AuthorizationEndpoint = new Uri($""{{ environment.configuration.authorization_endpoint | string.replace '\'' '""' }}"", UriKind.Absolute), {{~ end ~}} - _ => throw new InvalidOperationException(SR.FormatID0194(nameof(settings.Environment))) - }, + {{~ if environment.configuration.device_authorization_endpoint ~}} + DeviceAuthorizationEndpoint = new Uri($""{{ environment.configuration.device_authorization_endpoint | string.replace '\'' '""' }}"", UriKind.Absolute), + {{~ end ~}} - ClientId = settings.ClientId, - ClientSecret = settings.ClientSecret, + {{~ if environment.configuration.token_endpoint ~}} + TokenEndpoint = new Uri($""{{ environment.configuration.token_endpoint | string.replace '\'' '""' }}"", UriKind.Absolute), + {{~ end ~}} - PostLogoutRedirectUri = settings.PostLogoutRedirectUri, - RedirectUri = settings.RedirectUri, + {{~ if environment.configuration.userinfo_endpoint ~}} + UserinfoEndpoint = new Uri($""{{ environment.configuration.userinfo_endpoint | string.replace '\'' '""' }}"", UriKind.Absolute), + {{~ end ~}} - Configuration = settings.Environment switch - { - {{~ for environment in provider.environments ~}} - {{~ if environment.configuration ~}} - OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.{{ environment.name }} => new OpenIddictConfiguration + CodeChallengeMethodsSupported = { - {{~ if environment.configuration.authorization_endpoint ~}} - AuthorizationEndpoint = new Uri($""{{ environment.configuration.authorization_endpoint | string.replace '\'' '""' }}"", UriKind.Absolute), + {{~ for method in environment.configuration.code_challenge_methods_supported ~}} + ""{{ method }}"", {{~ end ~}} + }, - {{~ if environment.configuration.device_authorization_endpoint ~}} - DeviceAuthorizationEndpoint = new Uri($""{{ environment.configuration.device_authorization_endpoint | string.replace '\'' '""' }}"", UriKind.Absolute), + GrantTypesSupported = + { + {{~ for type in environment.configuration.grant_types_supported ~}} + ""{{ type }}"", {{~ end ~}} + }, - {{~ if environment.configuration.token_endpoint ~}} - TokenEndpoint = new Uri($""{{ environment.configuration.token_endpoint | string.replace '\'' '""' }}"", UriKind.Absolute), + ResponseModesSupported = + { + {{~ for mode in environment.configuration.response_modes_supported ~}} + ""{{ mode }}"", {{~ end ~}} + }, - {{~ if environment.configuration.userinfo_endpoint ~}} - UserinfoEndpoint = new Uri($""{{ environment.configuration.userinfo_endpoint | string.replace '\'' '""' }}"", UriKind.Absolute), + ResponseTypesSupported = + { + {{~ for type in environment.configuration.response_types_supported ~}} + ""{{ type }}"", {{~ end ~}} + }, - CodeChallengeMethodsSupported = - { - {{~ for method in environment.configuration.code_challenge_methods_supported ~}} - ""{{ method }}"", - {{~ end ~}} - }, - - GrantTypesSupported = - { - {{~ for type in environment.configuration.grant_types_supported ~}} - ""{{ type }}"", - {{~ end ~}} - }, - - ResponseModesSupported = - { - {{~ for mode in environment.configuration.response_modes_supported ~}} - ""{{ mode }}"", - {{~ end ~}} - }, - - ResponseTypesSupported = - { - {{~ for type in environment.configuration.response_types_supported ~}} - ""{{ type }}"", - {{~ end ~}} - }, - - ScopesSupported = - { - {{~ for scope in environment.configuration.scopes_supported ~}} - ""{{ scope }}"", - {{~ end ~}} - }, - - DeviceAuthorizationEndpointAuthMethodsSupported = - { - {{~ for method in environment.configuration.device_authorization_endpoint_auth_methods_supported ~}} - ""{{ method }}"", - {{~ end ~}} - }, - - TokenEndpointAuthMethodsSupported = - { - {{~ for method in environment.configuration.token_endpoint_auth_methods_supported ~}} - ""{{ method }}"", - {{~ end ~}} - } + ScopesSupported = + { + {{~ for scope in environment.configuration.scopes_supported ~}} + ""{{ scope }}"", + {{~ end ~}} }, - {{~ else ~}} - OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.{{ environment.name }} => null, - {{~ end ~}} - {{~ end ~}} - _ => throw new InvalidOperationException(SR.FormatID0194(nameof(settings.Environment))) - }, + DeviceAuthorizationEndpointAuthMethodsSupported = + { + {{~ for method in environment.configuration.device_authorization_endpoint_auth_methods_supported ~}} + ""{{ method }}"", + {{~ end ~}} + }, - EncryptionCredentials = - { - {{~ for setting in provider.settings ~}} - {{~ if setting.type == 'EncryptionKey' ~}} - new EncryptingCredentials(settings.{{ setting.property_name }}, ""{{ setting.encryption_algorithm }}"", SecurityAlgorithms.Aes256CbcHmacSha512), - {{~ end ~}} - {{~ end ~}} + TokenEndpointAuthMethodsSupported = + { + {{~ for method in environment.configuration.token_endpoint_auth_methods_supported ~}} + ""{{ method }}"", + {{~ end ~}} + } }, + {{~ else ~}} + OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.{{ environment.name }} => null, + {{~ end ~}} + {{~ end ~}} - SigningCredentials = - { - {{~ for setting in provider.settings ~}} - {{~ if setting.type == 'SigningKey' ~}} - new SigningCredentials(settings.{{ setting.property_name }}, ""{{ setting.signing_algorithm }}""), - {{~ end ~}} - {{~ end ~}} - } + _ => throw new InvalidOperationException(SR.FormatID0194(nameof(settings.Environment))) }; - registration.Scopes.UnionWith(settings.Scopes); + {{~ for setting in provider.settings ~}} + {{~ if setting.type == 'EncryptionKey' ~}} + registration.EncryptionCredentials.Add(new EncryptingCredentials(settings.{{ setting.property_name }}, ""{{ setting.encryption_algorithm }}"", SecurityAlgorithms.Aes256CbcHmacSha512)); + {{~ end ~}} + {{~ end ~}} - options.Registrations.Add(registration); + {{~ for setting in provider.settings ~}} + {{~ if setting.type == 'SigningKey' ~}} + registration.SigningCredentials.Add(new SigningCredentials(settings.{{ setting.property_name }}, ""{{ setting.signing_algorithm }}"")); + {{~ end ~}} + {{~ end ~}} + } + {{~ end ~}} + + else + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0407)); } } - {{~ end ~}} } "); return template.Render(new @@ -1126,8 +1211,18 @@ public static partial class OpenIddictClientWebIntegrationHelpers /// The client registration. /// The {{ provider.display_name }} provider options. /// The provider options cannot be resolved. + [Obsolete($""This extension was replaced by {nameof(Get{{ provider.name }}Settings)} and will be removed in a future version."", error: true)] public static OpenIddictClientWebIntegrationOptions.{{ provider.name }} Get{{ provider.name }}Options(this OpenIddictClientRegistration registration) - => registration.ProviderOptions is OpenIddictClientWebIntegrationOptions.{{ provider.name }} options ? options : + => throw new NotSupportedException(SR.GetResourceString(SR.ID0403)); + + /// + /// Resolves the {{ provider.display_name }} provider settings from the specified registration. + /// + /// The client registration. + /// The {{ provider.display_name }} provider settings. + /// The provider options cannot be resolved. + public static OpenIddictClientWebIntegrationSettings.{{ provider.name }} Get{{ provider.name }}Settings(this OpenIddictClientRegistration registration) + => registration.ProviderSettings is OpenIddictClientWebIntegrationSettings.{{ provider.name }} settings ? settings : throw new InvalidOperationException(SR.FormatID0333(Providers.{{ provider.name }})); {{~ end ~}} @@ -1160,6 +1255,7 @@ public sealed partial class OpenIddictClientWebIntegrationOptions /// /// Provides various options needed to configure the {{ provider.display_name }} integration. /// + [Obsolete(""This class is no longer supported and will be removed in a future version."")] public sealed class {{ provider.name }} { /// @@ -1260,6 +1356,89 @@ public sealed partial class OpenIddictClientWebIntegrationOptions .ToList() }); } + + static string GenerateSettings(XDocument document) + { + var template = Template.Parse(@"#nullable enable + +using System.Security.Cryptography.X509Certificates; +using Microsoft.IdentityModel.Tokens; + +namespace OpenIddict.Client.WebIntegration; + +public sealed partial class OpenIddictClientWebIntegrationSettings +{ + {{~ for provider in providers ~}} + /// + /// Provides various options needed to configure the {{ provider.display_name }} integration. + /// + public sealed class {{ provider.name }} + { + /// + /// Gets or sets the environment that determines the endpoints to use (by default, ""Production""). + /// + public string? Environment { get; set; } = OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.Production; + + {{~ for setting in provider.settings ~}} + /// + /// Gets or sets {{ setting.description }}. + /// + {{~ if setting.obsolete ~}} + [Obsolete(""This option is no longer supported and will be removed in a future version."")] + {{~ end ~}} + {{~ if setting.collection ~}} + public HashSet<{{ setting.clr_type }}> {{ setting.property_name }} { get; } = new(); + {{~ else ~}} + public {{ setting.clr_type }}? {{ setting.property_name }} { get; set; } + {{~ end ~}} + + {{~ end ~}} + } + {{~ end ~}} +} +"); + return template.Render(new + { + Providers = document.Root.Elements("Provider") + .Select(provider => new + { + Name = (string) provider.Attribute("Name"), + DisplayName = (string?) provider.Attribute("DisplayName") ?? (string) provider.Attribute("Name"), + + Settings = provider.Elements("Setting").Select(setting => new + { + PropertyName = (string) setting.Attribute("PropertyName"), + + Collection = (bool?) setting.Attribute("Collection") ?? false, + Obsolete = (bool?) setting.Attribute("Obsolete") ?? false, + + Description = (string) setting.Attribute("Description") is string description ? + char.ToLower(description[0], CultureInfo.GetCultureInfo("en-US")) + description[1..] : null, + ClrType = (string) setting.Attribute("Type") switch + { + "EncryptionKey" when (string) setting.Element("EncryptionAlgorithm").Attribute("Value") + is "RS256" or "RS384" or "RS512" => "RsaSecurityKey", + + "SigningKey" when (string) setting.Element("SigningAlgorithm").Attribute("Value") + is "ES256" or "ES384" or "ES512" => "ECDsaSecurityKey", + + "SigningKey" when (string) setting.Element("SigningAlgorithm").Attribute("Value") + is "PS256" or "PS384" or "PS512" or + "RS256" or "RS384" or "RS512" => "RsaSecurityKey", + + "Certificate" => "X509Certificate2", + "String" => "string", + "StringHashSet" => "HashSet", + "Uri" => "Uri", + + string value => value + } + }) + .ToList() + }) + .ToList() + }); + } } public void Initialize(GeneratorInitializationContext context) diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Client/Controllers/AuthenticationController.cs b/sandbox/OpenIddict.Sandbox.AspNet.Client/Controllers/AuthenticationController.cs index b019190a..df0e6ef0 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Client/Controllers/AuthenticationController.cs +++ b/sandbox/OpenIddict.Sandbox.AspNet.Client/Controllers/AuthenticationController.cs @@ -98,16 +98,14 @@ namespace OpenIddict.Sandbox.AspNet.Client.Controllers // Remove the local authentication cookie before triggering a redirection to the remote server. context.Authentication.SignOut(CookieAuthenticationDefaults.AuthenticationType); - // Resolve the provider of the user identifier claim stored in the local authentication cookie. + // Extract the client registration identifier and retrieve the associated server configuration. // If the provider is known to support remote sign-out, ask OpenIddict to initiate a logout request. - if (Uri.TryCreate(identity.FindFirst(Claims.AuthorizationServer)?.Value, UriKind.Absolute, out Uri issuer) && - await _service.GetServerConfigurationAsync(issuer) is { EndSessionEndpoint: Uri }) + if (identity.FindFirst(Claims.Private.RegistrationId)?.Value is string identifier && + await _service.GetServerConfigurationByRegistrationIdAsync(identifier) is { EndSessionEndpoint: Uri }) { var properties = new AuthenticationProperties(new Dictionary { - // Note: when only one client is registered in the client options, - // setting the issuer property is not required and can be omitted. - [OpenIddictClientOwinConstants.Properties.Issuer] = issuer.AbsoluteUri, + [OpenIddictClientOwinConstants.Properties.RegistrationId] = identifier, // While not required, the specification encourages sending an id_token_hint // parameter containing an identity token returned by the server for this user. @@ -205,8 +203,10 @@ namespace OpenIddict.Sandbox.AspNet.Client.Controllers "http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider" } => true, - // Preserve the identity of the authorization server as a dedicated claim. - { Type: Claims.AuthorizationServer } => true, + // Preserve the client registration identifier as a dedicated claim so that the + // associated server configuration can be resolved from the logout endpoint to + // determine whether the authorization server supports client-initiated logouts. + { Type: Claims.Private.RegistrationId } => true, // Applications that use multiple client registrations can filter claims based on the issuer. { Type: "bio", Issuer: "https://github.com/" } => true, diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Client/Controllers/HomeController.cs b/sandbox/OpenIddict.Sandbox.AspNet.Client/Controllers/HomeController.cs index dd99411a..c58fe983 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Client/Controllers/HomeController.cs +++ b/sandbox/OpenIddict.Sandbox.AspNet.Client/Controllers/HomeController.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Threading; @@ -9,6 +8,7 @@ using System.Web.Mvc; using Microsoft.Owin.Security; using Microsoft.Owin.Security.Cookies; using OpenIddict.Client; +using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Client.Owin.OpenIddictClientOwinConstants; namespace OpenIddict.Sandbox.AspNet.Client.Controllers @@ -55,32 +55,34 @@ namespace OpenIddict.Sandbox.AspNet.Client.Controllers { var context = HttpContext.GetOwinContext(); - var result = await context.Authentication.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationType); - if (!result.Properties.Dictionary.TryGetValue(Tokens.RefreshToken, out string token)) + var ticket = await context.Authentication.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationType); + if (!ticket.Properties.Dictionary.TryGetValue(Tokens.RefreshToken, out string token)) { return new HttpStatusCodeResult(400); } - var (response, principal) = await _service.AuthenticateWithRefreshTokenAsync( - issuer: new Uri(result.Identity.Claims.Select(claim => claim.Issuer).First(), UriKind.Absolute), - token: token, - cancellationToken: cancellationToken); + var result = await _service.AuthenticateWithRefreshTokenAsync(new() + { + CancellationToken = cancellationToken, + RefreshToken = token, + RegistrationId = ticket.Identity.FindFirst(Claims.Private.RegistrationId)?.Value + }); - var properties = new AuthenticationProperties(result.Properties.Dictionary) + var properties = new AuthenticationProperties(ticket.Properties.Dictionary) { RedirectUri = null }; - properties.Dictionary[Tokens.BackchannelAccessToken] = response.AccessToken; + properties.Dictionary[Tokens.BackchannelAccessToken] = result.AccessToken; - if (!string.IsNullOrEmpty(response.RefreshToken)) + if (!string.IsNullOrEmpty(result.RefreshToken)) { - properties.Dictionary[Tokens.RefreshToken] = response.RefreshToken; + properties.Dictionary[Tokens.RefreshToken] = result.RefreshToken; } - context.Authentication.SignIn(properties, result.Identity); + context.Authentication.SignIn(properties, ticket.Identity); - return View("Index", model: response.AccessToken); + return View("Index", model: result.AccessToken); } } } diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs b/sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs index e3579324..fffe7f7f 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs +++ b/sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs @@ -83,13 +83,13 @@ namespace OpenIddict.Sandbox.AspNet.Client // parameter containing their URL as part of authorization responses. For more information, // see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.4. options.UseWebProviders() - .UseGitHub(options => + .AddGitHub(options => { options.SetClientId("c4ade52327b01ddacff3") .SetClientSecret("da6bed851b75e317bf6b2cb67013679d9467c122") .SetRedirectUri("callback/login/github"); }) - .UseGoogle(options => + .AddGoogle(options => { options.SetClientId("1016114395689-kgtgq2p6dj27d7v6e2kjkoj54dgrrckh.apps.googleusercontent.com") .SetClientSecret("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") diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Client/Web.config b/sandbox/OpenIddict.Sandbox.AspNet.Client/Web.config index 913c4bd7..2caf5a0f 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Client/Web.config +++ b/sandbox/OpenIddict.Sandbox.AspNet.Client/Web.config @@ -1,4 +1,4 @@ - + - - - @@ -82,7 +83,8 @@ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ --> - + @@ -94,7 +96,8 @@ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ --> - + - - + - @@ -183,7 +187,8 @@ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ --> - + - + - + - + - @@ -286,7 +294,8 @@ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ --> - + - + - + - + @@ -371,7 +383,8 @@ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ --> - + - + - + @@ -429,7 +444,8 @@ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ --> - + - + - + - - + - + - + - + - - @@ -655,7 +677,8 @@ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ --> - + - + - + - + - + - - + - + - + - + @@ -890,7 +921,8 @@ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ --> - + - + - + - + - + - + @@ -1070,7 +1107,8 @@ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ --> - + diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xsd b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xsd index 492b6e18..84603593 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xsd +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xsd @@ -473,6 +473,18 @@ + + + The provider identifier, represented as a GUID. + + + + + + + + + The documentation URI, if applicable. diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationSettings.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationSettings.cs new file mode 100644 index 00000000..42d6e149 --- /dev/null +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationSettings.cs @@ -0,0 +1,15 @@ +/* + * 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. + */ + +namespace OpenIddict.Client.WebIntegration; + +/// +/// Provides various settings needed to configure the OpenIddict client Web providers. +/// +public sealed partial class OpenIddictClientWebIntegrationSettings +{ + // Note: provider settings are automatically generated by the source generator. +} diff --git a/src/OpenIddict.Client/OpenIddictClientBuilder.cs b/src/OpenIddict.Client/OpenIddictClientBuilder.cs index 7d72f769..45d816cb 100644 --- a/src/OpenIddict.Client/OpenIddictClientBuilder.cs +++ b/src/OpenIddict.Client/OpenIddictClientBuilder.cs @@ -922,7 +922,6 @@ public sealed class OpenIddictClientBuilder /// specific OAuth 2.0 flow, visit https://tools.ietf.org/html/rfc8628. /// /// The instance. - [RequiresPreviewFeatures] public OpenIddictClientBuilder AllowDeviceCodeFlow() => Configure(options => options.GrantTypes.Add(GrantTypes.DeviceCode)); diff --git a/src/OpenIddict.Client/OpenIddictClientConfiguration.cs b/src/OpenIddict.Client/OpenIddictClientConfiguration.cs index 5f7c98b9..4c014dd2 100644 --- a/src/OpenIddict.Client/OpenIddictClientConfiguration.cs +++ b/src/OpenIddict.Client/OpenIddictClientConfiguration.cs @@ -6,6 +6,9 @@ using System.ComponentModel; using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; +using System.Text; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Tokens; @@ -43,7 +46,14 @@ public sealed class OpenIddictClientConfiguration : IPostConfigureOptions result, + null => SHA256.Create(), + var result => throw new CryptographicException(SR.FormatID0351(result.GetType().FullName)) + }; + + TransformBlock(algorithm, registration.Issuer.AbsoluteUri); + + if (!string.IsNullOrEmpty(registration.ProviderName)) + { + TransformBlock(algorithm, registration.ProviderName); + } + + algorithm.TransformFinalBlock(Array.Empty(), 0, 0); + + registration.RegistrationId = Base64UrlEncoder.Encode(algorithm.Hash); + } + if (registration.ConfigurationManager is null) { if (registration.Configuration is not null) @@ -165,24 +198,13 @@ public sealed class OpenIddictClientConfiguration : IPostConfigureOptions registration.Issuer) - .Distinct() - .Count()) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0342)); - } - - // Ensure provider names are not used in multiple client registrations. + // Ensure registration identifiers are not used in multiple client registrations. // - // Note: a string comparer ignoring casing is deliberately used to prevent - // two providers using the same name with a different casing from being added. - if (options.Registrations - .Where(registration => !string.IsNullOrEmpty(registration.ProviderName)) - .Count() != options.Registrations.Select(registration => registration.ProviderName) - .Where(name => !string.IsNullOrEmpty(name)) - .Distinct(StringComparer.OrdinalIgnoreCase) - .Count()) + // Note: a string comparer ignoring casing is deliberately used to prevent two + // registrations using the same identifier with a different casing from being added. + if (options.Registrations.Count != options.Registrations.Select(registration => registration.RegistrationId) + .Distinct(StringComparer.OrdinalIgnoreCase) + .Count()) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0347)); } @@ -281,5 +303,12 @@ public sealed class OpenIddictClientConfiguration : IPostConfigureOptions /// Gets or sets the principal containing the claims resolved from the token response. /// - [Obsolete] + [Obsolete("This property is no longer supported and will be removed in a future version.")] public ClaimsPrincipal? Principal { get; set; } } } diff --git a/src/OpenIddict.Client/OpenIddictClientEvents.cs b/src/OpenIddict.Client/OpenIddictClientEvents.cs index a5fd9355..76eaee4c 100644 --- a/src/OpenIddict.Client/OpenIddictClientEvents.cs +++ b/src/OpenIddict.Client/OpenIddictClientEvents.cs @@ -329,10 +329,22 @@ public static partial class OpenIddictClientEvents public string? Nonce { get; set; } /// - /// Gets or sets the issuer used for the authentication demand, if applicable. + /// Gets or sets the identifier that will be used to resolve the client registration, if applicable. + /// + public string? RegistrationId { get; set; } + + /// + /// Gets or sets the issuer URI of the provider that will be + /// used to resolve the client registration, if applicable. /// public Uri? Issuer { get; set; } + /// + /// Gets or sets the name of the provider that will be + /// used to resolve the client registration, if applicable. + /// + public string? ProviderName { get; set; } + /// /// Gets or sets the grant type used for the authentication demand, if applicable. /// @@ -878,13 +890,19 @@ public static partial class OpenIddictClientEvents public Dictionary Properties { get; } = new(StringComparer.Ordinal); /// - /// Gets or sets the issuer used for the challenge demand, if applicable. + /// Gets or sets the identifier that will be used to resolve the client registration, if applicable. + /// + public string? RegistrationId { get; set; } + + /// + /// Gets or sets the issuer URI of the provider that will be + /// used to resolve the client registration, if applicable. /// public Uri? Issuer { get; set; } /// /// Gets or sets the name of the provider that will be - /// used to resolve the issuer identity, if applicable. + /// used to resolve the client registration, if applicable. /// public string? ProviderName { get; set; } @@ -1189,13 +1207,19 @@ public static partial class OpenIddictClientEvents public Dictionary Properties { get; } = new(StringComparer.Ordinal); /// - /// Gets or sets the issuer used for the sign-out demand, if applicable. + /// Gets or sets the identifier that will be used to resolve the client registration, if applicable. + /// + public string? RegistrationId { get; set; } + + /// + /// Gets or sets the issuer URI of the provider that will be + /// used to resolve the client registration, if applicable. /// public Uri? Issuer { get; set; } /// /// Gets or sets the name of the provider that will be - /// used to resolve the issuer identity, if applicable. + /// used to resolve the client registration, if applicable. /// public string? ProviderName { get; set; } diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.cs index 0bcc69d5..b4dbfa47 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.cs @@ -325,15 +325,14 @@ public static partial class OpenIddictClientHandlers throw new InvalidOperationException(SR.GetResourceString(SR.ID0311)); } - // If no issuer was explicitly attached and a single client is registered, use it. - // Otherwise, throw an exception to indicate that setting an explicit issuer - // is required when multiple clients are registered. - context.Issuer ??= context.Options.Registrations.Count switch + if (context.Registration is null && string.IsNullOrEmpty(context.RegistrationId) && + context.Issuer is null && string.IsNullOrEmpty(context.ProviderName) && + context.Options.Registrations.Count is not 1) { - 0 => throw new InvalidOperationException(SR.GetResourceString(SR.ID0304)), - 1 => context.Options.Registrations[0].Issuer, - _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0355)) - }; + throw context.Options.Registrations.Count is 0 ? + new InvalidOperationException(SR.GetResourceString(SR.ID0304)) : + new InvalidOperationException(SR.GetResourceString(SR.ID0355)); + } } break; @@ -377,15 +376,48 @@ public static partial class OpenIddictClientHandlers // // Client registrations/configurations that need to be resolved as part of authentication demands // triggered from the redirection or post-logout redirection requests are handled elsewhere. - if (context.Issuer is null || context.EndpointType is not OpenIddictClientEndpointType.Unknown) + if (!string.IsNullOrEmpty(context.Nonce) || context.EndpointType is not OpenIddictClientEndpointType.Unknown) { return; } - // Note: if the static registration cannot be found in the options, this may indicate - // the client was removed after the authorization dance started and thus, can no longer - // be used to authenticate users. In this case, throw an exception to abort the flow. - context.Registration ??= await _service.GetClientRegistrationAsync(context.Issuer, context.CancellationToken); + context.Registration ??= context switch + { + // If specified, resolve the registration using the attached registration identifier. + { RegistrationId: string identifier } when !string.IsNullOrEmpty(identifier) + => await _service.GetClientRegistrationByIdAsync(identifier, context.CancellationToken), + + // If specified, resolve the registration using the attached issuer URI. + { Issuer: Uri issuer } => await _service.GetClientRegistrationAsync(issuer, context.CancellationToken), + + // If specified, resolve the registration using the attached provider name. + { ProviderName: string provider } when !string.IsNullOrEmpty(provider) + => await _service.GetClientRegistrationAsync(provider, context.CancellationToken), + + // Otherwise, default to the unique registration available, if possible. + { Options.Registrations: [OpenIddictClientRegistration registration] } => registration, + + // If no registration was added or multiple registrations are present, throw an exception. + { Options.Registrations: [] } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0304)), + { Options.Registrations: _ } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0355)) + }; + + if (!string.IsNullOrEmpty(context.RegistrationId) && + !string.Equals(context.RegistrationId, context.Registration.RegistrationId, StringComparison.Ordinal)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0348)); + } + + if (!string.IsNullOrEmpty(context.ProviderName) && + !string.Equals(context.ProviderName, context.Registration.ProviderName, StringComparison.Ordinal)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0349)); + } + + if (context.Issuer is not null && context.Issuer != context.Registration.Issuer) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0408)); + } try { @@ -967,19 +999,15 @@ public static partial class OpenIddictClientHandlers Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); - // Retrieve the client definition using the authorization server stored in the state token. + // Retrieve the client registration using the private claim stored in the state token. // // Note: there's no guarantee that the state token was not replaced by a malicious actor // with a state token meant to be used with a different authorization server as part of a // mix-up attack where the state token and the authorization code or access/identity tokens // wouldn't match. To mitigate this, additional defenses are added later by other handlers. - // Restore the identity of the authorization server from the special "as" claim. - // See https://datatracker.ietf.org/doc/html/draft-bradley-oauth-jwt-encoded-state-09#section-2 - // for more information. - var server = context.StateTokenPrincipal.GetClaim(Claims.AuthorizationServer); - if (string.IsNullOrEmpty(server) || !Uri.TryCreate(server, UriKind.Absolute, out Uri? issuer) || - !issuer.IsWellFormedOriginalString()) + context.RegistrationId = context.StateTokenPrincipal.GetClaim(Claims.Private.RegistrationId); + if (string.IsNullOrEmpty(context.RegistrationId)) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0291)); } @@ -987,17 +1015,7 @@ public static partial class OpenIddictClientHandlers // Note: if the static registration cannot be found in the options, this may indicate // the client was removed after the authorization dance started and thus, can no longer // be used to authenticate users. In this case, throw an exception to abort the flow. - context.Issuer = issuer; - context.Registration = await _service.GetClientRegistrationAsync(issuer, context.CancellationToken); - - // If an explicit provider name was also added, ensure the two values point to the same issuer. - var provider = context.StateTokenPrincipal.GetClaim(Claims.Private.ProviderName); - if (!string.IsNullOrEmpty(provider) && - !string.IsNullOrEmpty(context.Registration.ProviderName) && - !string.Equals(provider, context.Registration.ProviderName, StringComparison.Ordinal)) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0349)); - } + context.Registration = await _service.GetClientRegistrationByIdAsync(context.RegistrationId, context.CancellationToken); try { @@ -1046,8 +1064,6 @@ public static partial class OpenIddictClientHandlers throw new ArgumentNullException(nameof(context)); } - Debug.Assert(context.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013)); - // To help mitigate mix-up attacks, the identity of the issuer can be returned by // authorization servers that support it as a part of the "iss" parameter, which // allows comparing it to the issuer in the state token. Depending on the selected @@ -1078,7 +1094,7 @@ public static partial class OpenIddictClientHandlers // If the two values don't match, this may indicate a mix-up attack attempt. if (!Uri.TryCreate(issuer, UriKind.Absolute, out Uri? uri) || - !uri.IsWellFormedOriginalString() || uri != context.Issuer) + !uri.IsWellFormedOriginalString() || uri != context.Registration.Issuer) { context.Reject( error: Errors.InvalidRequest, @@ -2340,7 +2356,7 @@ public static partial class OpenIddictClientHandlers throw new ArgumentNullException(nameof(context)); } - Debug.Assert(context.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013)); + Debug.Assert(context.Registration.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013)); // Create a new principal that will be used to store the client assertion claims. var principal = new ClaimsPrincipal(new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType)); @@ -2365,7 +2381,7 @@ public static partial class OpenIddictClientHandlers // If the token endpoint URI is not available, use the issuer URI as the audience. else { - principal.SetAudiences(context.Issuer.OriginalString); + principal.SetAudiences(context.Registration.Issuer.OriginalString); } // Use the client_id as both the subject and the issuer, as required by the specifications. @@ -2546,7 +2562,8 @@ public static partial class OpenIddictClientHandlers try { context.TokenResponse = await _service.SendTokenRequestAsync( - context.Registration, context.TokenRequest, context.TokenEndpoint); + context.Registration, context.Configuration, + context.TokenRequest, context.TokenEndpoint); } catch (ProtocolException exception) @@ -3554,7 +3571,9 @@ public static partial class OpenIddictClientHandlers try { (context.UserinfoResponse, (context.UserinfoTokenPrincipal, context.UserinfoToken)) = - await _service.SendUserinfoRequestAsync(context.Registration, context.UserinfoRequest, context.UserinfoEndpoint); + await _service.SendUserinfoRequestAsync( + context.Registration, context.Configuration, + context.UserinfoRequest, context.UserinfoEndpoint); } catch (ProtocolException exception) @@ -3913,32 +3932,15 @@ public static partial class OpenIddictClientHandlers } } - // If a provider name was specified, resolve the corresponding issuer. - if (!string.IsNullOrEmpty(context.ProviderName)) + if (context.Registration is null && string.IsNullOrEmpty(context.RegistrationId) && + context.Issuer is null && string.IsNullOrEmpty(context.ProviderName) && + context.Options.Registrations.Count is not 1) { - var registration = context.Options.Registrations.Find(registration => string.Equals( - registration.ProviderName, context.ProviderName, StringComparison.Ordinal)) ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0348)); - - // If an explicit issuer was also attached, ensure the two values point to the same instance. - if (context.Issuer is not null && context.Issuer != registration.Issuer) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0349)); - } - - context.Issuer = registration.Issuer; + throw context.Options.Registrations.Count is 0 ? + new InvalidOperationException(SR.GetResourceString(SR.ID0304)) : + new InvalidOperationException(SR.GetResourceString(SR.ID0305)); } - // If no issuer was explicitly attached and a single client is registered, use it. - // Otherwise, throw an exception to indicate that setting an explicit issuer - // is required when multiple clients are registered. - context.Issuer ??= context.Options.Registrations.Count switch - { - 0 => throw new InvalidOperationException(SR.GetResourceString(SR.ID0304)), - 1 => context.Options.Registrations[0].Issuer, - _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0305)) - }; - return default; } } @@ -3971,12 +3973,43 @@ public static partial class OpenIddictClientHandlers throw new ArgumentNullException(nameof(context)); } - Debug.Assert(context.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013)); + context.Registration ??= context switch + { + // If specified, resolve the registration using the attached registration identifier. + { RegistrationId: string identifier } when !string.IsNullOrEmpty(identifier) + => await _service.GetClientRegistrationByIdAsync(identifier, context.CancellationToken), - // Note: if the static registration cannot be found in the options, this may indicate - // the client was removed after the authorization dance started and thus, can no longer - // be used to authenticate users. In this case, throw an exception to abort the flow. - context.Registration ??= await _service.GetClientRegistrationAsync(context.Issuer, context.CancellationToken); + // If specified, resolve the registration using the attached issuer URI. + { Issuer: Uri issuer } => await _service.GetClientRegistrationAsync(issuer, context.CancellationToken), + + // If specified, resolve the registration using the attached provider name. + { ProviderName: string provider } when !string.IsNullOrEmpty(provider) + => await _service.GetClientRegistrationAsync(provider, context.CancellationToken), + + // Otherwise, default to the unique registration available, if possible. + { Options.Registrations: [OpenIddictClientRegistration registration] } => registration, + + // If no registration was added or multiple registrations are present, throw an exception. + { Options.Registrations: [] } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0304)), + { Options.Registrations: _ } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0305)) + }; + + if (!string.IsNullOrEmpty(context.RegistrationId) && + !string.Equals(context.RegistrationId, context.Registration.RegistrationId, StringComparison.Ordinal)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0348)); + } + + if (!string.IsNullOrEmpty(context.ProviderName) && + !string.Equals(context.ProviderName, context.Registration.ProviderName, StringComparison.Ordinal)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0349)); + } + + if (context.Issuer is not null && context.Issuer != context.Registration.Issuer) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0408)); + } try { @@ -4704,7 +4737,7 @@ public static partial class OpenIddictClientHandlers throw new ArgumentNullException(nameof(context)); } - Debug.Assert(context.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013)); + Debug.Assert(context.Registration.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013)); Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); // Create a new principal containing only the filtered claims. @@ -4743,14 +4776,15 @@ public static partial class OpenIddictClientHandlers // Use the client identity as the token issuer. principal.SetClaim(Claims.Private.Issuer, (context.Options.ClientUri ?? context.BaseUri)?.AbsoluteUri); - // Store the identity of the authorization server in the state token principal to allow + // Store the identifier of the client registration in the state token principal to allow // resolving it when handling the authorization callback. Note: additional security checks // are generally required to ensure the state token was not replaced with a state token // meant to be used with a different authorization server (e.g using the "iss" parameter). // // See https://datatracker.ietf.org/doc/html/draft-bradley-oauth-jwt-encoded-state-09 - // for more information about this special claim. - principal.SetClaim(Claims.AuthorizationServer, context.Issuer.AbsoluteUri) + // for more information about the "as" claim. + principal.SetClaim(Claims.AuthorizationServer, context.Registration.Issuer.AbsoluteUri) + .SetClaim(Claims.Private.RegistrationId, context.Registration.RegistrationId) .SetClaim(Claims.Private.ProviderName, context.Registration.ProviderName); // Store the request forgery protection in the state token so it can be later used to @@ -5140,7 +5174,7 @@ public static partial class OpenIddictClientHandlers throw new ArgumentNullException(nameof(context)); } - Debug.Assert(context.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013)); + Debug.Assert(context.Registration.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013)); // Create a new principal that will be used to store the client assertion claims. var principal = new ClaimsPrincipal(new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType)); @@ -5154,7 +5188,7 @@ public static partial class OpenIddictClientHandlers // Use the issuer URI as the audience. Applications that need to // use a different value can register a custom event handler. - principal.SetAudiences(context.Issuer.OriginalString); + principal.SetAudiences(context.Registration.Issuer.OriginalString); // Use the client_id as both the subject and the issuer, as required by the specifications. principal.SetClaim(Claims.Private.Issuer, context.ClientId) @@ -5331,7 +5365,8 @@ public static partial class OpenIddictClientHandlers try { context.DeviceAuthorizationResponse = await _service.SendDeviceAuthorizationRequestAsync( - context.Registration, context.DeviceAuthorizationRequest, context.DeviceAuthorizationEndpoint); + context.Registration, context.Configuration, + context.DeviceAuthorizationRequest, context.DeviceAuthorizationEndpoint); } catch (ProtocolException exception) @@ -5517,32 +5552,15 @@ public static partial class OpenIddictClientHandlers throw new InvalidOperationException(SR.GetResourceString(SR.ID0358)); } - // If a provider name was specified, resolve the corresponding issuer. - if (!string.IsNullOrEmpty(context.ProviderName)) + if (context.Registration is null && string.IsNullOrEmpty(context.RegistrationId) && + context.Issuer is null && string.IsNullOrEmpty(context.ProviderName) && + context.Options.Registrations.Count is not 1) { - var registration = context.Options.Registrations.Find(registration => string.Equals( - registration.ProviderName, context.ProviderName, StringComparison.Ordinal)) ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0348)); - - // If an explicit issuer was also attached, ensure the two values point to the same instance. - if (context.Issuer is not null && context.Issuer != registration.Issuer) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0349)); - } - - context.Issuer = registration.Issuer; + throw context.Options.Registrations.Count is 0 ? + new InvalidOperationException(SR.GetResourceString(SR.ID0304)) : + new InvalidOperationException(SR.GetResourceString(SR.ID0341)); } - // If no issuer was explicitly attached and a single client is registered, use it. - // Otherwise, throw an exception to indicate that setting an explicit issuer - // is required when multiple clients are registered. - context.Issuer ??= context.Options.Registrations.Count switch - { - 0 => throw new InvalidOperationException(SR.GetResourceString(SR.ID0304)), - 1 => context.Options.Registrations[0].Issuer, - _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0341)) - }; - return default; } } @@ -5575,12 +5593,43 @@ public static partial class OpenIddictClientHandlers throw new ArgumentNullException(nameof(context)); } - Debug.Assert(context.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013)); + context.Registration ??= context switch + { + // If specified, resolve the registration using the attached registration identifier. + { RegistrationId: string identifier } when !string.IsNullOrEmpty(identifier) + => await _service.GetClientRegistrationByIdAsync(identifier, context.CancellationToken), - // Note: if the static registration cannot be found in the options, this may indicate - // the client was removed after the authorization dance started and thus, can no longer - // be used to authenticate users. In this case, throw an exception to abort the flow. - context.Registration ??= await _service.GetClientRegistrationAsync(context.Issuer, context.CancellationToken); + // If specified, resolve the registration using the attached issuer URI. + { Issuer: Uri issuer } => await _service.GetClientRegistrationAsync(issuer, context.CancellationToken), + + // If specified, resolve the registration using the attached provider name. + { ProviderName: string provider } when !string.IsNullOrEmpty(provider) + => await _service.GetClientRegistrationAsync(provider, context.CancellationToken), + + // Otherwise, default to the unique registration available, if possible. + { Options.Registrations: [OpenIddictClientRegistration registration] } => registration, + + // If no registration was added or multiple registrations are present, throw an exception. + { Options.Registrations: [] } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0304)), + { Options.Registrations: _ } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0341)) + }; + + if (!string.IsNullOrEmpty(context.RegistrationId) && + !string.Equals(context.RegistrationId, context.Registration.RegistrationId, StringComparison.Ordinal)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0348)); + } + + if (!string.IsNullOrEmpty(context.ProviderName) && + !string.Equals(context.ProviderName, context.Registration.ProviderName, StringComparison.Ordinal)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0349)); + } + + if (context.Issuer is not null && context.Issuer != context.Registration.Issuer) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0408)); + } try { @@ -5812,7 +5861,7 @@ public static partial class OpenIddictClientHandlers throw new ArgumentNullException(nameof(context)); } - Debug.Assert(context.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013)); + Debug.Assert(context.Registration.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013)); Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); // Create a new principal containing only the filtered claims. @@ -5851,12 +5900,13 @@ public static partial class OpenIddictClientHandlers // Use the client identity as the token issuer. principal.SetClaim(Claims.Private.Issuer, (context.Options.ClientUri ?? context.BaseUri)?.AbsoluteUri); - // Store the identity of the authorization server in the state token + // Store the identifier of the client registration in the state token // principal to allow resolving it when handling the post-logout callback. // // See https://datatracker.ietf.org/doc/html/draft-bradley-oauth-jwt-encoded-state-09 - // for more information about this special claim. - principal.SetClaim(Claims.AuthorizationServer, context.Issuer.AbsoluteUri) + // for more information about the "as" claim. + principal.SetClaim(Claims.AuthorizationServer, context.Registration.Issuer.AbsoluteUri) + .SetClaim(Claims.Private.RegistrationId, context.Registration.RegistrationId) .SetClaim(Claims.Private.ProviderName, context.Registration.ProviderName); // Store the request forgery protection in the state token so it can be later used to diff --git a/src/OpenIddict.Client/OpenIddictClientModels.cs b/src/OpenIddict.Client/OpenIddictClientModels.cs new file mode 100644 index 00000000..24740a1d --- /dev/null +++ b/src/OpenIddict.Client/OpenIddictClientModels.cs @@ -0,0 +1,760 @@ +/* + * 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.ComponentModel; +using System.Security.Claims; + +namespace OpenIddict.Client; + +/// +/// Exposes various records used to represent client requests and responses. +/// +public static class OpenIddictClientModels +{ + /// + /// Represents an interactive authentication request. + /// + public sealed record class InteractiveAuthenticationRequest + { + /// + /// Gets or sets the parameters that will be added to the token request. + /// + public Dictionary? AdditionalTokenRequestParameters { get; init; } + + /// + /// Gets or sets the cancellation token that will be + /// used to determine if the operation was aborted. + /// + public CancellationToken CancellationToken { get; init; } + + /// + /// Gets or sets the nonce that was returned during the challenge operation. + /// + public required string Nonce { get; init; } + + /// + /// Gets or sets the application-specific properties that will be added to the context. + /// + public Dictionary? Properties { get; init; } + + /// + /// Gets the scopes that will be sent to the authorization server. + /// + public List? Scopes { get; init; } + } + + /// + /// Represents an interactive authentication result. + /// + public sealed record class InteractiveAuthenticationResult + { + /// + /// Gets or sets the authorization code, if available. + /// + public required string? AuthorizationCode { get; init; } + + /// + /// Gets or sets the authorization response. + /// + public required OpenIddictResponse AuthorizationResponse { get; init; } + + /// + /// Gets or sets the backchannel access token, if available. + /// + public required string? BackchannelAccessToken { get; init; } + + /// + /// Gets or sets the backchannel identity token, if available. + /// + public required string? BackchannelIdentityToken { get; init; } + + /// + /// Gets or sets the principal extracted from the backchannel identity token, if available. + /// + [EditorBrowsable(EditorBrowsableState.Advanced)] + public required ClaimsPrincipal? BackchannelIdentityTokenPrincipal { get; init; } + + /// + /// Gets or sets the frontchannel access token, if available. + /// + public required string? FrontchannelAccessToken { get; init; } + + /// + /// Gets or sets the frontchannel identity token, if available. + /// + public required string? FrontchannelIdentityToken { get; init; } + + /// + /// Gets or sets the principal extracted from the frontchannel identity token, if available. + /// + [EditorBrowsable(EditorBrowsableState.Advanced)] + public required ClaimsPrincipal? FrontchannelIdentityTokenPrincipal { get; init; } + + /// + /// Gets or sets a merged principal containing all the claims + /// extracted from the identity token and userinfo token principals. + /// + public required ClaimsPrincipal Principal { get; init; } + + /// + /// Gets or sets the application-specific properties that were present in the context. + /// + public required Dictionary Properties { get; init; } + + /// + /// Gets or sets the refresh token, if available. + /// + public required string? RefreshToken { get; init; } + + /// + /// Gets or sets the principal extracted from the state token, if available. + /// + [EditorBrowsable(EditorBrowsableState.Advanced)] + public required ClaimsPrincipal? StateTokenPrincipal { get; init; } + + /// + /// Gets or sets the token response. + /// + public required OpenIddictResponse TokenResponse { get; init; } + + /// + /// Gets or sets the principal extracted from the userinfo token or response, if available. + /// + [EditorBrowsable(EditorBrowsableState.Advanced)] + public required ClaimsPrincipal? UserinfoTokenPrincipal { get; init; } + } + + /// + /// Represents an interactive challenge request. + /// + public sealed record class InteractiveChallengeRequest + { + /// + /// Gets or sets the parameters that will be added to the authorization request. + /// + public Dictionary? AdditionalAuthorizationRequestParameters { get; init; } + + /// + /// Gets or sets the cancellation token that will be + /// used to determine if the operation was aborted. + /// + public CancellationToken CancellationToken { get; init; } + + /// + /// Gets or sets the application-specific properties that will be added to the context. + /// + public Dictionary? Properties { get; init; } + + /// + /// Gets or sets the provider name used to resolve the client registration. + /// + /// + /// Note: if multiple client registrations use the same provider name. + /// the property must be explicitly set. + /// + public string? ProviderName { get; init; } + + /// + /// Gets or sets the unique identifier of the client registration that will be used. + /// + public string? RegistrationId { get; init; } + + /// + /// Gets the scopes that will be sent to the authorization server. + /// + public List? Scopes { get; init; } + + /// + /// Gets or sets the issuer used to resolve the client registration. + /// + /// + /// Note: if multiple client registrations point to the same issuer, + /// the property must be explicitly set. + /// + public Uri? Issuer { get; init; } + } + + /// + /// Represents an interactive challenge result. + /// + public sealed record class InteractiveChallengeResult + { + /// + /// Gets or sets the nonce that is used as a unique identifier for the challenge operation. + /// + public required string Nonce { get; init; } + + /// + /// Gets or sets the application-specific properties that were present in the context. + /// + public required Dictionary Properties { get; init; } + } + + /// + /// Represents a client credentials authentication request. + /// + public sealed record class ClientCredentialsAuthenticationRequest + { + /// + /// Gets or sets the parameters that will be added to the token request. + /// + public Dictionary? AdditionalTokenRequestParameters { get; init; } + + /// + /// Gets or sets the cancellation token that will be + /// used to determine if the operation was aborted. + /// + public CancellationToken CancellationToken { get; init; } + + /// + /// Gets or sets the application-specific properties that will be added to the context. + /// + public Dictionary? Properties { get; init; } + + /// + /// Gets or sets the provider name used to resolve the client registration. + /// + /// + /// Note: if multiple client registrations use the same provider name. + /// the property must be explicitly set. + /// + public string? ProviderName { get; init; } + + /// + /// Gets or sets the unique identifier of the client registration that will be used. + /// + public string? RegistrationId { get; init; } + + /// + /// Gets the scopes that will be sent to the authorization server. + /// + public List? Scopes { get; init; } + + /// + /// Gets or sets the issuer used to resolve the client registration. + /// + /// + /// Note: if multiple client registrations point to the same issuer, + /// the property must be explicitly set. + /// + public Uri? Issuer { get; init; } + } + + /// + /// Represents a client credentials authentication result. + /// + public sealed record class ClientCredentialsAuthenticationResult + { + /// + /// Gets or sets the access token. + /// + public required string AccessToken { get; init; } + + /// + /// Gets or sets the identity token, if available. + /// + /// + /// Note: this property is generally not set, unless when dealing with an identity + /// provider that returns an identity token for the client credentials grant. + /// + public required string? IdentityToken { get; init; } + + /// + /// Gets or sets the principal extracted from the identity token, if available. + /// + /// + /// Note: this property is generally not set, unless when dealing with an identity + /// provider that returns an identity token for the client credentials grant. + /// + [EditorBrowsable(EditorBrowsableState.Advanced)] + public required ClaimsPrincipal? IdentityTokenPrincipal { get; init; } + + /// + /// Gets or sets a merged principal containing all the claims + /// extracted from the identity token and userinfo token principals. + /// + /// + /// Note: in most cases, an empty principal will be returned, unless the authorization server + /// supports returning a non-standard identity token for the client credentials grant or + /// allows sending userinfo requests with an access token representing a client application. + /// + public required ClaimsPrincipal Principal { get; init; } + + /// + /// Gets or sets the application-specific properties that were present in the context. + /// + public required Dictionary Properties { get; init; } + + /// + /// Gets or sets the refresh token, if available. + /// + public required string? RefreshToken { get; init; } + + /// + /// Gets or sets the token response. + /// + public required OpenIddictResponse TokenResponse { get; init; } + + /// + /// Gets or sets the userinfo token, if available. + /// + /// + /// Note: this property is generally not set, unless when dealing with non-standard providers. + /// + public required string? UserinfoToken { get; init; } + + /// + /// Gets or sets the principal extracted from the userinfo token or response, if available. + /// + /// + /// Note: this property is generally not set, unless when dealing with non-standard providers. + /// + [EditorBrowsable(EditorBrowsableState.Advanced)] + public required ClaimsPrincipal? UserinfoTokenPrincipal { get; init; } + } + + /// + /// Represents a device authentication request. + /// + public sealed record class DeviceAuthenticationRequest + { + /// + /// Gets or sets the parameters that will be added to the token request. + /// + public Dictionary? AdditionalTokenRequestParameters { get; init; } + + /// + /// Gets or sets the cancellation token that will be + /// used to determine if the operation was aborted. + /// + public CancellationToken CancellationToken { get; init; } + + /// + /// Gets or sets the device code that will be sent to the authorization server. + /// + public required string DeviceCode { get; init; } + + /// + /// Gets or sets the maximum duration during which token requests will be sent + /// (typically, the same value as the "expires_in" parameter returned by the + /// authorization server during the challenge phase or a lower value). + /// + public required TimeSpan Timeout { get; init; } + + /// + /// Gets or sets the interval at which token requests will be sent (typically, the same + /// value as the one returned by the authorization server during the challenge phase). + /// + public required TimeSpan Interval { get; init; } + + /// + /// Gets or sets the application-specific properties that will be added to the context. + /// + public Dictionary? Properties { get; init; } + + /// + /// Gets or sets the provider name used to resolve the client registration. + /// + /// + /// Note: if multiple client registrations use the same provider name. + /// the property must be explicitly set. + /// + public string? ProviderName { get; init; } + + /// + /// Gets or sets the unique identifier of the client registration that will be used. + /// + public string? RegistrationId { get; init; } + + /// + /// Gets the scopes that will be sent to the authorization server. + /// + public List? Scopes { get; init; } + + /// + /// Gets or sets the issuer used to resolve the client registration. + /// + /// + /// Note: if multiple client registrations point to the same issuer, + /// the property must be explicitly set. + /// + public Uri? Issuer { get; init; } + } + + /// + /// Represents a device authentication result. + /// + public sealed record class DeviceAuthenticationResult + { + /// + /// Gets or sets the access token. + /// + public required string AccessToken { get; init; } + + /// + /// Gets or sets the identity token, if available. + /// + public required string? IdentityToken { get; init; } + + /// + /// Gets or sets the principal extracted from the identity token, if available. + /// + /// + [EditorBrowsable(EditorBrowsableState.Advanced)] + public required ClaimsPrincipal? IdentityTokenPrincipal { get; init; } + + /// + /// Gets or sets a merged principal containing all the claims + /// extracted from the identity token and userinfo token principals. + /// + public required ClaimsPrincipal Principal { get; init; } + + /// + /// Gets or sets the application-specific properties that were present in the context. + /// + public required Dictionary Properties { get; init; } + + /// + /// Gets or sets the refresh token, if available. + /// + public required string? RefreshToken { get; init; } + + /// + /// Gets or sets the token response. + /// + public required OpenIddictResponse TokenResponse { get; init; } + + /// + /// Gets or sets the userinfo token, if available. + /// + public required string? UserinfoToken { get; init; } + + /// + /// Gets or sets the principal extracted from the userinfo token or response, if available. + /// + [EditorBrowsable(EditorBrowsableState.Advanced)] + public required ClaimsPrincipal? UserinfoTokenPrincipal { get; init; } + } + + /// + /// Represents a device challenge request. + /// + public sealed record class DeviceChallengeRequest + { + /// + /// Gets or sets the parameters that will be added to the device authorization request. + /// + public Dictionary? AdditionalDeviceAuthorizationRequestParameters { get; init; } + + /// + /// Gets or sets the cancellation token that will be + /// used to determine if the operation was aborted. + /// + public CancellationToken CancellationToken { get; init; } + + /// + /// Gets or sets the application-specific properties that will be added to the context. + /// + public Dictionary? Properties { get; init; } + + /// + /// Gets or sets the provider name used to resolve the client registration. + /// + /// + /// Note: if multiple client registrations use the same provider name. + /// the property must be explicitly set. + /// + public string? ProviderName { get; init; } + + /// + /// Gets or sets the unique identifier of the client registration that will be used. + /// + public string? RegistrationId { get; init; } + + /// + /// Gets the scopes that will be sent to the authorization server. + /// + public List? Scopes { get; init; } + + /// + /// Gets or sets the issuer used to resolve the client registration. + /// + /// + /// Note: if multiple client registrations point to the same issuer, + /// the property must be explicitly set. + /// + public Uri? Issuer { get; init; } + } + + /// + /// Represents a device challenge result. + /// + public sealed record class DeviceChallengeResult + { + /// + /// Gets or sets the device authorization response. + /// + public required OpenIddictResponse DeviceAuthorizationResponse { get; init; } + + /// + /// Gets or sets the device code. + /// + public required string DeviceCode { get; init; } + + /// + /// Gets or sets the remaining lifetime of the device and user codes. + /// + public required TimeSpan ExpiresIn { get; init; } + + /// + /// Gets or sets the interval at which token requests should be sent. + /// + public required TimeSpan Interval { get; init; } + + /// + /// Gets or sets the application-specific properties that were present in the context. + /// + public required Dictionary Properties { get; init; } + + /// + /// Gets or sets the user code. + /// + public required string UserCode { get; init; } + + /// + /// Gets or sets the verification URI. + /// + public required Uri VerificationUri { get; init; } + + /// + /// Gets or sets the complete verification URI, if available. + /// + public Uri? VerificationUriComplete { get; init; } + } + + /// + /// Represents a resource owner password credentials authentication request. + /// + public sealed record class PasswordAuthenticationRequest + { + /// + /// Gets or sets the parameters that will be added to the token request. + /// + public Dictionary? AdditionalTokenRequestParameters { get; init; } + + /// + /// Gets or sets the cancellation token that will be + /// used to determine if the operation was aborted. + /// + public CancellationToken CancellationToken { get; init; } + + /// + /// Gets or sets the password that will be sent to the authorization server. + /// + public required string Password { get; init; } + + /// + /// Gets or sets the application-specific properties that will be added to the context. + /// + public Dictionary? Properties { get; init; } + + /// + /// Gets or sets the provider name used to resolve the client registration. + /// + /// + /// Note: if multiple client registrations use the same provider name. + /// the property must be explicitly set. + /// + public string? ProviderName { get; init; } + + /// + /// Gets or sets the unique identifier of the client registration that will be used. + /// + public string? RegistrationId { get; init; } + + /// + /// Gets the scopes that will be sent to the authorization server. + /// + public List? Scopes { get; init; } + + /// + /// Gets or sets the username that will be sent to the authorization server. + /// + public required string Username { get; init; } + + /// + /// Gets or sets the issuer used to resolve the client registration. + /// + /// + /// Note: if multiple client registrations point to the same issuer, + /// the property must be explicitly set. + /// + public Uri? Issuer { get; init; } + } + + /// + /// Represents a resource owner password credentials authentication result. + /// + public sealed record class PasswordAuthenticationResult + { + /// + /// Gets or sets the access token. + /// + public required string AccessToken { get; init; } + + /// + /// Gets or sets the identity token, if available. + /// + public required string? IdentityToken { get; init; } + + /// + /// Gets or sets the principal extracted from the identity token, if available. + /// + [EditorBrowsable(EditorBrowsableState.Advanced)] + public required ClaimsPrincipal? IdentityTokenPrincipal { get; init; } + + /// + /// Gets or sets a merged principal containing all the claims + /// extracted from the identity token and userinfo token principals. + /// + public required ClaimsPrincipal Principal { get; init; } + + /// + /// Gets or sets the application-specific properties that were present in the context. + /// + public required Dictionary Properties { get; init; } + + /// + /// Gets or sets the refresh token, if available. + /// + public required string? RefreshToken { get; init; } + + /// + /// Gets or sets the token response. + /// + public required OpenIddictResponse TokenResponse { get; init; } + + /// + /// Gets or sets the userinfo token, if available. + /// + public required string? UserinfoToken { get; init; } + + /// + /// Gets or sets the principal extracted from the userinfo token or response, if available. + /// + [EditorBrowsable(EditorBrowsableState.Advanced)] + public required ClaimsPrincipal? UserinfoTokenPrincipal { get; init; } + } + + /// + /// Represents a refresh token authentication request. + /// + public sealed record class RefreshTokenAuthenticationRequest + { + /// + /// Gets or sets the parameters that will be added to the token request. + /// + public Dictionary? AdditionalTokenRequestParameters { get; init; } + + /// + /// Gets or sets the cancellation token that will be + /// used to determine if the operation was aborted. + /// + public CancellationToken CancellationToken { get; init; } + + /// + /// Gets or sets the application-specific properties that will be added to the context. + /// + public Dictionary? Properties { get; init; } + + /// + /// Gets or sets the provider name used to resolve the client registration. + /// + /// + /// Note: if multiple client registrations use the same provider name. + /// the property must be explicitly set. + /// + public string? ProviderName { get; init; } + + /// + /// Gets or sets the unique identifier of the client registration that will be used. + /// + public string? RegistrationId { get; init; } + + /// + /// Gets the scopes that will be sent to the authorization server. + /// + public List? Scopes { get; init; } + + /// + /// Gets or sets the refresh token that will be sent to the authorization server. + /// + public required string RefreshToken { get; init; } + + /// + /// Gets or sets the issuer used to resolve the client registration. + /// + /// + /// Note: if multiple client registrations point to the same issuer, + /// the property must be explicitly set. + /// + public Uri? Issuer { get; init; } + } + + /// + /// Represents a refresh token authentication result. + /// + public sealed record class RefreshTokenAuthenticationResult + { + /// + /// Gets or sets the access token. + /// + public required string AccessToken { get; init; } + + /// + /// Gets or sets the identity token, if available. + /// + public required string? IdentityToken { get; init; } + + /// + /// Gets or sets the principal extracted from the identity token, if available. + /// + [EditorBrowsable(EditorBrowsableState.Advanced)] + public required ClaimsPrincipal? IdentityTokenPrincipal { get; init; } + + /// + /// Gets or sets a merged principal containing all the claims + /// extracted from the identity token and userinfo token principals. + /// + public required ClaimsPrincipal Principal { get; init; } + + /// + /// Gets or sets the application-specific properties that were present in the context. + /// + public required Dictionary Properties { get; init; } + + /// + /// Gets or sets the refresh token, if available. + /// + public required string? RefreshToken { get; init; } + + /// + /// Gets or sets the token response. + /// + public required OpenIddictResponse TokenResponse { get; init; } + + /// + /// Gets or sets the userinfo token, if available. + /// + public required string? UserinfoToken { get; init; } + + /// + /// Gets or sets the principal extracted from the userinfo token or response, if available. + /// + [EditorBrowsable(EditorBrowsableState.Advanced)] + public required ClaimsPrincipal? UserinfoTokenPrincipal { get; init; } + } +} diff --git a/src/OpenIddict.Client/OpenIddictClientRegistration.cs b/src/OpenIddict.Client/OpenIddictClientRegistration.cs index 68ffa056..224a8862 100644 --- a/src/OpenIddict.Client/OpenIddictClientRegistration.cs +++ b/src/OpenIddict.Client/OpenIddictClientRegistration.cs @@ -16,6 +16,11 @@ namespace OpenIddict.Client; [DebuggerDisplay("{Issuer,nq}")] public sealed class OpenIddictClientRegistration { + /// + /// Gets or sets the unique identifier assigned to the registration. + /// + public string? RegistrationId { get; set; } + /// /// Gets or sets the client identifier assigned by the authorization server. /// @@ -104,18 +109,36 @@ public sealed class OpenIddictClientRegistration public Uri? Issuer { get; set; } /// - /// Gets or sets the provider name, if applicable. + /// Gets or sets the provider name. /// /// - /// If a Web provider integration with the same name was enabled, the - /// provider-specific options will be automatically imported and applied. + /// The provider name can be safely used as a stable public identifier. /// public string? ProviderName { get; set; } /// /// Gets or sets the provider options, if applicable. /// - public dynamic? ProviderOptions { get; set; } + [Obsolete($"This property was replaced by {nameof(ProviderSettings)} and will be removed in a future version.")] + public dynamic? ProviderOptions + { + get => throw new NotSupportedException(SR.GetResourceString(SR.ID0403)); + set => throw new NotSupportedException(SR.GetResourceString(SR.ID0403)); + } + + /// + /// Gets or sets the provider settings, if applicable. + /// + public dynamic? ProviderSettings { get; set; } + + /// + /// Gets or sets the provider type, if applicable. + /// + /// + /// Note: when manually set, the specified value MUST match the type of an existing + /// provider supported by the OpenIddict.Client.WebIntegration companion package. + /// + public string? ProviderType { get; set; } /// /// Gets or sets the static server configuration, if applicable. diff --git a/src/OpenIddict.Client/OpenIddictClientService.cs b/src/OpenIddict.Client/OpenIddictClientService.cs index 757ec36c..eacdc482 100644 --- a/src/OpenIddict.Client/OpenIddictClientService.cs +++ b/src/OpenIddict.Client/OpenIddictClientService.cs @@ -5,7 +5,6 @@ */ using System.Diagnostics; -using System.Runtime.Versioning; using System.Security.Claims; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -13,6 +12,7 @@ using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using OpenIddict.Extensions; using static OpenIddict.Abstractions.OpenIddictExceptions; +using static OpenIddict.Client.OpenIddictClientModels; namespace OpenIddict.Client; @@ -37,6 +37,9 @@ public sealed class OpenIddictClientService /// /// No was registered with the specified . /// + /// + /// Multiple instances share the same . + /// public ValueTask GetClientRegistrationAsync( Uri issuer, CancellationToken cancellationToken = default) { @@ -52,8 +55,16 @@ public sealed class OpenIddictClientService var options = _provider.GetRequiredService>(); - return new(options.CurrentValue.Registrations.Find(registration => registration.Issuer == issuer) ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0292))); + return options.CurrentValue.Registrations.FindAll(registration => registration.Issuer == issuer) switch + { + [var registration] => new(registration), + + [] => new(Task.FromException( + new InvalidOperationException(SR.GetResourceString(SR.ID0292)))), + + _ => new(Task.FromException( + new InvalidOperationException(SR.GetResourceString(SR.ID0404)))) + }; } /// @@ -81,9 +92,47 @@ public sealed class OpenIddictClientService var options = _provider.GetRequiredService>(); + return options.CurrentValue.Registrations.FindAll(registration => string.Equals( + registration.ProviderName, provider, StringComparison.Ordinal)) switch + { + [var registration] => new(registration), + + [] => new(Task.FromException( + new InvalidOperationException(SR.GetResourceString(SR.ID0397)))), + + _ => new(Task.FromException( + new InvalidOperationException(SR.GetResourceString(SR.ID0409)))) + }; + } + + /// + /// Resolves the client registration associated with the specified . + /// + /// The registration identifier. + /// The that can be used to abort the operation. + /// The associated with the specified . + /// is or empty. + /// + /// No was registered with the specified . + /// + public ValueTask GetClientRegistrationByIdAsync( + string identifier, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException(SR.FormatID0366(nameof(identifier)), nameof(identifier)); + } + + if (cancellationToken.IsCancellationRequested) + { + return new(Task.FromCanceled(cancellationToken)); + } + + var options = _provider.GetRequiredService>(); + return new(options.CurrentValue.Registrations.Find(registration => string.Equals( - registration.ProviderName, provider, StringComparison.Ordinal)) ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0397))); + registration.RegistrationId, identifier, StringComparison.Ordinal)) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0410))); } /// @@ -121,6 +170,9 @@ public sealed class OpenIddictClientService /// /// No was registered with the specified . /// + /// + /// Multiple instances share the same . + /// public async ValueTask GetServerConfigurationAsync( string provider, CancellationToken cancellationToken = default) { @@ -136,6 +188,31 @@ public sealed class OpenIddictClientService throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); } + /// + /// Resolves the server configuration associated with the specified registration . + /// + /// The registration identifier. + /// The that can be used to abort the operation. + /// The associated with the specified . + /// is or empty. + /// + /// No was registered with the specified . + /// + public async ValueTask GetServerConfigurationByRegistrationIdAsync( + string identifier, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException(SR.FormatID0366(nameof(identifier)), nameof(identifier)); + } + + var registration = await GetClientRegistrationByIdAsync(identifier, cancellationToken); + return await registration.ConfigurationManager + .GetConfigurationAsync(cancellationToken) + .WaitAsync(cancellationToken) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); + } + /// /// Initiates an interactive user authentication demand. /// @@ -145,7 +222,7 @@ public sealed class OpenIddictClientService /// The application-specific properties that will be added to the authentication context. /// The that can be used to abort the operation. /// The response and a merged principal containing the claims extracted from the tokens and userinfo response. - [RequiresPreviewFeatures] + [Obsolete("This method is obsolete and will be removed in a future version.")] public async ValueTask<(OpenIddictResponse AuthorizationResponse, OpenIddictResponse TokenResponse, ClaimsPrincipal Principal)> AuthenticateInteractivelyAsync( Uri issuer, string[]? scopes = null, Dictionary? parameters = null, @@ -156,9 +233,23 @@ public sealed class OpenIddictClientService throw new ArgumentNullException(nameof(issuer)); } - var registration = await GetClientRegistrationAsync(issuer, cancellationToken); - var nonce = await ChallengeInteractivelyAsync(registration, scopes, parameters, properties, cancellationToken); - return await AuthenticateInteractivelyAsync(nonce, cancellationToken); + var nonce = (await ChallengeInteractivelyAsync(new() + { + AdditionalAuthorizationRequestParameters = parameters, + CancellationToken = cancellationToken, + Issuer = issuer, + Properties = properties!, + Scopes = scopes?.ToList() + })).Nonce; + + var result = await AuthenticateInteractivelyAsync(new() + { + CancellationToken = cancellationToken, + Nonce = nonce, + Properties = properties! + }); + + return (result.AuthorizationResponse, result.TokenResponse, result.Principal); } /// @@ -170,7 +261,7 @@ public sealed class OpenIddictClientService /// The application-specific properties that will be added to the authentication context. /// The that can be used to abort the operation. /// The response and a merged principal containing the claims extracted from the tokens and userinfo response. - [RequiresPreviewFeatures] + [Obsolete("This method is obsolete and will be removed in a future version.")] public async ValueTask<(OpenIddictResponse AuthorizationResponse, OpenIddictResponse TokenResponse, ClaimsPrincipal Principal)> AuthenticateInteractivelyAsync( string provider, string[]? scopes = null, Dictionary? parameters = null, @@ -181,22 +272,38 @@ public sealed class OpenIddictClientService throw new ArgumentException(SR.FormatID0366(nameof(provider)), nameof(provider)); } - var registration = await GetClientRegistrationAsync(provider, cancellationToken); - var nonce = await ChallengeInteractivelyAsync(registration, scopes, parameters, properties, cancellationToken); - return await AuthenticateInteractivelyAsync(nonce, cancellationToken); + var nonce = (await ChallengeInteractivelyAsync(new() + { + AdditionalAuthorizationRequestParameters = parameters, + CancellationToken = cancellationToken, + Properties = properties!, + ProviderName = provider, + Scopes = scopes?.ToList() + })).Nonce; + + var result = await AuthenticateInteractivelyAsync(new() + { + CancellationToken = cancellationToken, + Nonce = nonce, + Properties = properties! + }); + + return (result.AuthorizationResponse, result.TokenResponse, result.Principal); } /// /// Completes the interactive authentication demand corresponding to the specified nonce. /// - /// The nonce obtained after a challenge operation. - /// The that can be used to abort the operation. - /// The response and a merged principal containing the claims extracted from the tokens and userinfo response. - [RequiresPreviewFeatures] - private async ValueTask<(OpenIddictResponse AuthorizationResponse, OpenIddictResponse TokenResponse, ClaimsPrincipal Principal)> AuthenticateInteractivelyAsync( - string nonce, CancellationToken cancellationToken = default) + /// The interactive authentication request. + /// The interactive authentication result. + public async ValueTask AuthenticateInteractivelyAsync(InteractiveAuthenticationRequest request) { - cancellationToken.ThrowIfCancellationRequested(); + if (request is null) + { + throw new ArgumentNullException(nameof(request)); + } + + request.CancellationToken.ThrowIfCancellationRequested(); // Note: this service is registered as a singleton service. As such, it cannot // directly depend on scoped services like the validation provider. To work around @@ -214,8 +321,8 @@ public sealed class OpenIddictClientService var context = new ProcessAuthenticationContext(transaction) { - CancellationToken = cancellationToken, - Nonce = nonce + CancellationToken = request.CancellationToken, + Nonce = request.Nonce }; await dispatcher.DispatchAsync(context); @@ -229,22 +336,30 @@ public sealed class OpenIddictClientService else { - Debug.Assert(context.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013)); - - var principal = OpenIddictHelpers.CreateMergedPrincipal( - context.FrontchannelIdentityTokenPrincipal, - context.BackchannelIdentityTokenPrincipal, - context.UserinfoTokenPrincipal) ?? new ClaimsPrincipal(new ClaimsIdentity()); + Debug.Assert(context.Registration.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013)); - // Attach the identity of the authorization server to the returned principal to allow resolving it even if no other - // claim was added to the principal (e.g when no id_token was returned and no userinfo endpoint is available). - principal.SetClaim(Claims.AuthorizationServer, context.Issuer.AbsoluteUri) - .SetClaim(Claims.Private.ProviderName, context.Registration.ProviderName); - - return ( - AuthorizationResponse: context.Request is not null ? new(context.Request.GetParameters()) : new(), - TokenResponse : context.TokenResponse ?? new(), - Principal : principal); + return new() + { + AuthorizationCode = context.AuthorizationCode, + AuthorizationResponse = context.Request is not null ? new(context.Request.GetParameters()) : new(), + BackchannelAccessToken = context.BackchannelAccessToken, + BackchannelIdentityToken = context.BackchannelIdentityToken, + BackchannelIdentityTokenPrincipal = context.BackchannelIdentityTokenPrincipal, + FrontchannelAccessToken = context.FrontchannelAccessToken, + FrontchannelIdentityToken = context.FrontchannelIdentityToken, + FrontchannelIdentityTokenPrincipal = context.FrontchannelIdentityTokenPrincipal, + Principal = OpenIddictHelpers.CreateMergedPrincipal(context.FrontchannelIdentityTokenPrincipal, + context.BackchannelIdentityTokenPrincipal, + context.UserinfoTokenPrincipal) + .SetClaim(Claims.AuthorizationServer, context.Registration.Issuer.AbsoluteUri) + .SetClaim(Claims.Private.RegistrationId, context.Registration.RegistrationId) + .SetClaim(Claims.Private.ProviderName, context.Registration.ProviderName), + Properties = context.Properties, + RefreshToken = context.RefreshToken, + StateTokenPrincipal = context.StateTokenPrincipal, + TokenResponse = context.TokenResponse ?? new(), + UserinfoTokenPrincipal = context.UserinfoTokenPrincipal + }; } } @@ -265,34 +380,16 @@ public sealed class OpenIddictClientService /// /// Initiates an interactive user authentication demand. /// - /// The client registration. - /// The scopes to request to the authorization server. - /// The additional parameters to send as part of the token request. - /// The application-specific properties that will be added to the authentication context. - /// The that can be used to abort the operation. - /// The response and a merged principal containing the claims extracted from the tokens and userinfo response. - [RequiresPreviewFeatures] - private async ValueTask ChallengeInteractivelyAsync( - OpenIddictClientRegistration registration, string[]? scopes = null, - Dictionary? parameters = null, - Dictionary? properties = null, CancellationToken cancellationToken = default) + /// The interactive challenge request. + /// The interactive challenge result. + public async ValueTask ChallengeInteractivelyAsync(InteractiveChallengeRequest request) { - if (registration is null) - { - throw new ArgumentNullException(nameof(registration)); - } - - if (scopes is not null && Array.Exists(scopes, string.IsNullOrEmpty)) + if (request is null) { - throw new ArgumentException(SR.GetResourceString(SR.ID0074), nameof(scopes)); + throw new ArgumentNullException(nameof(request)); } - var configuration = await registration.ConfigurationManager - .GetConfigurationAsync(cancellationToken) - .WaitAsync(cancellationToken) ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); - - cancellationToken.ThrowIfCancellationRequested(); + request.CancellationToken.ThrowIfCancellationRequested(); // Note: this service is registered as a singleton service. As such, it cannot // directly depend on scoped services like the validation provider. To work around @@ -310,22 +407,23 @@ public sealed class OpenIddictClientService var context = new ProcessChallengeContext(transaction) { - CancellationToken = cancellationToken, - Configuration = configuration, - Issuer = registration.Issuer, + CancellationToken = request.CancellationToken, + Issuer = request.Issuer, Principal = new ClaimsPrincipal(new ClaimsIdentity()), - Registration = registration, - Request = parameters is not null ? new(parameters) : new(), + ProviderName = request.ProviderName, + RegistrationId = request.RegistrationId, + Request = request.AdditionalAuthorizationRequestParameters + is Dictionary parameters ? new(parameters) : new(), }; - if (scopes is { Length: > 0 }) + if (request.Scopes is { Count: > 0 }) { - context.Scopes.UnionWith(scopes); + context.Scopes.UnionWith(request.Scopes); } - if (properties is { Count: > 0 }) + if (request.Properties is { Count: > 0 }) { - foreach (var property in properties) + foreach (var property in request.Properties) { context.Properties[property.Key] = property.Value; } @@ -345,7 +443,11 @@ public sealed class OpenIddictClientService throw new InvalidOperationException(SR.GetResourceString(SR.ID0352)); } - return context.Nonce; + return new() + { + Nonce = context.Nonce, + Properties = context.Properties + }; } finally @@ -363,7 +465,7 @@ public sealed class OpenIddictClientService } /// - /// Authenticates using the client credentials grant and resolves the corresponding tokens. + /// Authenticates using the client credentials grant. /// /// The issuer. /// The scopes to request to the authorization server. @@ -371,6 +473,7 @@ public sealed class OpenIddictClientService /// The application-specific properties that will be added to the authentication context. /// The that can be used to abort the operation. /// The response and a merged principal containing the claims extracted from the tokens and userinfo response. + [Obsolete("This method is obsolete and will be removed in a future version.")] public async ValueTask<(OpenIddictResponse Response, ClaimsPrincipal Principal)> AuthenticateWithClientCredentialsAsync( Uri issuer, string[]? scopes = null, Dictionary? parameters = null, @@ -381,12 +484,20 @@ public sealed class OpenIddictClientService throw new ArgumentNullException(nameof(issuer)); } - var registration = await GetClientRegistrationAsync(issuer, cancellationToken); - return await AuthenticateWithClientCredentialsAsync(registration, scopes, parameters, properties, cancellationToken); + var result = await AuthenticateWithClientCredentialsAsync(new() + { + AdditionalTokenRequestParameters = parameters, + CancellationToken = cancellationToken, + Issuer = issuer, + Properties = properties!, + Scopes = scopes?.ToList() + }); + + return (result.TokenResponse, result.Principal); } /// - /// Authenticates using the client credentials grant and resolves the corresponding tokens. + /// Authenticates using the client credentials grant. /// /// The name of the provider (see ). /// The scopes to request to the authorization server. @@ -394,6 +505,7 @@ public sealed class OpenIddictClientService /// The application-specific properties that will be added to the authentication context. /// The that can be used to abort the operation. /// The response and a merged principal containing the claims extracted from the tokens and userinfo response. + [Obsolete("This method is obsolete and will be removed in a future version.")] public async ValueTask<(OpenIddictResponse Response, ClaimsPrincipal Principal)> AuthenticateWithClientCredentialsAsync( string provider, string[]? scopes = null, Dictionary? parameters = null, @@ -404,45 +516,32 @@ public sealed class OpenIddictClientService throw new ArgumentException(SR.FormatID0366(nameof(provider)), nameof(provider)); } - var registration = await GetClientRegistrationAsync(provider, cancellationToken); - return await AuthenticateWithClientCredentialsAsync(registration, scopes, parameters, properties, cancellationToken); + var result = await AuthenticateWithClientCredentialsAsync(new() + { + AdditionalTokenRequestParameters = parameters, + CancellationToken = cancellationToken, + Properties = properties!, + ProviderName = provider, + Scopes = scopes?.ToList() + }); + + return (result.TokenResponse, result.Principal); } /// - /// Authenticates using the client credentials grant and resolves the corresponding tokens. + /// Authenticates using the client credentials grant. /// - /// The client registration. - /// The scopes to request to the authorization server. - /// The additional parameters to send as part of the token request. - /// The application-specific properties that will be added to the authentication context. - /// The that can be used to abort the operation. - /// The response and a merged principal containing the claims extracted from the tokens and userinfo response. - private async ValueTask<(OpenIddictResponse Response, ClaimsPrincipal Principal)> AuthenticateWithClientCredentialsAsync( - OpenIddictClientRegistration registration, string[]? scopes = null, - Dictionary? parameters = null, - Dictionary? properties = null, CancellationToken cancellationToken = default) + /// The client credentials authentication request. + /// The client credentials authentication result. + public async ValueTask AuthenticateWithClientCredentialsAsync( + ClientCredentialsAuthenticationRequest request) { - if (registration is null) - { - throw new ArgumentNullException(nameof(registration)); - } - - if (scopes is not null && Array.Exists(scopes, string.IsNullOrEmpty)) - { - throw new ArgumentException(SR.GetResourceString(SR.ID0074), nameof(scopes)); - } - - var configuration = await registration.ConfigurationManager - .GetConfigurationAsync(cancellationToken) - .WaitAsync(cancellationToken) ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); - - if (configuration.TokenEndpoint is not { IsAbsoluteUri: true } uri || !uri.IsWellFormedOriginalString()) + if (request is null) { - throw new InvalidOperationException(SR.FormatID0301(Metadata.TokenEndpoint)); + throw new ArgumentNullException(nameof(request)); } - cancellationToken.ThrowIfCancellationRequested(); + request.CancellationToken.ThrowIfCancellationRequested(); // Note: this service is registered as a singleton service. As such, it cannot // directly depend on scoped services like the validation provider. To work around @@ -459,23 +558,23 @@ public sealed class OpenIddictClientService var context = new ProcessAuthenticationContext(transaction) { - CancellationToken = cancellationToken, - Configuration = configuration, + CancellationToken = request.CancellationToken, GrantType = GrantTypes.ClientCredentials, - Issuer = registration.Issuer, - Registration = registration, - TokenEndpoint = uri, - TokenRequest = parameters is not null ? new(parameters) : null, + Issuer = request.Issuer, + ProviderName = request.ProviderName, + RegistrationId = request.RegistrationId, + TokenRequest = request.AdditionalTokenRequestParameters + is Dictionary parameters ? new(parameters) : new(), }; - if (scopes is { Length: > 0 }) + if (request.Scopes is { Count: > 0 }) { - context.Scopes.UnionWith(scopes); + context.Scopes.UnionWith(request.Scopes); } - if (properties is { Count: > 0 }) + if (request.Properties is { Count: > 0 }) { - foreach (var property in properties) + foreach (var property in request.Properties) { context.Properties[property.Key] = property.Value; } @@ -490,13 +589,25 @@ public sealed class OpenIddictClientService context.Error, context.ErrorDescription, context.ErrorUri); } + Debug.Assert(context.Registration.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013)); Debug.Assert(context.TokenResponse is not null, SR.GetResourceString(SR.ID4007)); - // Create a composite principal containing claims resolved from the - // backchannel identity token and the userinfo token, if available. - return (context.TokenResponse, OpenIddictHelpers.CreateMergedPrincipal( - context.BackchannelIdentityTokenPrincipal, - context.UserinfoTokenPrincipal)); + return new() + { + AccessToken = context.BackchannelAccessToken!, + IdentityToken = context.BackchannelIdentityToken, + IdentityTokenPrincipal = context.BackchannelIdentityTokenPrincipal, + Principal = OpenIddictHelpers.CreateMergedPrincipal(context.BackchannelIdentityTokenPrincipal, + context.UserinfoTokenPrincipal) + .SetClaim(Claims.AuthorizationServer, context.Registration.Issuer.AbsoluteUri) + .SetClaim(Claims.Private.RegistrationId, context.Registration.RegistrationId) + .SetClaim(Claims.Private.ProviderName, context.Registration.ProviderName), + Properties = context.Properties, + RefreshToken = context.RefreshToken, + TokenResponse = context.TokenResponse, + UserinfoToken = context.UserinfoToken, + UserinfoTokenPrincipal = context.UserinfoTokenPrincipal + }; } finally @@ -514,7 +625,7 @@ public sealed class OpenIddictClientService } /// - /// Authenticates using the specified device authorization code and resolves the corresponding tokens. + /// Authenticates using the specified device authorization code. /// /// The issuer. /// The device authorization code returned by the server during the challenge process. @@ -523,7 +634,7 @@ public sealed class OpenIddictClientService /// The application-specific properties that will be added to the authentication context. /// The that can be used to abort the operation. /// The response and a merged principal containing the claims extracted from the tokens and userinfo response. - [RequiresPreviewFeatures] + [Obsolete("This method is obsolete and will be removed in a future version.")] public async ValueTask<(OpenIddictResponse TokenResponse, ClaimsPrincipal Principal)> AuthenticateWithDeviceAsync( Uri issuer, string code, TimeSpan? interval = null, Dictionary? parameters = null, @@ -534,39 +645,22 @@ public sealed class OpenIddictClientService throw new ArgumentNullException(nameof(issuer)); } - var registration = await GetClientRegistrationAsync(issuer, cancellationToken); - - while (true) + var result = await AuthenticateWithDeviceAsync(new() { - cancellationToken.ThrowIfCancellationRequested(); - - try - { - return await AuthenticateWithDeviceAsync(registration, code, parameters, properties, cancellationToken); - } - - catch (ProtocolException exception) when (exception.Error is Errors.AuthorizationPending) - { - // Default to a standard 5-second interval if no explicit value was configured. - // See https://www.rfc-editor.org/rfc/rfc8628#section-3.5 for more information. - await Task.Delay(interval ?? TimeSpan.FromSeconds(5), cancellationToken); - } - - catch (ProtocolException exception) when (exception.Error is Errors.SlowDown) - { - // When the error indicates that token requests are sent too frequently, - // slow down the token redeeming process by increasing the interval. - // - // See https://www.rfc-editor.org/rfc/rfc8628#section-3.5 for more information. - interval = (interval ?? TimeSpan.FromSeconds(5)) + TimeSpan.FromSeconds(5); + AdditionalTokenRequestParameters = parameters, + CancellationToken = cancellationToken, + DeviceCode = code, + Interval = interval ?? TimeSpan.FromSeconds(5), + Issuer = issuer, + Properties = properties!, + Timeout = TimeSpan.FromMinutes(5) + }); - await Task.Delay(interval.GetValueOrDefault(), cancellationToken); - } - } + return (result.TokenResponse, result.Principal); } /// - /// Authenticates using the specified device authorization code and resolves the corresponding tokens. + /// Authenticates using the specified device authorization code. /// /// The name of the provider (see ). /// The device authorization code returned by the server during the challenge process. @@ -575,7 +669,7 @@ public sealed class OpenIddictClientService /// The application-specific properties that will be added to the authentication context. /// The that can be used to abort the operation. /// The response and a merged principal containing the claims extracted from the tokens and userinfo response. - [RequiresPreviewFeatures] + [Obsolete("This method is obsolete and will be removed in a future version.")] public async ValueTask<(OpenIddictResponse TokenResponse, ClaimsPrincipal Principal)> AuthenticateWithDeviceAsync( string provider, string code, TimeSpan? interval = null, Dictionary? parameters = null, @@ -586,144 +680,137 @@ public sealed class OpenIddictClientService throw new ArgumentException(SR.FormatID0366(nameof(provider)), nameof(provider)); } - var registration = await GetClientRegistrationAsync(provider, cancellationToken); - - while (true) + var result = await AuthenticateWithDeviceAsync(new() { - cancellationToken.ThrowIfCancellationRequested(); - - try - { - return await AuthenticateWithDeviceAsync(registration, code, parameters, properties, cancellationToken); - } + AdditionalTokenRequestParameters = parameters, + CancellationToken = cancellationToken, + DeviceCode = code, + Interval = interval ?? TimeSpan.FromSeconds(5), + Properties = properties!, + ProviderName = provider, + Timeout = TimeSpan.FromMinutes(5) + }); - catch (ProtocolException exception) when (exception.Error is Errors.AuthorizationPending) - { - // Default to a standard 5-second interval if no explicit value was configured. - // See https://www.rfc-editor.org/rfc/rfc8628#section-3.5 for more information. - await Task.Delay(interval ?? TimeSpan.FromSeconds(5), cancellationToken); - } - - catch (ProtocolException exception) when (exception.Error is Errors.SlowDown) - { - // When the error indicates that token requests are sent too frequently, - // slow down the token redeeming process by increasing the interval. - // - // See https://www.rfc-editor.org/rfc/rfc8628#section-3.5 for more information. - interval = (interval ?? TimeSpan.FromSeconds(5)) + TimeSpan.FromSeconds(5); - - await Task.Delay(interval.GetValueOrDefault(), cancellationToken); - } - } + return (result.TokenResponse, result.Principal); } /// - /// Authenticates using the specified device authorization code and resolves the corresponding tokens. + /// Authenticates using the specified device authorization code. /// - /// The client registration. - /// The device code obtained after a challenge operation. - /// The additional parameters to send as part of the token request. - /// The application-specific properties that will be added to the authentication context. - /// The that can be used to abort the operation. - /// The response and a merged principal containing the claims extracted from the tokens and userinfo response. - [RequiresPreviewFeatures] - private async ValueTask<(OpenIddictResponse TokenResponse, ClaimsPrincipal Principal)> AuthenticateWithDeviceAsync( - OpenIddictClientRegistration registration, string code, - Dictionary? parameters = null, - Dictionary? properties = null, CancellationToken cancellationToken = default) + /// The device authentication request. + /// The device authentication result. + public async ValueTask AuthenticateWithDeviceAsync(DeviceAuthenticationRequest request) { - if (registration is null) - { - throw new ArgumentNullException(nameof(registration)); - } - - if (string.IsNullOrEmpty(code)) - { - throw new ArgumentException(SR.FormatID0366(nameof(code)), nameof(code)); - } - - var configuration = await registration.ConfigurationManager - .GetConfigurationAsync(cancellationToken) - .WaitAsync(cancellationToken) ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); - - if (configuration.TokenEndpoint is not { IsAbsoluteUri: true } uri || !uri.IsWellFormedOriginalString()) + if (request is null) { - throw new InvalidOperationException(SR.FormatID0301(Metadata.TokenEndpoint)); + throw new ArgumentNullException(nameof(request)); } - cancellationToken.ThrowIfCancellationRequested(); + using var source = CancellationTokenSource.CreateLinkedTokenSource(request.CancellationToken); + source.CancelAfter(request.Timeout); - // Note: this service is registered as a singleton service. As such, it cannot - // directly depend on scoped services like the validation provider. To work around - // this limitation, a scope is manually created for each method to this service. - var scope = _provider.CreateScope(); + var interval = request.Interval; - // Note: a try/finally block is deliberately used here to ensure the service scope - // can be disposed of asynchronously if it implements IAsyncDisposable. - try + while (true) { - var dispatcher = scope.ServiceProvider.GetRequiredService(); - var factory = scope.ServiceProvider.GetRequiredService(); - - var transaction = await factory.CreateTransactionAsync(); + source.Token.ThrowIfCancellationRequested(); - var context = new ProcessAuthenticationContext(transaction) + try { - CancellationToken = cancellationToken, - Configuration = configuration, - DeviceCode = code, - GrantType = GrantTypes.DeviceCode, - Issuer = registration.Issuer, - TokenEndpoint = uri, - TokenRequest = parameters is not null ? new(parameters) : null - }; + // Note: this service is registered as a singleton service. As such, it cannot + // directly depend on scoped services like the validation provider. To work around + // this limitation, a scope is manually created for each method to this service. + var scope = _provider.CreateScope(); - if (properties is { Count: > 0 }) - { - foreach (var property in properties) + // Note: a try/finally block is deliberately used here to ensure the service scope + // can be disposed of asynchronously if it implements IAsyncDisposable. + try { - context.Properties[property.Key] = property.Value; + var dispatcher = scope.ServiceProvider.GetRequiredService(); + var factory = scope.ServiceProvider.GetRequiredService(); + + var transaction = await factory.CreateTransactionAsync(); + + var context = new ProcessAuthenticationContext(transaction) + { + CancellationToken = source.Token, + DeviceCode = request.DeviceCode, + GrantType = GrantTypes.DeviceCode, + Issuer = request.Issuer, + ProviderName = request.ProviderName, + RegistrationId = request.RegistrationId, + Request = request.AdditionalTokenRequestParameters + is Dictionary parameters ? new(parameters) : new(), + }; + + if (request.Properties is { Count: > 0 }) + { + foreach (var property in request.Properties) + { + context.Properties[property.Key] = property.Value; + } + } + + await dispatcher.DispatchAsync(context); + + if (context.IsRejected) + { + throw new ProtocolException( + message: SR.GetResourceString(SR.ID0374), + context.Error, context.ErrorDescription, context.ErrorUri); + } + + else + { + Debug.Assert(context.Registration.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013)); + + return new() + { + AccessToken = context.BackchannelAccessToken!, + IdentityToken = context.BackchannelIdentityToken, + IdentityTokenPrincipal = context.BackchannelIdentityTokenPrincipal, + Principal = OpenIddictHelpers.CreateMergedPrincipal(context.BackchannelIdentityTokenPrincipal, + context.UserinfoTokenPrincipal) + .SetClaim(Claims.AuthorizationServer, context.Registration.Issuer.AbsoluteUri) + .SetClaim(Claims.Private.RegistrationId, context.Registration.RegistrationId) + .SetClaim(Claims.Private.ProviderName, context.Registration.ProviderName), + Properties = context.Properties, + RefreshToken = context.RefreshToken, + TokenResponse = context.TokenResponse ?? new(), + UserinfoToken = context.UserinfoToken, + UserinfoTokenPrincipal = context.UserinfoTokenPrincipal + }; + } } - } - await dispatcher.DispatchAsync(context); - - if (context.IsRejected) - { - throw new ProtocolException( - message: SR.GetResourceString(SR.ID0374), - context.Error, context.ErrorDescription, context.ErrorUri); - } - - else - { - Debug.Assert(context.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013)); - - var principal = OpenIddictHelpers.CreateMergedPrincipal( - context.FrontchannelIdentityTokenPrincipal, - context.BackchannelIdentityTokenPrincipal, - context.UserinfoTokenPrincipal) ?? new ClaimsPrincipal(new ClaimsIdentity()); - - // Attach the identity of the authorization server to the returned principal to allow resolving it even if no other - // claim was added to the principal (e.g when no id_token was returned and no userinfo endpoint is available). - principal.SetClaim(Claims.AuthorizationServer, context.Issuer.AbsoluteUri) - .SetClaim(Claims.Private.ProviderName, context.Registration.ProviderName); - - return (TokenResponse: context.TokenResponse ?? new(), Principal: principal); + finally + { + if (scope is IAsyncDisposable disposable) + { + await disposable.DisposeAsync(); + } + + else + { + scope.Dispose(); + } + } } - } - finally - { - if (scope is IAsyncDisposable disposable) + catch (ProtocolException exception) when (exception.Error is Errors.AuthorizationPending) { - await disposable.DisposeAsync(); + // Default to a standard 5-second interval if no explicit value was configured. + // See https://www.rfc-editor.org/rfc/rfc8628#section-3.5 for more information. + await Task.Delay(interval, source.Token); } - else + catch (ProtocolException exception) when (exception.Error is Errors.SlowDown) { - scope.Dispose(); + // When the error indicates that token requests are sent too frequently, + // slow down the token redeeming process by increasing the interval. + // + // See https://www.rfc-editor.org/rfc/rfc8628#section-3.5 for more information. + await Task.Delay(interval += TimeSpan.FromSeconds(5), source.Token); } } } @@ -737,7 +824,7 @@ public sealed class OpenIddictClientService /// The application-specific properties that will be added to the authentication context. /// The that can be used to abort the operation. /// The device authorization parameters. - [RequiresPreviewFeatures] + [Obsolete("This method is obsolete and will be removed in a future version.")] public async ValueTask<(string DeviceCode, string UserCode, Uri VerificationUri, Uri? VerificationUriComplete, TimeSpan ExpiresIn, TimeSpan Interval)> ChallengeUsingDeviceAsync( Uri issuer, string[]? scopes = null, Dictionary? parameters = null, @@ -748,8 +835,19 @@ public sealed class OpenIddictClientService throw new ArgumentNullException(nameof(issuer)); } - var registration = await GetClientRegistrationAsync(issuer, cancellationToken); - return await ChallengeUsingDeviceAsync(registration, scopes, parameters, properties, cancellationToken); + var result = await ChallengeUsingDeviceAsync(new() + { + AdditionalDeviceAuthorizationRequestParameters = parameters, + CancellationToken = cancellationToken, + Issuer = issuer, + Properties = properties!, + Scopes = scopes?.ToList() + }); + + return ( + result.DeviceCode, result.UserCode, + result.VerificationUri, result.VerificationUriComplete, + result.ExpiresIn, result.Interval); } /// @@ -761,7 +859,7 @@ public sealed class OpenIddictClientService /// The application-specific properties that will be added to the authentication context. /// The that can be used to abort the operation. /// The device authorization parameters. - [RequiresPreviewFeatures] + [Obsolete("This method is obsolete and will be removed in a future version.")] public async ValueTask<(string DeviceCode, string UserCode, Uri VerificationUri, Uri? VerificationUriComplete, TimeSpan ExpiresIn, TimeSpan Interval)> ChallengeUsingDeviceAsync( string provider, string[]? scopes = null, Dictionary? parameters = null, @@ -772,41 +870,34 @@ public sealed class OpenIddictClientService throw new ArgumentException(SR.FormatID0366(nameof(provider)), nameof(provider)); } - var registration = await GetClientRegistrationAsync(provider, cancellationToken); - return await ChallengeUsingDeviceAsync(registration, scopes, parameters, properties, cancellationToken); + var result = await ChallengeUsingDeviceAsync(new() + { + AdditionalDeviceAuthorizationRequestParameters = parameters, + CancellationToken = cancellationToken, + Properties = properties!, + ProviderName = provider, + Scopes = scopes?.ToList() + }); + + return ( + result.DeviceCode, result.UserCode, + result.VerificationUri, result.VerificationUriComplete, + result.ExpiresIn, result.Interval); } /// - /// Initiates a device authorization demand. + /// Initiates a device authorization process. /// - /// The client registration. - /// The scopes to request to the authorization server. - /// The additional parameters to send as part of the token request. - /// The application-specific properties that will be added to the authentication context. - /// The that can be used to abort the operation. - /// The device authorization parameters. - [RequiresPreviewFeatures] - private async ValueTask<(string DeviceCode, string UserCode, Uri VerificationUri, Uri? VerificationUriComplete, TimeSpan ExpiresIn, TimeSpan Interval)> ChallengeUsingDeviceAsync( - OpenIddictClientRegistration registration, string[]? scopes = null, - Dictionary? parameters = null, - Dictionary? properties = null, CancellationToken cancellationToken = default) + /// The device challenge request. + /// The device challenge result. + public async ValueTask ChallengeUsingDeviceAsync(DeviceChallengeRequest request) { - if (registration is null) - { - throw new ArgumentNullException(nameof(registration)); - } - - if (scopes is not null && Array.Exists(scopes, string.IsNullOrEmpty)) + if (request is null) { - throw new ArgumentException(SR.GetResourceString(SR.ID0074), nameof(scopes)); + throw new ArgumentNullException(nameof(request)); } - var configuration = await registration.ConfigurationManager - .GetConfigurationAsync(cancellationToken) - .WaitAsync(cancellationToken) ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); - - cancellationToken.ThrowIfCancellationRequested(); + request.CancellationToken.ThrowIfCancellationRequested(); // Note: this service is registered as a singleton service. As such, it cannot // directly depend on scoped services like the validation provider. To work around @@ -824,23 +915,24 @@ public sealed class OpenIddictClientService var context = new ProcessChallengeContext(transaction) { - CancellationToken = cancellationToken, - Configuration = configuration, + CancellationToken = request.CancellationToken, GrantType = GrantTypes.DeviceCode, - Issuer = registration.Issuer, + Issuer = request.Issuer, Principal = new ClaimsPrincipal(new ClaimsIdentity()), - Registration = registration, - Request = parameters is not null ? new(parameters) : new(), + ProviderName = request.ProviderName, + RegistrationId = request.RegistrationId, + Request = request.AdditionalDeviceAuthorizationRequestParameters + is Dictionary parameters ? new(parameters) : new(), }; - if (scopes is { Length: > 0 }) + if (request.Scopes is { Count: > 0 }) { - context.Scopes.UnionWith(scopes); + context.Scopes.UnionWith(request.Scopes); } - if (properties is { Count: > 0 }) + if (request.Properties is { Count: > 0 }) { - foreach (var property in properties) + foreach (var property in request.Properties) { context.Properties[property.Key] = property.Value; } @@ -855,13 +947,18 @@ public sealed class OpenIddictClientService context.Error, context.ErrorDescription, context.ErrorUri); } - return (DeviceCode: context.DeviceCode!, - UserCode: context.UserCode!, - VerificationUri: new Uri(context.DeviceAuthorizationResponse?.VerificationUri!, UriKind.Absolute), - VerificationUriComplete: context.DeviceAuthorizationResponse?.VerificationUriComplete - is string value ? new Uri(value, UriKind.Absolute) : null, - ExpiresIn: TimeSpan.FromSeconds((double) context.DeviceAuthorizationResponse?.ExpiresIn!), - Interval: TimeSpan.FromSeconds((long?) context.DeviceAuthorizationResponse[Parameters.Interval] ?? 5)); + return new() + { + DeviceAuthorizationResponse = context.DeviceAuthorizationResponse ?? new(), + DeviceCode = context.DeviceCode!, + ExpiresIn = TimeSpan.FromSeconds((double) context.DeviceAuthorizationResponse?.ExpiresIn!), + Interval = TimeSpan.FromSeconds((long?) context.DeviceAuthorizationResponse[Parameters.Interval] ?? 5), + Properties = context.Properties, + UserCode = context.UserCode!, + VerificationUri = new Uri(context.DeviceAuthorizationResponse?.VerificationUri!, UriKind.Absolute), + VerificationUriComplete = context.DeviceAuthorizationResponse?.VerificationUriComplete + is string value ? new Uri(value, UriKind.Absolute) : null + }; } finally @@ -879,7 +976,7 @@ public sealed class OpenIddictClientService } /// - /// Authenticates using the resource owner password credentials grant and resolves the corresponding tokens. + /// Authenticates using the resource owner password credentials grant. /// /// The issuer. /// The username to use. @@ -889,6 +986,7 @@ public sealed class OpenIddictClientService /// The application-specific properties that will be added to the authentication context. /// The that can be used to abort the operation. /// The response and a merged principal containing the claims extracted from the tokens and userinfo response. + [Obsolete("This method is obsolete and will be removed in a future version.")] public async ValueTask<(OpenIddictResponse Response, ClaimsPrincipal Principal)> AuthenticateWithPasswordAsync( Uri issuer, string username, string password, string[]? scopes = null, Dictionary? parameters = null, @@ -899,12 +997,22 @@ public sealed class OpenIddictClientService throw new ArgumentNullException(nameof(issuer)); } - var registration = await GetClientRegistrationAsync(issuer, cancellationToken); - return await AuthenticateWithPasswordAsync(registration, username, password, scopes, parameters, properties, cancellationToken); + var result = await AuthenticateWithPasswordAsync(new() + { + AdditionalTokenRequestParameters = parameters, + CancellationToken = cancellationToken, + Issuer = issuer, + Password = password, + Properties = properties!, + Scopes = scopes?.ToList(), + Username = username + }); + + return (result.TokenResponse, result.Principal); } /// - /// Authenticates using the resource owner password credentials grant and resolves the corresponding tokens. + /// Authenticates using the resource owner password credentials grant. /// /// The name of the provider (see ). /// The username to use. @@ -914,6 +1022,7 @@ public sealed class OpenIddictClientService /// The application-specific properties that will be added to the authentication context. /// The that can be used to abort the operation. /// The response and a merged principal containing the claims extracted from the tokens and userinfo response. + [Obsolete("This method is obsolete and will be removed in a future version.")] public async ValueTask<(OpenIddictResponse Response, ClaimsPrincipal Principal)> AuthenticateWithPasswordAsync( string provider, string username, string password, string[]? scopes = null, Dictionary? parameters = null, @@ -924,57 +1033,33 @@ public sealed class OpenIddictClientService throw new ArgumentException(SR.FormatID0366(nameof(provider)), nameof(provider)); } - var registration = await GetClientRegistrationAsync(provider, cancellationToken); - return await AuthenticateWithPasswordAsync(registration, username, password, scopes, parameters, properties, cancellationToken); + var result = await AuthenticateWithPasswordAsync(new() + { + AdditionalTokenRequestParameters = parameters, + CancellationToken = cancellationToken, + Password = password, + Properties = properties!, + ProviderName = provider, + Scopes = scopes?.ToList(), + Username = username + }); + + return (result.TokenResponse, result.Principal); } /// - /// Authenticates using the resource owner password credentials grant and resolves the corresponding tokens. + /// Authenticates using the resource owner password credentials grant. /// - /// The client registration. - /// The username to use. - /// The password to use. - /// The scopes to request to the authorization server. - /// The additional parameters to send as part of the token request. - /// The application-specific properties that will be added to the authentication context. - /// The that can be used to abort the operation. - /// The response and a merged principal containing the claims extracted from the tokens and userinfo response. - private async ValueTask<(OpenIddictResponse Response, ClaimsPrincipal Principal)> AuthenticateWithPasswordAsync( - OpenIddictClientRegistration registration, string username, string password, string[]? scopes = null, - Dictionary? parameters = null, - Dictionary? properties = null, CancellationToken cancellationToken = default) + /// The resource owner password credentials authentication request. + /// The resource owner password credentials authentication result. + public async ValueTask AuthenticateWithPasswordAsync(PasswordAuthenticationRequest request) { - if (registration is null) - { - throw new ArgumentNullException(nameof(registration)); - } - - if (string.IsNullOrEmpty(username)) - { - throw new ArgumentException(SR.FormatID0366(nameof(username)), nameof(username)); - } - - if (string.IsNullOrEmpty(password)) - { - throw new ArgumentException(SR.FormatID0366(nameof(password)), nameof(password)); - } - - if (scopes is not null && Array.Exists(scopes, string.IsNullOrEmpty)) - { - throw new ArgumentException(SR.GetResourceString(SR.ID0074), nameof(scopes)); - } - - var configuration = await registration.ConfigurationManager - .GetConfigurationAsync(cancellationToken) - .WaitAsync(cancellationToken) ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); - - if (configuration.TokenEndpoint is not { IsAbsoluteUri: true } uri || !uri.IsWellFormedOriginalString()) + if (request is null) { - throw new InvalidOperationException(SR.FormatID0301(Metadata.TokenEndpoint)); + throw new ArgumentNullException(nameof(request)); } - cancellationToken.ThrowIfCancellationRequested(); + request.CancellationToken.ThrowIfCancellationRequested(); // Note: this service is registered as a singleton service. As such, it cannot // directly depend on scoped services like the validation provider. To work around @@ -991,25 +1076,25 @@ public sealed class OpenIddictClientService var context = new ProcessAuthenticationContext(transaction) { - CancellationToken = cancellationToken, - Configuration = configuration, + CancellationToken = request.CancellationToken, GrantType = GrantTypes.Password, - Issuer = registration.Issuer, - Password = password, - Registration = registration, - TokenEndpoint = uri, - TokenRequest = parameters is not null ? new(parameters) : null, - Username = username + Issuer = request.Issuer, + Password = request.Password, + ProviderName = request.ProviderName, + RegistrationId = request.RegistrationId, + TokenRequest = request.AdditionalTokenRequestParameters + is Dictionary parameters ? new(parameters) : new(), + Username = request.Username }; - if (scopes is { Length: > 0 }) + if (request.Scopes is { Count: > 0 }) { - context.Scopes.UnionWith(scopes); + context.Scopes.UnionWith(request.Scopes); } - if (properties is { Count: > 0 }) + if (request.Properties is { Count: > 0 }) { - foreach (var property in properties) + foreach (var property in request.Properties) { context.Properties[property.Key] = property.Value; } @@ -1024,13 +1109,25 @@ public sealed class OpenIddictClientService context.Error, context.ErrorDescription, context.ErrorUri); } + Debug.Assert(context.Registration.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013)); Debug.Assert(context.TokenResponse is not null, SR.GetResourceString(SR.ID4007)); - // Create a composite principal containing claims resolved from the - // backchannel identity token and the userinfo token, if available. - return (context.TokenResponse, OpenIddictHelpers.CreateMergedPrincipal( - context.BackchannelIdentityTokenPrincipal, - context.UserinfoTokenPrincipal)); + return new() + { + AccessToken = context.BackchannelAccessToken!, + IdentityToken = context.BackchannelIdentityToken, + IdentityTokenPrincipal = context.BackchannelIdentityTokenPrincipal, + Principal = OpenIddictHelpers.CreateMergedPrincipal(context.BackchannelIdentityTokenPrincipal, + context.UserinfoTokenPrincipal) + .SetClaim(Claims.AuthorizationServer, context.Registration.Issuer.AbsoluteUri) + .SetClaim(Claims.Private.RegistrationId, context.Registration.RegistrationId) + .SetClaim(Claims.Private.ProviderName, context.Registration.ProviderName), + Properties = context.Properties, + RefreshToken = context.RefreshToken, + TokenResponse = context.TokenResponse, + UserinfoToken = context.UserinfoToken, + UserinfoTokenPrincipal = context.UserinfoTokenPrincipal + }; } finally @@ -1048,7 +1145,7 @@ public sealed class OpenIddictClientService } /// - /// Authenticates using the specified refresh token and resolves the corresponding tokens. + /// Authenticates using the specified refresh token. /// /// The issuer. /// The refresh token to use. @@ -1057,6 +1154,7 @@ public sealed class OpenIddictClientService /// The application-specific properties that will be added to the authentication context. /// The that can be used to abort the operation. /// The response and a merged principal containing the claims extracted from the tokens and userinfo response. + [Obsolete("This method is obsolete and will be removed in a future version.")] public async ValueTask<(OpenIddictResponse Response, ClaimsPrincipal Principal)> AuthenticateWithRefreshTokenAsync( Uri issuer, string token, string[]? scopes = null, Dictionary? parameters = null, @@ -1067,12 +1165,21 @@ public sealed class OpenIddictClientService throw new ArgumentNullException(nameof(issuer)); } - var registration = await GetClientRegistrationAsync(issuer, cancellationToken); - return await AuthenticateWithRefreshTokenAsync(registration, token, scopes, parameters, properties, cancellationToken); + var result = await AuthenticateWithRefreshTokenAsync(new() + { + AdditionalTokenRequestParameters = parameters, + CancellationToken = cancellationToken, + Issuer = issuer, + Properties = properties!, + RefreshToken = token, + Scopes = scopes?.ToList() + }); + + return (result.TokenResponse, result.Principal); } /// - /// Authenticates using the specified refresh token and resolves the corresponding tokens. + /// Authenticates using the specified refresh token. /// /// The name of the provider (see ). /// The refresh token to use. @@ -1081,6 +1188,7 @@ public sealed class OpenIddictClientService /// The application-specific properties that will be added to the authentication context. /// The that can be used to abort the operation. /// The response and a merged principal containing the claims extracted from the tokens and userinfo response. + [Obsolete("This method is obsolete and will be removed in a future version.")] public async ValueTask<(OpenIddictResponse Response, ClaimsPrincipal Principal)> AuthenticateWithRefreshTokenAsync( string provider, string token, string[]? scopes = null, Dictionary? parameters = null, @@ -1091,51 +1199,33 @@ public sealed class OpenIddictClientService throw new ArgumentException(SR.FormatID0366(nameof(provider)), nameof(provider)); } - var registration = await GetClientRegistrationAsync(provider, cancellationToken); - return await AuthenticateWithRefreshTokenAsync(registration, token, scopes, parameters, properties, cancellationToken); + var result = await AuthenticateWithRefreshTokenAsync(new() + { + AdditionalTokenRequestParameters = parameters, + CancellationToken = cancellationToken, + Properties = properties!, + ProviderName = provider, + RefreshToken = token, + Scopes = scopes?.ToList() + }); + + return (result.TokenResponse, result.Principal); } /// - /// Authenticates using the specified refresh token and resolves the corresponding tokens. + /// Authenticates using the specified refresh token. /// - /// The client registration. - /// The refresh token to use. - /// The scopes to request to the authorization server. - /// The additional parameters to send as part of the token request. - /// The application-specific properties that will be added to the authentication context. - /// The that can be used to abort the operation. - /// The response and a merged principal containing the claims extracted from the tokens and userinfo response. - private async ValueTask<(OpenIddictResponse Response, ClaimsPrincipal Principal)> AuthenticateWithRefreshTokenAsync( - OpenIddictClientRegistration registration, string token, string[]? scopes = null, - Dictionary? parameters = null, - Dictionary? properties = null, CancellationToken cancellationToken = default) + /// The refresh token authentication request. + /// The refresh token authentication result. + public async ValueTask AuthenticateWithRefreshTokenAsync( + RefreshTokenAuthenticationRequest request) { - if (registration is null) - { - throw new ArgumentNullException(nameof(registration)); - } - - if (string.IsNullOrEmpty(token)) - { - throw new ArgumentException(SR.FormatID0366(nameof(token)), nameof(token)); - } - - if (scopes is not null && Array.Exists(scopes, string.IsNullOrEmpty)) - { - throw new ArgumentException(SR.GetResourceString(SR.ID0074), nameof(scopes)); - } - - var configuration = await registration.ConfigurationManager - .GetConfigurationAsync(cancellationToken) - .WaitAsync(cancellationToken) ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); - - if (configuration.TokenEndpoint is not { IsAbsoluteUri: true } uri || !uri.IsWellFormedOriginalString()) + if (request is null) { - throw new InvalidOperationException(SR.FormatID0301(Metadata.TokenEndpoint)); + throw new ArgumentNullException(nameof(request)); } - cancellationToken.ThrowIfCancellationRequested(); + request.CancellationToken.ThrowIfCancellationRequested(); // Note: this service is registered as a singleton service. As such, it cannot // directly depend on scoped services like the validation provider. To work around @@ -1152,24 +1242,24 @@ public sealed class OpenIddictClientService var context = new ProcessAuthenticationContext(transaction) { - CancellationToken = cancellationToken, - Configuration = configuration, + CancellationToken = request.CancellationToken, GrantType = GrantTypes.RefreshToken, - Issuer = registration.Issuer, - RefreshToken = token, - Registration = registration, - TokenEndpoint = uri, - TokenRequest = parameters is not null ? new(parameters) : null, + Issuer = request.Issuer, + ProviderName = request.ProviderName, + RefreshToken = request.RefreshToken, + RegistrationId = request.RegistrationId, + TokenRequest = request.AdditionalTokenRequestParameters + is Dictionary parameters ? new(parameters) : new(), }; - if (scopes is { Length: > 0 }) + if (request.Scopes is { Count: > 0 }) { - context.Scopes.UnionWith(scopes); + context.Scopes.UnionWith(request.Scopes); } - if (properties is { Count: > 0 }) + if (request.Properties is { Count: > 0 }) { - foreach (var property in properties) + foreach (var property in request.Properties) { context.Properties[property.Key] = property.Value; } @@ -1184,13 +1274,26 @@ public sealed class OpenIddictClientService context.Error, context.ErrorDescription, context.ErrorUri); } + Debug.Assert(context.Registration.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013)); Debug.Assert(context.TokenResponse is not null, SR.GetResourceString(SR.ID4007)); - // Create a composite principal containing claims resolved from the - // backchannel identity token and the userinfo token, if available. - return (context.TokenResponse, OpenIddictHelpers.CreateMergedPrincipal( - context.BackchannelIdentityTokenPrincipal, - context.UserinfoTokenPrincipal)); + return new() + { + AccessToken = context.BackchannelAccessToken!, + IdentityToken = context.BackchannelIdentityToken, + IdentityTokenPrincipal = context.BackchannelIdentityTokenPrincipal, + Principal = OpenIddictHelpers.CreateMergedPrincipal( + context.BackchannelIdentityTokenPrincipal, + context.UserinfoTokenPrincipal) + .SetClaim(Claims.AuthorizationServer, context.Registration.Issuer.AbsoluteUri) + .SetClaim(Claims.Private.RegistrationId, context.Registration.RegistrationId) + .SetClaim(Claims.Private.ProviderName, context.Registration.ProviderName), + Properties = context.Properties, + RefreshToken = context.RefreshToken, + TokenResponse = context.TokenResponse, + UserinfoToken = context.UserinfoToken, + UserinfoTokenPrincipal = context.UserinfoTokenPrincipal + }; } finally @@ -1528,19 +1631,25 @@ public sealed class OpenIddictClientService /// Sends the device authorization request and retrieves the corresponding response. /// /// The client registration. + /// The server configuration. /// The device authorization request. /// The uri of the remote device authorization endpoint. /// The that can be used to abort the operation. /// The token response. internal async ValueTask SendDeviceAuthorizationRequestAsync( - OpenIddictClientRegistration registration, OpenIddictRequest request, - Uri? uri = null, CancellationToken cancellationToken = default) + OpenIddictClientRegistration registration, OpenIddictConfiguration configuration, + OpenIddictRequest request, Uri? uri = null, CancellationToken cancellationToken = default) { if (registration is null) { throw new ArgumentNullException(nameof(registration)); } + if (configuration is null) + { + throw new ArgumentNullException(nameof(configuration)); + } + if (request is null) { throw new ArgumentNullException(nameof(request)); @@ -1556,11 +1665,6 @@ public sealed class OpenIddictClientService throw new ArgumentException(SR.GetResourceString(SR.ID0144), nameof(uri)); } - var configuration = await registration.ConfigurationManager - .GetConfigurationAsync(cancellationToken) - .WaitAsync(cancellationToken) ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); - cancellationToken.ThrowIfCancellationRequested(); // Note: this service is registered as a singleton service. As such, it cannot @@ -1701,19 +1805,25 @@ public sealed class OpenIddictClientService /// Sends the token request and retrieves the corresponding response. /// /// The client registration. + /// The server configuration. /// The token request. /// The uri of the remote token endpoint. /// The that can be used to abort the operation. /// The token response. internal async ValueTask SendTokenRequestAsync( - OpenIddictClientRegistration registration, OpenIddictRequest request, - Uri? uri = null, CancellationToken cancellationToken = default) + OpenIddictClientRegistration registration, OpenIddictConfiguration configuration, + OpenIddictRequest request, Uri? uri = null, CancellationToken cancellationToken = default) { if (registration is null) { throw new ArgumentNullException(nameof(registration)); } + if (configuration is null) + { + throw new ArgumentNullException(nameof(configuration)); + } + if (request is null) { throw new ArgumentNullException(nameof(request)); @@ -1729,11 +1839,6 @@ public sealed class OpenIddictClientService throw new ArgumentException(SR.GetResourceString(SR.ID0144), nameof(uri)); } - var configuration = await registration.ConfigurationManager - .GetConfigurationAsync(cancellationToken) - .WaitAsync(cancellationToken) ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); - cancellationToken.ThrowIfCancellationRequested(); // Note: this service is registered as a singleton service. As such, it cannot @@ -1874,18 +1979,25 @@ public sealed class OpenIddictClientService /// Sends the userinfo request and retrieves the corresponding response. /// /// The client registration. + /// The server configuration. /// The userinfo request. /// The uri of the remote userinfo endpoint. /// The that can be used to abort the operation. /// The response and the principal extracted from the userinfo response or the userinfo token. internal async ValueTask<(OpenIddictResponse Response, (ClaimsPrincipal? Principal, string? Token))> SendUserinfoRequestAsync( - OpenIddictClientRegistration registration, OpenIddictRequest request, Uri uri, CancellationToken cancellationToken = default) + OpenIddictClientRegistration registration, OpenIddictConfiguration configuration, + OpenIddictRequest request, Uri uri, CancellationToken cancellationToken = default) { if (registration is null) { throw new ArgumentNullException(nameof(registration)); } + if (configuration is null) + { + throw new ArgumentNullException(nameof(configuration)); + } + if (uri is null) { throw new ArgumentNullException(nameof(uri)); @@ -1896,11 +2008,6 @@ public sealed class OpenIddictClientService throw new ArgumentException(SR.GetResourceString(SR.ID0144), nameof(uri)); } - var configuration = await registration.ConfigurationManager - .GetConfigurationAsync(cancellationToken) - .WaitAsync(cancellationToken) ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); - cancellationToken.ThrowIfCancellationRequested(); // Note: this service is registered as a singleton service. As such, it cannot diff --git a/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs b/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs index 4252f309..fa28dac7 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs @@ -44,10 +44,17 @@ public sealed class OpenIddictValidationConfiguration : IPostConfigureOptions