From 2a7becd6d358aeb37b964e991f4a925500be1aed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Thu, 29 Sep 2022 19:59:51 +0200 Subject: [PATCH] Update the web integration to use native options and revamp how user agents are generated --- ...OpenIddictClientWebIntegrationGenerator.cs | 639 +++++++++++------- .../Startup.cs | 31 +- .../Startup.cs | 13 +- .../Startup.cs | 41 +- .../Startup.cs | 14 +- .../OpenIddictClientAspNetCoreBuilder.cs | 16 +- .../OpenIddictClientOwinBuilder.cs | 16 +- .../OpenIddictClientSystemNetHttpBuilder.cs | 34 +- ...nIddictClientSystemNetHttpConfiguration.cs | 13 +- .../OpenIddictClientSystemNetHttpHandlers.cs | 17 +- .../OpenIddictClientSystemNetHttpOptions.cs | 9 +- ...OpenIddictClientWebIntegrationConstants.cs | 2 +- ...nIddictClientWebIntegrationEnvironments.cs | 15 - ...tClientWebIntegrationHandlers.Discovery.cs | 4 +- ...ctClientWebIntegrationHandlers.Exchange.cs | 21 - ...ClientWebIntegrationHandlers.Protection.cs | 2 +- ...ctClientWebIntegrationHandlers.Userinfo.cs | 11 +- .../OpenIddictClientWebIntegrationHandlers.cs | 59 +- .../OpenIddictClientWebIntegrationHelpers.cs | 31 +- .../OpenIddictClientWebIntegrationOptions.cs | 7 +- .../OpenIddictClientWebIntegrationProvider.cs | 46 -- ...penIddictClientWebIntegrationProviders.xml | 12 +- .../OpenIddictClientWebIntegrationSettings.cs | 43 -- .../OpenIddictClientBuilder.cs | 16 +- ...penIddictValidationSystemNetHttpBuilder.cs | 22 +- ...ictValidationSystemNetHttpConfiguration.cs | 5 +- ...enIddictValidationSystemNetHttpHandlers.cs | 17 +- ...penIddictValidationSystemNetHttpOptions.cs | 7 + 28 files changed, 592 insertions(+), 571 deletions(-) delete mode 100644 src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationEnvironments.cs delete mode 100644 src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Exchange.cs delete mode 100644 src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProvider.cs delete mode 100644 src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationSettings.cs diff --git a/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs b/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs index 60bc76e5..83e4ac5b 100644 --- a/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs +++ b/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Globalization; +using System.Text; using System.Xml.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; @@ -34,24 +35,22 @@ namespace OpenIddict.Client.WebIntegration.Generators "OpenIddictClientWebIntegrationConstants.generated.cs", SourceText.From(GenerateConstants(document), Encoding.UTF8)); - context.AddSource( - "OpenIddictClientWebIntegrationEnvironments.generated.cs", - SourceText.From(GenerateEnvironments(document), Encoding.UTF8)); - context.AddSource( "OpenIddictClientWebIntegrationHelpers.generated.cs", SourceText.From(GenerateHelpers(document), Encoding.UTF8)); context.AddSource( - "OpenIddictClientWebIntegrationSettings.generated.cs", - SourceText.From(GenerateSettings(document), Encoding.UTF8)); + "OpenIddictClientWebIntegrationOptions.generated.cs", + SourceText.From(GenerateOptions(document), Encoding.UTF8)); static string GenerateBuilderMethods(XDocument document) { var template = Template.Parse(@"#nullable enable +using System.ComponentModel; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; using OpenIddict.Client; using OpenIddict.Client.WebIntegration; using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants; @@ -62,27 +61,162 @@ public partial class OpenIddictClientWebIntegrationBuilder { {{~ for provider in providers ~}} /// - /// Enables {{ provider.name }} integration using the specified settings. + /// Enables {{ provider.name }} integration using the specified options. + {{~ if provider.documentation ~}} + /// For more information, visit the official website. /// + {{~ end ~}} + /// This extension can be safely called multiple times. + /// The . + public OpenIddictClientWebIntegrationBuilder.{{ provider.name }} Use{{ provider.name }}() + { + // Note: TryAddEnumerable() is used here to ensure the initializers are registered only once. + Services.TryAddEnumerable(new[] + { + ServiceDescriptor.Singleton< + IConfigureOptions, OpenIddictClientWebIntegrationConfiguration.{{ provider.name }}>(), + ServiceDescriptor.Singleton< + IPostConfigureOptions, OpenIddictClientWebIntegrationConfiguration.{{ provider.name }}>() + }); + + return new OpenIddictClientWebIntegrationBuilder.{{ provider.name }}(Services); + } + + /// + /// Enables {{ provider.name }} integration using the specified options. {{~ if provider.documentation ~}} - /// - /// For more information about {{ provider.name }} integration, visit the official website. - /// + /// For more information, visit the official website. + /// {{~ end ~}} - /// The provider settings. + /// This extension can be safely called multiple times. + /// The delegate used to configure the OpenIddict/{{ provider.name }} options. /// The . - public OpenIddictClientWebIntegrationBuilder Add{{ provider.name }}(OpenIddictClientWebIntegrationSettings.{{ provider.name }} settings) + public OpenIddictClientWebIntegrationBuilder Use{{ provider.name }}(Action configuration) { - if (settings is null) + if (configuration is null) { - throw new ArgumentNullException(nameof(settings)); + throw new ArgumentNullException(nameof(configuration)); } - // Note: TryAddEnumerable() is used here to ensure the initializer is registered only once. - Services.TryAddEnumerable(ServiceDescriptor.Singleton< - IConfigureOptions, OpenIddictClientWebIntegrationConfiguration.{{ provider.name }}>()); + configuration(Use{{ provider.name }}()); - return Configure(options => options.Providers.Add(new OpenIddictClientWebIntegrationProvider(Providers.{{ provider.name }}, settings))); + return this; + } + {{~ end ~}} + + {{~ for provider in providers ~}} + /// + /// Exposes the necessary methods required to configure the {{ provider.name }} integration. + /// + public class {{ provider.name }} + { + /// + /// Initializes a new instance of . + /// + /// The services collection. + public {{ provider.name }}(IServiceCollection services) + => Services = services ?? throw new ArgumentNullException(nameof(services)); + + /// + /// Gets the services collection. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public IServiceCollection Services { get; } + + /// + /// Amends the default OpenIddict client {{ provider.name }} configuration. + /// + /// The delegate used to configure the OpenIddict options. + /// This extension can be safely called multiple times. + /// The . + public {{ provider.name }} Configure(Action configuration) + { + if (configuration is null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + Services.Configure(configuration); + + return this; + } + + /// + /// Sets the client identifier. + /// + /// The client identifier. + /// The . + public {{ provider.name }} SetClientId(string identifier) + => Configure(options => options.ClientId = identifier); + + /// + /// Sets the client secret, if applicable. + /// + /// The client secret. + /// The . + public {{ provider.name }} SetClientSecret(string secret) + => Configure(options => options.ClientSecret = secret); + + /// + /// Sets the redirection URI, if applicable. + /// + /// The redirection URI. + /// The . + public {{ provider.name }} SetRedirectUri(Uri? address) + => Configure(options => options.RedirectUri = address); + + /// + /// Sets the redirection URI, if applicable. + /// + /// The redirection URI. + /// The . + public {{ provider.name }} SetRedirectUri(string? address) + => SetRedirectUri(!string.IsNullOrEmpty(address) ? new Uri(address, UriKind.RelativeOrAbsolute) : null); + + /// + /// Adds one or more scopes to the list of requested scopes, if applicable. + /// + /// The scopes. + /// The . + public {{ provider.name }} AddScopes(params string[] scopes) + => Configure(options => options.Scopes.UnionWith(scopes)); + + {{~ for environment in provider.environments ~}} + /// + /// Configures the provider to use the ""{{ environment.name }}"" environment. + /// + /// The . + public {{ provider.name }} Use{{ environment.name }}Environment() + => Configure(options => options.Environment = OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.{{ environment.name }}); + {{~ end ~}} + + {{~ for setting in provider.settings ~}} + {{~ if setting.description ~}} + /// + /// Configures {{ setting.description }}. + /// + {{~ end ~}} + {{~ if setting.collection ~}} + public {{ provider.name }} Add{{ setting.name }}(params {{ setting.clr_type }}[] values) + => Configure(options => options.{{ setting.name }}.UnionWith(values)); + {{~ else ~}} + public {{ provider.name }} Set{{ setting.name }}({{ setting.clr_type }}? value) + => Configure(options => options.{{ setting.name }} = value); + {{~ end ~}} + + {{~ end ~}} + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object? obj) => base.Equals(obj); + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override int GetHashCode() => base.GetHashCode(); + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override string? ToString() => base.ToString(); } {{~ end ~}} } @@ -93,7 +227,39 @@ public partial class OpenIddictClientWebIntegrationBuilder .Select(provider => new { Name = (string) provider.Attribute("Name"), - Documentation = (string?) provider.Attribute("Documentation") + Documentation = (string?) provider.Attribute("Documentation"), + + Environments = provider.Elements("Environment").Select(environment => new + { + Name = (string?) environment.Attribute("Name") ?? "Production" + }) + .ToList(), + + Settings = provider.Elements("Setting").Select(setting => new + { + Name = (string) setting.Attribute("Name"), + Collection = (bool?) setting.Attribute("Collection") ?? false, + Description = (string) setting.Attribute("Description") is string description ? + char.ToLower(description[0], CultureInfo.GetCultureInfo("en-US")) + description.Substring(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", + + "String" => "string", + "StringHashSet" => "HashSet", + + string value => value + } + }) + .ToList() }) .ToList() }); @@ -107,6 +273,18 @@ namespace OpenIddict.Client.WebIntegration; public static partial class OpenIddictClientWebIntegrationConstants { + {{~ for provider in providers ~}} + public static class {{ provider.name }} + { + public static class Environments + { + {{~ for environment in provider.environments ~}} + public const string {{ environment.name }} = ""{{ environment.name }}""; + {{~ end ~}} + } + } + {{~ end ~}} + public static class Providers { {{~ for provider in providers ~}} @@ -114,35 +292,6 @@ public static partial class OpenIddictClientWebIntegrationConstants {{~ end ~}} } } -"); - return template.Render(new - { - Providers = document.Root.Elements("Provider") - .Select(provider => new { Name = (string) provider.Attribute("Name") }) - .ToList() - }); - } - - static string GenerateEnvironments(XDocument document) - { - var template = Template.Parse(@"#nullable enable - -namespace OpenIddict.Client.WebIntegration; - -public partial class OpenIddictClientWebIntegrationEnvironments -{ - {{~ for provider in providers ~}} - /// - /// Exposes the environments supported by the {{ provider.name }} provider. - /// - public enum {{ provider.name }} - { - {{~ for environment in provider.environments ~}} - {{ environment.name }}, - {{~ end ~}} - } - {{~ end ~}} -} "); return template.Render(new { @@ -155,7 +304,7 @@ public partial class OpenIddictClientWebIntegrationEnvironments { Name = (string?) environment.Attribute("Name") ?? "Production" }) - .ToList() + .ToList(), }) .ToList() }); @@ -165,6 +314,7 @@ public partial class OpenIddictClientWebIntegrationEnvironments { var template = Template.Parse(@"#nullable enable +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using OpenIddict.Client; @@ -181,226 +331,224 @@ public partial class OpenIddictClientWebIntegrationConfiguration /// /// Contains the methods required to register the {{ provider.name }} integration in the OpenIddict client options. /// - public class {{ provider.name }} : IConfigureOptions + public class {{ provider.name }} : IConfigureOptions, + IPostConfigureOptions { - private readonly IOptions _options; + private readonly IServiceProvider _provider; /// /// Creates a new instance of the class. /// - /// The OpenIddict client web integration options. - /// is null. - public {{ provider.name }}(IOptions options) - => _options = options ?? throw new ArgumentNullException(nameof(options)); + /// The service provider. + /// is null. + public {{ provider.name }}(IServiceProvider provider) + => _provider = provider ?? throw new ArgumentNullException(nameof(provider)); /// - /// Ensures the {{ provider.name }} configuration is in a consistent and valid state - /// and registers the {{ provider.name }} integration in the OpenIddict client options. + /// Ensures the {{ provider.name }} configuration is in a consistent and valid state. /// + /// The name of the options instance to configure, if applicable. /// The options instance to initialize. - public void Configure(OpenIddictClientOptions options) + public void PostConfigure(string name, OpenIddictClientWebIntegrationOptions.{{ provider.name }} options) { - foreach (var provider in _options.Value.Providers) + if (string.IsNullOrEmpty(options.ClientId)) { - if (provider.Name is not Providers.{{ provider.name }}) - { - continue; - } - - if (provider.Settings is not OpenIddictClientWebIntegrationSettings.{{ provider.name }} settings) - { - throw new InvalidOperationException(SR.FormatID0331(Providers.{{ provider.name }})); - } - - if (string.IsNullOrEmpty(settings.ClientId)) - { - throw new InvalidOperationException(SR.FormatID0332(nameof(settings.ClientId), Providers.{{ provider.name }})); - } + throw new InvalidOperationException(SR.FormatID0332(nameof(options.ClientId), Providers.{{ provider.name }})); + } - if (settings.RedirectUri is null) - { - throw new InvalidOperationException(SR.FormatID0332(nameof(settings.RedirectUri), Providers.{{ provider.name }})); - } + if (options.RedirectUri is null) + { + throw new InvalidOperationException(SR.FormatID0332(nameof(options.RedirectUri), Providers.{{ provider.name }})); + } - {{~ for setting in provider.settings ~}} - {{~ if setting.required ~}} - {{~ if setting.type == 'String' ~}} - if (string.IsNullOrEmpty(settings.{{ setting.name }})) - {{~ else ~}} - if (settings.{{ setting.name }} is null) - {{~ end ~}} - { - throw new InvalidOperationException(SR.FormatID0332(nameof(settings.{{ setting.name }}), Providers.{{ provider.name }})); - } - {{~ end ~}} - {{~ end ~}} + {{~ for setting in provider.settings ~}} + {{~ if setting.required ~}} + {{~ if setting.type == 'String' ~}} + if (string.IsNullOrEmpty(options.{{ setting.name }})) + {{~ else ~}} + if (options.{{ setting.name }} is null) + {{~ end ~}} + { + throw new InvalidOperationException(SR.FormatID0332(nameof(options.{{ setting.name }}), Providers.{{ provider.name }})); + } + {{~ end ~}} + {{~ end ~}} - {{~ for environment in provider.environments ~}} - if (settings.Environment is OpenIddictClientWebIntegrationEnvironments.{{ provider.name }}.{{ environment.name }}) + {{~ for environment in provider.environments ~}} + if (options.Environment is OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.{{ environment.name }}) + { + if (options.Scopes.Count is 0) { - if (settings.Scopes.Count is 0) - { - {{~ for scope in environment.scopes ~}} - {{~ if scope.default && !scope.required ~}} - settings.Scopes.Add(""{{ scope.name }}""); - {{~ end ~}} - {{~ end ~}} - } - {{~ for scope in environment.scopes ~}} - {{~ if scope.required ~}} - settings.Scopes.Add(""{{ scope.name }}""); + {{~ if scope.default && !scope.required ~}} + options.Scopes.Add(""{{ scope.name }}""); {{~ end ~}} {{~ end ~}} } - {{~ end ~}} - {{~ for setting in provider.settings ~}} - {{~ if setting.default_value && setting.type == 'String' ~}} - if (string.IsNullOrEmpty(settings.{{ setting.name }})) - { - settings.{{ setting.name }} = ""{{ setting.default_value }}""; - } + {{~ for scope in environment.scopes ~}} + {{~ if scope.required ~}} + options.Scopes.Add(""{{ scope.name }}""); {{~ end ~}} {{~ end ~}} + } + {{~ end ~}} - {{~ for setting in provider.settings ~}} - {{~ if setting.collection ~}} - if (settings.{{ setting.name }}.Count is 0) - { - {{~ for item in setting.collection_items ~}} - {{~ if item.default && !item.required ~}} - settings.{{ setting.name }}.Add(""{{ item.value }}""); - {{~ end ~}} - {{~ end ~}} - } - {{~ end ~}} + {{~ for setting in provider.settings ~}} + {{~ if setting.default_value && setting.type == 'String' ~}} + if (string.IsNullOrEmpty(options.{{ setting.name }})) + { + options.{{ setting.name }} = ""{{ setting.default_value }}""; + } + {{~ end ~}} + {{~ end ~}} + {{~ for setting in provider.settings ~}} + {{~ if setting.collection ~}} + if (options.{{ setting.name }}.Count is 0) + { {{~ for item in setting.collection_items ~}} - {{~ if item.required ~}} - settings.{{ setting.name }}.Add(""{{ item.value }}""); - {{~ end ~}} + {{~ if item.default && !item.required ~}} + options.{{ setting.name }}.Add(""{{ item.value }}""); {{~ end ~}} {{~ end ~}} + } + {{~ end ~}} + + {{~ for item in setting.collection_items ~}} + {{~ if item.required ~}} + options.{{ setting.name }}.Add(""{{ item.value }}""); + {{~ end ~}} + {{~ end ~}} + {{~ end ~}} + } - var formatter = Smart.CreateDefaultSmartFormat(new SmartSettings + /// + /// Registers the {{ provider.name }} integration in the OpenIddict client options. + /// + /// The options instance to initialize. + public void Configure(OpenIddictClientOptions options) + { + var formatter = Smart.CreateDefaultSmartFormat(new SmartSettings + { + CaseSensitivity = CaseSensitivityType.CaseInsensitive + }); + + // Resolve the provider options from the service provider and create a registration based on the specified settings. + var settings = _provider.GetRequiredService>().CurrentValue; + + var registration = new OpenIddictClientRegistration + { + Issuer = settings.Environment switch { - CaseSensitivity = CaseSensitivityType.CaseInsensitive - }); + {{~ for environment in provider.environments ~}} + OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.{{ environment.name }} + => new Uri(formatter.Format(""{{ environment.issuer }}"", options), UriKind.Absolute), + {{~ end ~}} + + _ => throw new InvalidOperationException(SR.FormatID0194(nameof(settings.Environment))) + }, - var registration = new OpenIddictClientRegistration + ClientId = settings.ClientId, + ClientSecret = settings.ClientSecret, + RedirectUri = settings.RedirectUri, + + Configuration = settings.Environment switch { - Issuer = settings.Environment switch + {{~ for environment in provider.environments ~}} + {{~ if environment.configuration ~}} + OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.{{ environment.name }} => new OpenIddictConfiguration { - {{~ for environment in provider.environments ~}} - OpenIddictClientWebIntegrationEnvironments.{{ provider.name }}.{{ environment.name }} - => new Uri(formatter.Format(""{{ environment.issuer }}"", settings), UriKind.Absolute), + {{~ if environment.configuration.authorization_endpoint ~}} + AuthorizationEndpoint = new Uri(formatter.Format(""{{ environment.configuration.authorization_endpoint }}"", options), UriKind.Absolute), {{~ end ~}} - _ => throw new InvalidOperationException(SR.FormatID0194(nameof(settings.Environment))) - }, + {{~ if environment.configuration.token_endpoint ~}} + TokenEndpoint = new Uri(formatter.Format(""{{ environment.configuration.token_endpoint }}"", options), UriKind.Absolute), + {{~ end ~}} - ClientId = settings.ClientId, - ClientSecret = settings.ClientSecret, - RedirectUri = settings.RedirectUri, + {{~ if environment.configuration.userinfo_endpoint ~}} + UserinfoEndpoint = new Uri(formatter.Format(""{{ environment.configuration.userinfo_endpoint }}"", options), UriKind.Absolute), + {{~ end ~}} - Configuration = settings.Environment switch - { - {{~ for environment in provider.environments ~}} - {{~ if environment.configuration ~}} - OpenIddictClientWebIntegrationEnvironments.{{ provider.name }}.{{ environment.name }} => new OpenIddictConfiguration + CodeChallengeMethodsSupported = { - {{~ if environment.configuration.authorization_endpoint ~}} - AuthorizationEndpoint = new Uri(formatter.Format(""{{ environment.configuration.authorization_endpoint }}"", settings), UriKind.Absolute), + {{~ for method in environment.configuration.code_challenge_methods_supported ~}} + ""{{ method }}"", {{~ end ~}} + }, - {{~ if environment.configuration.token_endpoint ~}} - TokenEndpoint = new Uri(formatter.Format(""{{ environment.configuration.token_endpoint }}"", settings), UriKind.Absolute), + GrantTypesSupported = + { + {{~ for type in environment.configuration.grant_types_supported ~}} + ""{{ type }}"", {{~ end ~}} + }, - {{~ if environment.configuration.userinfo_endpoint ~}} - UserinfoEndpoint = new Uri(formatter.Format(""{{ environment.configuration.userinfo_endpoint }}"", settings), UriKind.Absolute), + ResponseModesSupported = + { + {{~ for mode in environment.configuration.response_modes_supported ~}} + ""{{ mode }}"", {{~ 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 ~}} - }, + ResponseTypesSupported = + { + {{~ for type in environment.configuration.response_types_supported ~}} + ""{{ type }}"", + {{~ 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 ~}} - OpenIddictClientWebIntegrationEnvironments.{{ provider.name }}.{{ environment.name }} => null, - {{~ end ~}} - {{~ end ~}} - _ => throw new InvalidOperationException(SR.FormatID0194(nameof(settings.Environment))) + TokenEndpointAuthMethodsSupported = + { + {{~ for method in environment.configuration.token_endpoint_auth_methods_supported ~}} + ""{{ method }}"", + {{~ end ~}} + } }, + {{~ else ~}} + OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.{{ environment.name }} => null, + {{~ end ~}} + {{~ end ~}} - EncryptionCredentials = - { - {{~ for setting in provider.settings ~}} - {{~ if setting.type == 'EncryptionKey' ~}} - new EncryptingCredentials(settings.{{ setting.name }}, ""{{ setting.encryption_algorithm }}"", SecurityAlgorithms.Aes256CbcHmacSha512), - {{~ end ~}} - {{~ end ~}} - }, + _ => throw new InvalidOperationException(SR.FormatID0194(nameof(settings.Environment))) + }, - SigningCredentials = - { - {{~ for setting in provider.settings ~}} - {{~ if setting.type == 'SigningKey' ~}} - new SigningCredentials(settings.{{ setting.name }}, ""{{ setting.signing_algorithm }}""), - {{~ end ~}} - {{~ end ~}} - }, + EncryptionCredentials = + { + {{~ for setting in provider.settings ~}} + {{~ if setting.type == 'EncryptionKey' ~}} + new EncryptingCredentials(settings.{{ setting.name }}, ""{{ setting.encryption_algorithm }}"", SecurityAlgorithms.Aes256CbcHmacSha512), + {{~ end ~}} + {{~ end ~}} + }, - Properties = - { - [Properties.ProviderName] = Providers.{{ provider.name }}, - [Properties.ProviderSettings] = settings - } - }; + SigningCredentials = + { + {{~ for setting in provider.settings ~}} + {{~ if setting.type == 'SigningKey' ~}} + new SigningCredentials(settings.{{ setting.name }}, ""{{ setting.signing_algorithm }}""), + {{~ end ~}} + {{~ end ~}} + }, + + Properties = + { + [Properties.ProviderName] = Providers.{{ provider.name }}, + [Properties.ProviderOptions] = settings + } + }; - registration.Scopes.UnionWith(settings.Scopes); + registration.Scopes.UnionWith(settings.Scopes); - options.Registrations.Add(registration); - } + options.Registrations.Add(registration); } } {{~ end ~}} @@ -529,14 +677,15 @@ public partial class OpenIddictClientWebIntegrationHelpers { {{~ for provider in providers ~}} /// - /// Resolves the {{ provider.name }} provider settings from the specified registration. + /// Resolves the {{ provider.name }} provider options from the specified registration. /// /// The client registration. - /// The {{ provider.name }} provider settings. - /// The provider settings cannot be resolved. - public static OpenIddictClientWebIntegrationSettings.{{ provider.name }} Get{{ provider.name }}Settings(this OpenIddictClientRegistration registration) - => registration.GetProviderSettings() ?? + /// The {{ provider.name }} provider options. + /// The provider options cannot be resolved. + public static OpenIddictClientWebIntegrationOptions.{{ provider.name }} Get{{ provider.name }}Options(this OpenIddictClientRegistration registration) + => registration.GetProviderOptions() ?? throw new InvalidOperationException(SR.FormatID0333(Providers.{{ provider.name }})); + {{~ end ~}} } "); @@ -548,7 +697,7 @@ public partial class OpenIddictClientWebIntegrationHelpers }); } - static string GenerateSettings(XDocument document) + static string GenerateOptions(XDocument document) { var template = Template.Parse(@"#nullable enable @@ -556,18 +705,43 @@ using Microsoft.IdentityModel.Tokens; namespace OpenIddict.Client.WebIntegration; -public partial class OpenIddictClientWebIntegrationSettings +public partial class OpenIddictClientWebIntegrationOptions { {{~ for provider in providers ~}} /// - /// Provides various settings needed to configure the {{ provider.name }} integration. + /// Provides various options needed to configure the {{ provider.name }} integration. /// - public class {{ provider.name }} : OpenIddictClientWebIntegrationSettings + public class {{ provider.name }} { + /// + /// Gets or sets the client identifier. + /// + public string? ClientId { get; set; } + + /// + /// Gets or sets the client secret, if applicable. + /// + public string? ClientSecret { get; set; } + + /// + /// Gets or sets the redirection URL. + /// + public Uri? RedirectUri { get; set; } + + /// + /// Gets the scopes requested to the authorization server. + /// + public HashSet Scopes { get; } = new(StringComparer.Ordinal); + + /// + /// 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 ~}} {{~ if setting.description ~}} /// - /// {{ setting.description }} + /// Gets or sets {{ setting.description }}. /// {{~ end ~}} {{~ if setting.collection ~}} @@ -575,12 +749,8 @@ public partial class OpenIddictClientWebIntegrationSettings {{~ else ~}} public {{ setting.clr_type }}? {{ setting.name }} { get; set; } {{~ end ~}} - {{~ end ~}} - /// - /// Gets or sets the environment that determines the endpoints to use. - /// - public OpenIddictClientWebIntegrationEnvironments.{{ provider.name }} Environment { get; set; } + {{~ end ~}} } {{~ end ~}} } @@ -596,7 +766,8 @@ public partial class OpenIddictClientWebIntegrationSettings { Name = (string) setting.Attribute("Name"), Collection = (bool?) setting.Attribute("Collection") ?? false, - Description = (string) setting.Attribute("Description"), + Description = (string) setting.Attribute("Description") is string description ? + char.ToLower(description[0], CultureInfo.GetCultureInfo("en-US")) + description.Substring(1) : null, ClrType = (string) setting.Attribute("Type") switch { "EncryptionKey" when (string) setting.Element("EncryptionAlgorithm").Attribute("Value") diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs b/sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs index 85136371..b724e927 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs +++ b/sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs @@ -92,8 +92,9 @@ namespace OpenIddict.Sandbox.AspNet.Client .EnableRedirectionEndpointPassthrough() .EnablePostLogoutRedirectionEndpointPassthrough(); - // Register the System.Net.Http integration. - options.UseSystemNetHttp(); + // Register the System.Net.Http integration and configure the HTTP options. + options.UseSystemNetHttp() + .SetProductInformation("DemoApp", "1.0.0"); // Add a client registration matching the client application definition in the server project. options.AddRegistration(new OpenIddictClientRegistration @@ -110,24 +111,24 @@ namespace OpenIddict.Sandbox.AspNet.Client // Register the Web providers integrations. options.UseWebProviders() - .AddGitHub(new() + .UseGitHub(options => { - ClientId = "c4ade52327b01ddacff3", - ClientSecret = "da6bed851b75e317bf6b2cb67013679d9467c122", - RedirectUri = new Uri("https://localhost:44378/callback/login/github", UriKind.Absolute) + options.SetClientId("c4ade52327b01ddacff3") + .SetClientSecret("da6bed851b75e317bf6b2cb67013679d9467c122") + .SetRedirectUri("https://localhost:44378/callback/login/github"); }) - .AddGoogle(new() + .UseGoogle(options => { - ClientId = "1016114395689-kgtgq2p6dj27d7v6e2kjkoj54dgrrckh.apps.googleusercontent.com", - ClientSecret = "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", - RedirectUri = new Uri("https://localhost:44378/callback/login/google", UriKind.Absolute), - Scopes = { Scopes.Profile } + options.SetClientId("1016114395689-kgtgq2p6dj27d7v6e2kjkoj54dgrrckh.apps.googleusercontent.com") + .SetClientSecret("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") + .SetRedirectUri("https://localhost:44378/callback/login/google") + .AddScopes(Scopes.Profile); }) - .AddTwitter(new() + .UseTwitter(options => { - ClientId = "bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ", - ClientSecret = "VcohOgBp-6yQCurngo4GAyKeZh0D6SUCCSjJgEo1uRzJarjIUS", - RedirectUri = new Uri("https://localhost:44378/callback/login/twitter", UriKind.Absolute) + options.SetClientId("bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ") + .SetClientSecret("VcohOgBp-6yQCurngo4GAyKeZh0D6SUCCSjJgEo1uRzJarjIUS") + .SetRedirectUri("https://localhost:44378/callback/login/twitter"); }); }); diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs b/sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs index 3cc2078e..fcd0019b 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs +++ b/sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs @@ -140,16 +140,17 @@ namespace OpenIddict.Sandbox.AspNet.Server options.UseOwin() .EnableRedirectionEndpointPassthrough(); - // Register the System.Net.Http integration. - options.UseSystemNetHttp(); + // Register the System.Net.Http integration and configure the HTTP options. + options.UseSystemNetHttp() + .SetProductInformation("DemoApp", "1.0.0"); // Register the Web providers integrations. options.UseWebProviders() - .AddGitHub(new() + .UseGitHub(options => { - ClientId = "c4ade52327b01ddacff3", - ClientSecret = "da6bed851b75e317bf6b2cb67013679d9467c122", - RedirectUri = new Uri("https://localhost:44349/callback/login/github", UriKind.Absolute) + options.SetClientId("c4ade52327b01ddacff3") + .SetClientSecret("da6bed851b75e317bf6b2cb67013679d9467c122") + .SetRedirectUri("https://localhost:44349/callback/login/github"); }); }) diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs index 889d1204..a8a59d91 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs @@ -102,8 +102,9 @@ public class Startup .EnableRedirectionEndpointPassthrough() .EnablePostLogoutRedirectionEndpointPassthrough(); - // Register the System.Net.Http integration. - options.UseSystemNetHttp(); + // Register the System.Net.Http integration and configure the HTTP options. + options.UseSystemNetHttp() + .SetProductInformation("DemoApp", "1.0.0"); // Add a client registration matching the client application definition in the server project. options.AddRegistration(new OpenIddictClientRegistration @@ -120,32 +121,30 @@ public class Startup // Register the Web providers integrations. options.UseWebProviders() - .AddGitHub(new() + .UseGitHub(options => { - ClientId = "c4ade52327b01ddacff3", - ClientSecret = "da6bed851b75e317bf6b2cb67013679d9467c122", - RedirectUri = new Uri("https://localhost:44381/callback/login/github", UriKind.Absolute) + options.SetClientId("c4ade52327b01ddacff3") + .SetClientSecret("da6bed851b75e317bf6b2cb67013679d9467c122") + .SetRedirectUri("https://localhost:44381/callback/login/github"); }) - .AddGoogle(new() + .UseGoogle(options => { - ClientId = "1016114395689-kgtgq2p6dj27d7v6e2kjkoj54dgrrckh.apps.googleusercontent.com", - ClientSecret = "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", - RedirectUri = new Uri("https://localhost:44381/callback/login/google", UriKind.Absolute), - Scopes = { Scopes.Profile } + options.SetClientId("1016114395689-kgtgq2p6dj27d7v6e2kjkoj54dgrrckh.apps.googleusercontent.com") + .SetClientSecret("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") + .SetRedirectUri("https://localhost:44381/callback/login/google") + .AddScopes(Scopes.Profile); }) - .AddReddit(new() + .UseReddit(options => { - ClientId = "vDLNqhrkwrvqHgnoBWF3og", - ClientSecret = "Tpab28Dz0upyZLqn7AN3GFD1O-zaAw", - RedirectUri = new Uri("https://localhost:44381/callback/login/reddit", UriKind.Absolute), - ProductName = "DemoApp", - ProductVersion = "1.0.0" + options.SetClientId("vDLNqhrkwrvqHgnoBWF3og") + .SetClientSecret("Tpab28Dz0upyZLqn7AN3GFD1O-zaAw") + .SetRedirectUri("https://localhost:44381/callback/login/reddit"); }) - .AddTwitter(new() + .UseTwitter(options => { - ClientId = "bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ", - ClientSecret = "VcohOgBp-6yQCurngo4GAyKeZh0D6SUCCSjJgEo1uRzJarjIUS", - RedirectUri = new Uri("https://localhost:44381/callback/login/twitter", UriKind.Absolute) + options.SetClientId("bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ") + .SetClientSecret("VcohOgBp-6yQCurngo4GAyKeZh0D6SUCCSjJgEo1uRzJarjIUS") + .SetRedirectUri("https://localhost:44381/callback/login/twitter"); }); }); diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs index 1aacc6d1..895c7ea2 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs @@ -1,3 +1,4 @@ +using System.Net.Http.Headers; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using OpenIddict.Sandbox.AspNetCore.Server.Models; @@ -86,16 +87,17 @@ public class Startup .EnableStatusCodePagesIntegration() .EnableRedirectionEndpointPassthrough(); - // Register the System.Net.Http integration. - options.UseSystemNetHttp(); + // Register the System.Net.Http integration and configure the HTTP options. + options.UseSystemNetHttp() + .SetProductInformation("DemoApp", "1.0.0"); // Register the Web providers integrations. options.UseWebProviders() - .AddGitHub(new() + .UseGitHub(options => { - ClientId = "c4ade52327b01ddacff3", - ClientSecret = "da6bed851b75e317bf6b2cb67013679d9467c122", - RedirectUri = new Uri("https://localhost:44395/callback/login/github", UriKind.Absolute) + options.SetClientId("c4ade52327b01ddacff3") + .SetClientSecret("da6bed851b75e317bf6b2cb67013679d9467c122") + .SetRedirectUri("https://localhost:44395/callback/login/github"); }); }) diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreBuilder.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreBuilder.cs index 0c01b124..9b42b165 100644 --- a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreBuilder.cs +++ b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreBuilder.cs @@ -89,25 +89,15 @@ public class OpenIddictClientAspNetCoreBuilder public OpenIddictClientAspNetCoreBuilder EnableStatusCodePagesIntegration() => Configure(options => options.EnableStatusCodePagesIntegration = true); - /// - /// Determines whether the specified object is equal to the current object. - /// - /// The object to compare with the current object. - /// if the specified object is equal to the current object; otherwise, false. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override bool Equals(object? obj) => base.Equals(obj); - /// - /// Serves as the default hash function. - /// - /// A hash code for the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override int GetHashCode() => base.GetHashCode(); - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override string? ToString() => base.ToString(); } diff --git a/src/OpenIddict.Client.Owin/OpenIddictClientOwinBuilder.cs b/src/OpenIddict.Client.Owin/OpenIddictClientOwinBuilder.cs index 2ede1cd2..e1ad39b4 100644 --- a/src/OpenIddict.Client.Owin/OpenIddictClientOwinBuilder.cs +++ b/src/OpenIddict.Client.Owin/OpenIddictClientOwinBuilder.cs @@ -81,25 +81,15 @@ public class OpenIddictClientOwinBuilder public OpenIddictClientOwinBuilder EnableErrorPassthrough() => Configure(options => options.EnableErrorPassthrough = true); - /// - /// Determines whether the specified object is equal to the current object. - /// - /// The object to compare with the current object. - /// if the specified object is equal to the current object; otherwise, false. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override bool Equals(object? obj) => base.Equals(obj); - /// - /// Serves as the default hash function. - /// - /// A hash code for the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override int GetHashCode() => base.GetHashCode(); - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override string? ToString() => base.ToString(); } diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpBuilder.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpBuilder.cs index aa859f8d..d584ac34 100644 --- a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpBuilder.cs +++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpBuilder.cs @@ -5,6 +5,7 @@ */ using System.ComponentModel; +using System.Net.Http.Headers; using OpenIddict.Client.SystemNetHttp; using Polly; @@ -51,28 +52,37 @@ public class OpenIddictClientSystemNetHttpBuilder /// /// The HTTP Polly error policy. /// The . - public OpenIddictClientSystemNetHttpBuilder SetHttpErrorPolicy(IAsyncPolicy policy) + public OpenIddictClientSystemNetHttpBuilder SetHttpErrorPolicy(IAsyncPolicy? policy) => Configure(options => options.HttpErrorPolicy = policy); /// - /// Determines whether the specified object is equal to the current object. + /// Sets the product information used in the user agent header that is attached + /// to the backchannel HTTP requests sent to the authorization server. /// - /// The object to compare with the current object. - /// if the specified object is equal to the current object; otherwise, false. - [EditorBrowsable(EditorBrowsableState.Never)] - public override bool Equals(object? obj) => base.Equals(obj); + /// The product information. + /// The . + public OpenIddictClientSystemNetHttpBuilder SetProductInformation(ProductInfoHeaderValue? information) + => Configure(options => options.ProductInformation = information); /// - /// Serves as the default hash function. + /// Sets the product information used in the user agent header that is attached + /// to the backchannel HTTP requests sent to the authorization server. /// - /// A hash code for the current object. + /// The product name. + /// The product version. + /// The . + public OpenIddictClientSystemNetHttpBuilder SetProductInformation(string? name, string? version) + => SetProductInformation(!string.IsNullOrEmpty(name) ? new ProductInfoHeaderValue(name, version) : null); + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object? obj) => base.Equals(obj); + + /// [EditorBrowsable(EditorBrowsableState.Never)] public override int GetHashCode() => base.GetHashCode(); - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override string? ToString() => base.ToString(); } diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs index f2c542de..ec339be5 100644 --- a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs +++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs @@ -18,10 +18,10 @@ public class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptions { #if !SUPPORTS_SERVICE_PROVIDER_IN_HTTP_MESSAGE_HANDLER_BUILDER - private readonly IServiceProvider _serviceProvider; + private readonly IServiceProvider _provider; - public OpenIddictClientSystemNetHttpConfiguration(IServiceProvider serviceProvider) - => _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + public OpenIddictClientSystemNetHttpConfiguration(IServiceProvider provider) + => _provider = provider ?? throw new ArgumentNullException(nameof(provider)); #endif public void Configure(OpenIddictClientOptions options) @@ -35,8 +35,7 @@ public class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptions Debug.Fail("This infrastructure method shouldn't be called."); + public void Configure(HttpClientFactoryOptions options) => Configure(Options.DefaultName, options); public void Configure(string name, HttpClientFactoryOptions options) { @@ -45,8 +44,8 @@ public class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptions>(); #else - var options = _serviceProvider.GetRequiredService>(); + var options = _provider.GetRequiredService>(); #endif var policy = options.CurrentValue.HttpErrorPolicy; if (policy is not null) diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs index 70871df3..a5432d47 100644 --- a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs +++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs @@ -11,6 +11,7 @@ using System.Diagnostics.CodeAnalysis; using System.Net.Http.Headers; using System.Text; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpConstants; @@ -114,6 +115,11 @@ public static partial class OpenIddictClientSystemNetHttpHandlers /// public class AttachUserAgent : IOpenIddictClientHandler where TContext : BaseExternalContext { + private readonly IOptionsMonitor _options; + + public AttachUserAgent(IOptionsMonitor options) + => _options = options ?? throw new ArgumentNullException(nameof(options)); + /// /// Gets the default descriptor definition assigned to this handler. /// @@ -140,9 +146,18 @@ public static partial class OpenIddictClientSystemNetHttpHandlers var request = context.Transaction.GetHttpRequestMessage() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0173)); - var assembly = typeof(OpenIddictClientSystemNetHttpHandlers).Assembly.GetName(); + // Some authorization servers are known to aggressively check user agents and encourage + // developers to use unique user agents. While a default user agent is always added, + // the default value doesn't differ accross applications. To reduce the risks of seeing + // requests blocked, a more specific user agent header can be configured by the developer. + // In this case, the value specified by the developer always appears first in the list. + if (_options.CurrentValue.ProductInformation is ProductInfoHeaderValue information) + { + request.Headers.UserAgent.Add(information); + } // Attach a user agent based on the assembly version of the System.Net.Http integration. + var assembly = typeof(OpenIddictClientSystemNetHttpHandlers).Assembly.GetName(); request.Headers.UserAgent.Add(new ProductInfoHeaderValue( productName: assembly.Name!, productVersion: assembly.Version!.ToString())); diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpOptions.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpOptions.cs index c31806b3..10338513 100644 --- a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpOptions.cs +++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpOptions.cs @@ -5,6 +5,7 @@ */ using System.Net; +using System.Net.Http.Headers; using Polly; using Polly.Extensions.Http; @@ -18,8 +19,14 @@ public class OpenIddictClientSystemNetHttpOptions /// /// Gets or sets the HTTP Polly error policy used by the internal OpenIddict HTTP clients. /// - public IAsyncPolicy HttpErrorPolicy { get; set; } + public IAsyncPolicy? HttpErrorPolicy { get; set; } = HttpPolicyExtensions.HandleTransientHttpError() .OrResult(response => response.StatusCode == HttpStatusCode.NotFound) .WaitAndRetryAsync(4, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt))); + + /// + /// Gets or sets the product information used in the user agent header that is + /// attached to the backchannel HTTP requests sent to the authorization server. + /// + public ProductInfoHeaderValue? ProductInformation { get; set; } } diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationConstants.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationConstants.cs index 3f48b474..7f8796cb 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationConstants.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationConstants.cs @@ -16,6 +16,6 @@ public static partial class OpenIddictClientWebIntegrationConstants public static class Properties { public const string ProviderName = ".provider_name"; - public const string ProviderSettings = ".provider_settings"; + public const string ProviderOptions = ".provider_options"; } } diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationEnvironments.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationEnvironments.cs deleted file mode 100644 index 351a9786..00000000 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationEnvironments.cs +++ /dev/null @@ -1,15 +0,0 @@ -/* - * 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; - -/// -/// Exposes the provider-specific environments supported by the OpenIddict client Web integration services. -/// -public static partial class OpenIddictClientWebIntegrationEnvironments -{ - // Note: environments are automatically generated by the source generator. -} diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Discovery.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Discovery.cs index 836b2e53..c8ecbe59 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Discovery.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Discovery.cs @@ -53,8 +53,8 @@ public static partial class OpenIddictClientWebIntegrationHandlers // is replaced by this handler to always use "https://login.microsoftonline.com/common/v2.0". if (context.Registration.GetProviderName() is Providers.Microsoft) { - var settings = context.Registration.GetMicrosoftSettings(); - if (string.Equals(settings.Tenant, "common", StringComparison.OrdinalIgnoreCase)) + var options = context.Registration.GetMicrosoftOptions(); + if (string.Equals(options.Tenant, "common", StringComparison.OrdinalIgnoreCase)) { context.Response[Metadata.Issuer] = "https://login.microsoftonline.com/common/v2.0"; } diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Exchange.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Exchange.cs deleted file mode 100644 index 8ed8c4df..00000000 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Exchange.cs +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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.Collections.Immutable; - -namespace OpenIddict.Client.WebIntegration; - -public static partial class OpenIddictClientWebIntegrationHandlers -{ - public static class Exchange - { - public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( - /* - * Token request preparation: - */ - AddProductNameToUserAgentHeader.Descriptor); - } -} diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Protection.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Protection.cs index ba509c1a..a488f401 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Protection.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Protection.cs @@ -57,7 +57,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers // that is associated with the client application. Since the tenant cannot be // inferred when targeting the common tenant instance, issuer validation is disabled. Providers.Microsoft when string.Equals( - context.Registration.GetMicrosoftSettings().Tenant, + context.Registration.GetMicrosoftOptions().Tenant, "common", StringComparison.OrdinalIgnoreCase) => false, diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs index 5330b5b1..eca50931 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs @@ -19,7 +19,6 @@ public static partial class OpenIddictClientWebIntegrationHandlers /* * Userinfo request preparation: */ - AddProductNameToUserAgentHeader.Descriptor, AttachNonStandardFieldParameter.Descriptor, /* @@ -59,11 +58,11 @@ public static partial class OpenIddictClientWebIntegrationHandlers if (context.Registration.GetProviderName() is Providers.Twitter) { - var settings = context.Registration.GetTwitterSettings(); + var options = context.Registration.GetTwitterOptions(); - context.Request["expansions"] = string.Join(",", settings.Expansions); - context.Request["tweet.fields"] = string.Join(",", settings.TweetFields); - context.Request["user.fields"] = string.Join(",", settings.UserFields); + context.Request["expansions"] = string.Join(",", options.Expansions); + context.Request["tweet.fields"] = string.Join(",", options.TweetFields); + context.Request["user.fields"] = string.Join(",", options.UserFields); } return default; @@ -97,7 +96,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers Debug.Assert(context.Response is not null, SR.GetResourceString(SR.ID4007)); // Some providers are known to wrap their userinfo payloads in top-level JSON nodes - // (generally named "d", "data" or "content"), which prevents the default extraction + // (generally named "d", "data" or "response"), which prevents the default extraction // logic from mapping the parameters to CLR claims. To work around that, this handler // is responsible for extracting the nested payload and replacing the userinfo response. diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs index 2025d817..59625ba0 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs @@ -7,10 +7,7 @@ using System.Collections.Immutable; using System.ComponentModel; using System.Diagnostics; -using System.Net.Http.Headers; using System.Security.Claims; -using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpHandlerFilters; -using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpHandlers; using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants; namespace OpenIddict.Client.WebIntegration; @@ -31,7 +28,6 @@ public static partial class OpenIddictClientWebIntegrationHandlers AttachNonDefaultResponseMode.Descriptor, FormatNonStandardScopeParameter.Descriptor) .AddRange(Discovery.DefaultHandlers) - .AddRange(Exchange.DefaultHandlers) .AddRange(Protection.DefaultHandlers) .AddRange(Userinfo.DefaultHandlers); @@ -70,8 +66,8 @@ public static partial class OpenIddictClientWebIntegrationHandlers // see https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens. if (context.Registration.GetProviderName() is Providers.Apple) { - var settings = context.Registration.GetAppleSettings(); - context.ClientAssertionTokenPrincipal.SetClaim(Claims.Private.Issuer, settings.TeamId); + var options = context.Registration.GetAppleOptions(); + context.ClientAssertionTokenPrincipal.SetClaim(Claims.Private.Issuer, options.TeamId); context.ClientAssertionTokenPrincipal.SetAudiences("https://appleid.apple.com"); } @@ -200,55 +196,4 @@ public static partial class OpenIddictClientWebIntegrationHandlers return default; } } - - /// - /// Contains the logic responsible for enriching the user agent with an optional product name/product version. - /// - public class AddProductNameToUserAgentHeader : IOpenIddictClientHandler - where TContext : BaseExternalContext - { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictClientHandlerDescriptor Descriptor { get; } - = OpenIddictClientHandlerDescriptor.CreateBuilder() - .AddFilter() - .UseSingletonHandler>() - .SetOrder(AttachUserAgent.Descriptor.Order + 500) - .SetType(OpenIddictClientHandlerType.BuiltIn) - .Build(); - - /// - public ValueTask HandleAsync(TContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - // This handler only applies to System.Net.Http requests. If the HTTP request cannot be resolved, - // this may indicate that the request was incorrectly processed by another client stack. - var request = context.Transaction.GetHttpRequestMessage() ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0173)); - - // A few providers (like Reddit) are known to aggressively check user agents and encourage - // developers to use unique user agents. While OpenIddict itself always adds a user agent, - // the default value doesn't differ accross applications. To reduce the risks of seeing - // requests blocked by these providers, a more specific user agent header containing the - // product name/version set by the user (or the client identifier if unset) is appended. - var settings = context.Registration.GetProviderSettings(); - if (settings is not null) - { - var name = settings.ProductName ?? context.Registration.ClientId; - if (!string.IsNullOrEmpty(name)) - { - request.Headers.UserAgent.Add(new ProductInfoHeaderValue( - productName: name, - productVersion: settings.ProductVersion)); - } - } - - return default; - } - } } diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHelpers.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHelpers.cs index d3d67587..35f95a9b 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHelpers.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHelpers.cs @@ -32,35 +32,24 @@ public static partial class OpenIddictClientWebIntegrationHelpers } /// - /// Resolves the provider settings associated with the client registration or - /// if no provider information is attached to the registration. + /// Resolves the provider options associated with the client registration or + /// if no provider information is attached to the registration or if + /// the actual setting information doesn't match the specified . /// + /// The type of the provider options. /// The client registration. - /// The provider settings, if applicable. + /// The provider options, if applicable. /// is null. - public static OpenIddictClientWebIntegrationSettings? GetProviderSettings(this OpenIddictClientRegistration registration) + public static TOptions? GetProviderOptions(this OpenIddictClientRegistration registration) { if (registration is null) { throw new ArgumentNullException(nameof(registration)); } - return registration.Properties.TryGetValue(Properties.ProviderSettings, out var value) - && value is OpenIddictClientWebIntegrationSettings settings ? settings : null; - } - - /// - /// Resolves the provider settings associated with the client registration or - /// if no provider information is attached to the registration or if - /// the actual setting information doesn't match the specified . - /// - /// The type of the provider settings. - /// The client registration. - /// The provider settings, if applicable. - /// is null. - public static TSettings? GetProviderSettings(this OpenIddictClientRegistration registration) - where TSettings : OpenIddictClientWebIntegrationSettings - => registration.GetProviderSettings() is TSettings settings ? settings : null; + return registration.Properties.TryGetValue(Properties.ProviderOptions, out var value) + && value is TOptions options ? options : default; - // Note: provider-specific helpers are automatically generated by the source generator. + // Note: provider-specific helpers are automatically generated by the source generator. + } } diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationOptions.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationOptions.cs index bf441112..62295e17 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationOptions.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationOptions.cs @@ -9,10 +9,7 @@ namespace OpenIddict.Client.WebIntegration; /// /// Provides various settings needed to configure the OpenIddict client Web integration. /// -public class OpenIddictClientWebIntegrationOptions +public partial class OpenIddictClientWebIntegrationOptions { - /// - /// Gets the list of provider integrations enabled for this application. - /// - public List Providers { get; } = new(); + // Note: provider options are automatically generated by the source generator. } diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProvider.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProvider.cs deleted file mode 100644 index 3df2792e..00000000 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProvider.cs +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System.Diagnostics; - -namespace OpenIddict.Client.WebIntegration; - -/// -/// Represents an OpenIddict client web integration provider. -/// -[DebuggerDisplay("{Name,nq}")] -public class OpenIddictClientWebIntegrationProvider -{ - /// - /// Creates a new instance of the class. - /// - /// The provider name. - /// The provider settings. - /// is null or empty. - /// are null. - public OpenIddictClientWebIntegrationProvider( - string name, - OpenIddictClientWebIntegrationSettings settings) - { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentException(SR.GetResourceString(SR.ID0330), nameof(name)); - } - - Name = name; - Settings = settings ?? throw new ArgumentNullException(nameof(settings)); - } - - /// - /// Gets the provider name associated with the current instance. - /// - public string Name { get; } - - /// - /// Gets the provider settings associated with the current instance. - /// - public OpenIddictClientWebIntegrationSettings Settings { get; } -} diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml index cd4eb360..c62120f7 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml @@ -5,12 +5,12 @@ + Description="The Elliptic Curve Digital Signature Algorithm (ECDSA) signing key associated with the developer account"> + Description="The team ID associated with the developer account" /> @@ -36,7 +36,7 @@ + Description="The tenant used to identify the Azure AD instance (by default, the common tenant is used)" /> @@ -79,12 +79,12 @@ + Description="The list of data objects to expand from the userinfo endpoint (by default, all known expansions are requested)"> + Description="The tweet fields that should be retrieved from the userinfo endpoint (by default, all known tweet fields are requested)"> @@ -108,7 +108,7 @@ + Description="The user fields that should be retrieved from the userinfo endpoint (by default, all known user fields are requested)"> diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationSettings.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationSettings.cs deleted file mode 100644 index bfdb5a1b..00000000 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationSettings.cs +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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 abstract partial class OpenIddictClientWebIntegrationSettings -{ - /// - /// Gets or sets the client identifier. - /// - public virtual string? ClientId { get; set; } - - /// - /// Gets or sets the client secret, if applicable. - /// - public virtual string? ClientSecret { get; set; } - - /// - /// Gets or sets the product name used in the user agent header. - /// - public string? ProductName { get; set; } - - /// - /// Gets or sets the product version used in the user agent header. - /// - public string? ProductVersion { get; set; } - - /// - /// Gets or sets the redirection URL. - /// - public virtual Uri? RedirectUri { get; set; } - - /// - /// Gets the scopes requested to the authorization server. - /// - public virtual HashSet Scopes { get; } = new(StringComparer.Ordinal); -} diff --git a/src/OpenIddict.Client/OpenIddictClientBuilder.cs b/src/OpenIddict.Client/OpenIddictClientBuilder.cs index b8215768..71e2f408 100644 --- a/src/OpenIddict.Client/OpenIddictClientBuilder.cs +++ b/src/OpenIddict.Client/OpenIddictClientBuilder.cs @@ -1096,25 +1096,15 @@ public class OpenIddictClientBuilder public OpenIddictClientBuilder SetStateTokenLifetime(TimeSpan? lifetime) => Configure(options => options.StateTokenLifetime = lifetime); - /// - /// Determines whether the specified object is equal to the current object. - /// - /// The object to compare with the current object. - /// if the specified object is equal to the current object; otherwise, false. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override bool Equals(object? obj) => base.Equals(obj); - /// - /// Serves as the default hash function. - /// - /// A hash code for the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override int GetHashCode() => base.GetHashCode(); - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. + /// [EditorBrowsable(EditorBrowsableState.Never)] public override string? ToString() => base.ToString(); } diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpBuilder.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpBuilder.cs index 6ce2d98c..c190758a 100644 --- a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpBuilder.cs +++ b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpBuilder.cs @@ -5,6 +5,7 @@ */ using System.ComponentModel; +using System.Net.Http.Headers; using OpenIddict.Validation.SystemNetHttp; using Polly; @@ -51,9 +52,28 @@ public class OpenIddictValidationSystemNetHttpBuilder /// /// The HTTP Polly error policy. /// The . - public OpenIddictValidationSystemNetHttpBuilder SetHttpErrorPolicy(IAsyncPolicy policy) + public OpenIddictValidationSystemNetHttpBuilder SetHttpErrorPolicy(IAsyncPolicy? policy) => Configure(options => options.HttpErrorPolicy = policy); + /// + /// Sets the product information used in the user agent header that is attached + /// to the backchannel HTTP requests sent to the authorization server. + /// + /// The product information. + /// The . + public OpenIddictValidationSystemNetHttpBuilder SetProductInformation(ProductInfoHeaderValue? information) + => Configure(options => options.ProductInformation = information); + + /// + /// Sets the product information used in the user agent header that is attached + /// to the backchannel HTTP requests sent to the authorization server. + /// + /// The product name. + /// The product version. + /// The . + public OpenIddictValidationSystemNetHttpBuilder SetProductInformation(string? name, string? version) + => SetProductInformation(!string.IsNullOrEmpty(name) ? new ProductInfoHeaderValue(name, version) : null); + /// [EditorBrowsable(EditorBrowsableState.Never)] public override bool Equals(object? obj) => base.Equals(obj); diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConfiguration.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConfiguration.cs index f083b6bb..b19dd314 100644 --- a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConfiguration.cs +++ b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConfiguration.cs @@ -35,8 +35,7 @@ public class OpenIddictValidationSystemNetHttpConfiguration : IConfigureOptions< options.Handlers.AddRange(OpenIddictValidationSystemNetHttpHandlers.DefaultHandlers); } - public void Configure(HttpClientFactoryOptions options) - => Debug.Fail("This infrastructure method shouldn't be called."); + public void Configure(HttpClientFactoryOptions options) => Configure(Options.DefaultName, options); public void Configure(string name, HttpClientFactoryOptions options) { @@ -45,8 +44,8 @@ public class OpenIddictValidationSystemNetHttpConfiguration : IConfigureOptions< throw new ArgumentNullException(nameof(options)); } + // Only amend the HTTP client factory options if the instance is managed by OpenIddict. var assembly = typeof(OpenIddictValidationSystemNetHttpOptions).Assembly.GetName(); - if (!string.Equals(name, assembly.Name, StringComparison.Ordinal)) { return; diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs index 9388f1ef..6fc8ecaa 100644 --- a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs +++ b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs @@ -11,6 +11,7 @@ using System.Diagnostics.CodeAnalysis; using System.Net.Http.Headers; using System.Text; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using static OpenIddict.Validation.SystemNetHttp.OpenIddictValidationSystemNetHttpConstants; @@ -113,6 +114,11 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers /// public class AttachUserAgent : IOpenIddictValidationHandler where TContext : BaseExternalContext { + private readonly IOptionsMonitor _options; + + public AttachUserAgent(IOptionsMonitor options) + => _options = options ?? throw new ArgumentNullException(nameof(options)); + /// /// Gets the default descriptor definition assigned to this handler. /// @@ -139,9 +145,18 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers var request = context.Transaction.GetHttpRequestMessage() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0173)); - var assembly = typeof(OpenIddictValidationSystemNetHttpHandlers).Assembly.GetName(); + // Some authorization servers are known to aggressively check user agents and encourage + // developers to use unique user agents. While a default user agent is always added, + // the default value doesn't differ accross applications. To reduce the risks of seeing + // requests blocked, a more specific user agent header can be configured by the developer. + // In this case, the value specified by the developer always appears first in the list. + if (_options.CurrentValue.ProductInformation is ProductInfoHeaderValue information) + { + request.Headers.UserAgent.Add(information); + } // Attach a user agent based on the assembly version of the System.Net.Http integration. + var assembly = typeof(OpenIddictValidationSystemNetHttpHandlers).Assembly.GetName(); request.Headers.UserAgent.Add(new ProductInfoHeaderValue( productName: assembly.Name!, productVersion: assembly.Version!.ToString())); diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpOptions.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpOptions.cs index 1f11611a..aeb5caf5 100644 --- a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpOptions.cs +++ b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpOptions.cs @@ -5,6 +5,7 @@ */ using System.Net; +using System.Net.Http.Headers; using Polly; using Polly.Extensions.Http; @@ -22,4 +23,10 @@ public class OpenIddictValidationSystemNetHttpOptions = HttpPolicyExtensions.HandleTransientHttpError() .OrResult(response => response.StatusCode == HttpStatusCode.NotFound) .WaitAndRetryAsync(3, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt))); + + /// + /// Gets or sets the product information used in the user agent header that is + /// attached to the backchannel HTTP requests sent to the authorization server. + /// + public ProductInfoHeaderValue? ProductInformation { get; set; } }