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