diff --git a/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs b/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs
index 92726f85..3da86db5 100644
--- a/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs
+++ b/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs
@@ -124,6 +124,23 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder
[EditorBrowsable(EditorBrowsableState.Never)]
public OpenIddictClientRegistration Registration { get; }
+ ///
+ /// Adds one or more client authentication methods to the list of client authentication methods that can be negotiated for this provider.
+ ///
+ /// The client authentication methods.
+ /// Note: explicitly configuring the allowed client authentication methods is NOT recommended in most cases.
+ /// The instance.
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public {{ provider.name }} AddClientAuthenticationMethods(params string[] methods)
+ {
+ if (methods is null)
+ {
+ throw new ArgumentNullException(nameof(methods));
+ }
+
+ return Set(registration => registration.ClientAuthenticationMethods.UnionWith(methods));
+ }
+
///
/// Adds one or more code challenge methods to the list of code challenge methods that can be negotiated for this provider.
///
@@ -791,17 +808,23 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder
ClrType = (string) setting.Attribute("Type") switch
{
"Boolean" => "bool",
- "EncryptionKey" when (string) setting.Element("EncryptionAlgorithm").Attribute("Value")
- is "RS256" or "RS384" or "RS512" => "RsaSecurityKey",
+ "EncryptionKey" => (string?) setting.Element("EncryptionAlgorithm")?.Attribute("Value") switch
+ {
+ "RS256" or "RS384" or "RS512" => "RsaSecurityKey",
- "SigningKey" when (string) setting.Element("SigningAlgorithm").Attribute("Value")
- is "ES256" or "ES384" or "ES512" => "ECDsaSecurityKey",
+ _ => "SecurityKey"
+ },
- "SigningKey" when (string) setting.Element("SigningAlgorithm").Attribute("Value")
- is "PS256" or "PS384" or "PS512" or
- "RS256" or "RS384" or "RS512" => "RsaSecurityKey",
+ "SigningCertificate" => "X509Certificate2",
+
+ "SigningKey" => (string?) setting.Element("SigningAlgorithm")?.Attribute("Value") switch
+ {
+ "ES256" or "ES384" or "ES512" => "ECDsaSecurityKey",
+ "PS256" or "PS384" or "PS512" or "RS256" or "RS384" or "RS512" => "RsaSecurityKey",
+
+ _ => "SecurityKey"
+ },
- "Certificate" => "X509Certificate2",
"String" => "string",
"StringHashSet" => "HashSet",
"Uri" => "Uri",
@@ -1121,13 +1144,111 @@ public sealed partial class OpenIddictClientWebIntegrationConfiguration
{{~ for setting in provider.settings ~}}
{{~ if setting.type == 'EncryptionKey' ~}}
- registration.EncryptionCredentials.Add(new EncryptingCredentials(settings.{{ setting.property_name }}, ""{{ setting.encryption_algorithm }}"", SecurityAlgorithms.Aes256CbcHmacSha512));
+ if (settings.{{ setting.property_name }} is not null)
+ {
+ registration.EncryptionCredentials.Add(new EncryptingCredentials(settings.{{ setting.property_name }}, ""{{ setting.encryption_algorithm }}"", SecurityAlgorithms.Aes256CbcHmacSha512));
+ }
{{~ end ~}}
{{~ end ~}}
{{~ for setting in provider.settings ~}}
+ {{~ if setting.type == 'SigningCertificate' ~}}
+ if (settings.{{ setting.property_name }} is not null)
+ {
+ var key = new X509SecurityKey(settings.{{ setting.property_name }});
+ if (key.IsSupportedAlgorithm(SecurityAlgorithms.RsaSha256))
+ {
+ registration.SigningCredentials.Add(new SigningCredentials(key, SecurityAlgorithms.RsaSha256));
+ }
+
+ else if (key.IsSupportedAlgorithm(SecurityAlgorithms.HmacSha256))
+ {
+ registration.SigningCredentials.Add(new SigningCredentials(key, SecurityAlgorithms.HmacSha256));
+ }
+
+#if SUPPORTS_ECDSA
+ // Note: ECDSA algorithms are bound to specific curves and must be treated separately.
+ else if (key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha256))
+ {
+ registration.SigningCredentials.Add(new SigningCredentials(key, SecurityAlgorithms.EcdsaSha256));
+ }
+
+ else if (key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha384))
+ {
+ registration.SigningCredentials.Add(new SigningCredentials(key, SecurityAlgorithms.EcdsaSha384));
+ }
+
+ else if (key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha512))
+ {
+ registration.SigningCredentials.Add(new SigningCredentials(key, SecurityAlgorithms.EcdsaSha512));
+ }
+#else
+ else if (key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha256) ||
+ key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha384) ||
+ key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha512))
+ {
+ throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0069));
+ }
+#endif
+ else
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0068));
+ }
+ }
+ {{~ end ~}}
{{~ if setting.type == 'SigningKey' ~}}
- registration.SigningCredentials.Add(new SigningCredentials(settings.{{ setting.property_name }}, ""{{ setting.signing_algorithm }}""));
+ if (settings.{{ setting.property_name }} is not null)
+ {
+ // If the signing key is an asymmetric security key, ensure it has a private key.
+ if (settings.{{ setting.property_name }} is AsymmetricSecurityKey asymmetricSecurityKey &&
+ asymmetricSecurityKey.PrivateKeyStatus is PrivateKeyStatus.DoesNotExist)
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0067));
+ }
+
+ {{~ if setting.signing_algorithm ~}}
+ registration.SigningCredentials.Add(new SigningCredentials(settings.{{ setting.property_name }}, ""{{ setting.signing_algorithm }}""));
+ {{~ else ~}}
+ if (settings.{{ setting.property_name }}.IsSupportedAlgorithm(SecurityAlgorithms.RsaSha256))
+ {
+ registration.SigningCredentials.Add(new SigningCredentials(settings.{{ setting.property_name }}, SecurityAlgorithms.RsaSha256));
+ }
+
+ else if (settings.{{ setting.property_name }}.IsSupportedAlgorithm(SecurityAlgorithms.HmacSha256))
+ {
+ registration.SigningCredentials.Add(new SigningCredentials(settings.{{ setting.property_name }}, SecurityAlgorithms.HmacSha256));
+ }
+
+#if SUPPORTS_ECDSA
+ // Note: ECDSA algorithms are bound to specific curves and must be treated separately.
+ else if (settings.{{ setting.property_name }}.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha256))
+ {
+ registration.SigningCredentials.Add(new SigningCredentials(settings.{{ setting.property_name }}, SecurityAlgorithms.EcdsaSha256));
+ }
+
+ else if (settings.{{ setting.property_name }}.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha384))
+ {
+ registration.SigningCredentials.Add(new SigningCredentials(settings.{{ setting.property_name }}, SecurityAlgorithms.EcdsaSha384));
+ }
+
+ else if (settings.{{ setting.property_name }}.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha512))
+ {
+ registration.SigningCredentials.Add(new SigningCredentials(settings.{{ setting.property_name }}, SecurityAlgorithms.EcdsaSha512));
+ }
+#else
+ else if (settings.{{ setting.property_name }}.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha256) ||
+ settings.{{ setting.property_name }}.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha384) ||
+ settings.{{ setting.property_name }}.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha512))
+ {
+ throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0069));
+ }
+#endif
+ else
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0068));
+ }
+ {{~ end ~}}
+ }
{{~ end ~}}
{{~ end ~}}
}
@@ -1379,17 +1500,23 @@ public sealed partial class OpenIddictClientWebIntegrationSettings
ClrType = (string) setting.Attribute("Type") switch
{
"Boolean" => "bool",
- "EncryptionKey" when (string) setting.Element("EncryptionAlgorithm").Attribute("Value")
- is "RS256" or "RS384" or "RS512" => "RsaSecurityKey",
+ "EncryptionKey" => (string?) setting.Element("EncryptionAlgorithm")?.Attribute("Value") switch
+ {
+ "RS256" or "RS384" or "RS512" => "RsaSecurityKey",
- "SigningKey" when (string) setting.Element("SigningAlgorithm").Attribute("Value")
- is "ES256" or "ES384" or "ES512" => "ECDsaSecurityKey",
+ _ => "SecurityKey"
+ },
- "SigningKey" when (string) setting.Element("SigningAlgorithm").Attribute("Value")
- is "PS256" or "PS384" or "PS512" or
- "RS256" or "RS384" or "RS512" => "RsaSecurityKey",
+ "SigningCertificate" => "X509Certificate2",
+
+ "SigningKey" => (string?) setting.Element("SigningAlgorithm")?.Attribute("Value") switch
+ {
+ "ES256" or "ES384" or "ES512" => "ECDsaSecurityKey",
+ "PS256" or "PS384" or "PS512" or "RS256" or "RS384" or "RS512" => "RsaSecurityKey",
+
+ _ => "SecurityKey"
+ },
- "Certificate" => "X509Certificate2",
"String" => "string",
"StringHashSet" => "HashSet",
"Uri" => "Uri",
diff --git a/src/OpenIddict.Abstractions/OpenIddictConstants.cs b/src/OpenIddict.Abstractions/OpenIddictConstants.cs
index 728a42bf..d53e17e4 100644
--- a/src/OpenIddict.Abstractions/OpenIddictConstants.cs
+++ b/src/OpenIddict.Abstractions/OpenIddictConstants.cs
@@ -178,6 +178,8 @@ public static class OpenIddictConstants
public const string ClientSecretPost = "client_secret_post";
public const string None = "none";
public const string PrivateKeyJwt = "private_key_jwt";
+ public const string SelfSignedTlsClientAuth = "self_signed_tls_client_auth";
+ public const string TlsClientAuth = "tls_client_auth";
}
public static class ClientTypes
@@ -290,6 +292,7 @@ public static class OpenIddictConstants
public const string IntrospectionEndpointAuthSigningAlgValuesSupported = "introspection_endpoint_auth_signing_alg_values_supported";
public const string Issuer = "issuer";
public const string JwksUri = "jwks_uri";
+ public const string MtlsEndpointAliases = "mtls_endpoint_aliases";
public const string OpPolicyUri = "op_policy_uri";
public const string OpTosUri = "op_tos_uri";
public const string RequestObjectEncryptionAlgValuesSupported = "request_object_encryption_alg_values_supported";
@@ -306,6 +309,7 @@ public static class OpenIddictConstants
public const string ScopesSupported = "scopes_supported";
public const string ServiceDocumentation = "service_documentation";
public const string SubjectTypesSupported = "subject_types_supported";
+ public const string TlsClientCertificateBoundAccessTokens = "tls_client_certificate_bound_access_tokens";
public const string TokenEndpoint = "token_endpoint";
public const string TokenEndpointAuthMethodsSupported = "token_endpoint_auth_methods_supported";
public const string TokenEndpointAuthSigningAlgValuesSupported = "token_endpoint_auth_signing_alg_values_supported";
@@ -531,6 +535,12 @@ public static class OpenIddictConstants
public const string Public = "public";
}
+ public static class TokenBindingMethods
+ {
+ public const string SelfSignedTlsClientCertificate = "self_signed_tls_client_certificate";
+ public const string TlsClientCertificate = "tls_client_certificate";
+ }
+
public static class TokenFormats
{
public const string Jwt = "urn:ietf:params:oauth:token-type:jwt";
diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx
index 901b978e..3fa78aca 100644
--- a/src/OpenIddict.Abstractions/OpenIddictResources.resx
+++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx
@@ -1698,6 +1698,12 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
The format of the specified certificate is not supported.
+
+ Registration identifiers cannot contain U+001E or U+001F characters.
+
+
+ The specified client authentication method/token binding methods combination is not valid.
+
The security token is missing.
diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictConfiguration.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictConfiguration.cs
index 35ea9b0a..3b357df8 100644
--- a/src/OpenIddict.Abstractions/Primitives/OpenIddictConfiguration.cs
+++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictConfiguration.cs
@@ -76,6 +76,31 @@ public sealed class OpenIddictConfiguration
///
public Uri? JwksUri { get; set; }
+ ///
+ /// Gets or sets the URI of the mTLS-enabled device authorization endpoint.
+ ///
+ public Uri? MtlsDeviceAuthorizationEndpoint { get; set; }
+
+ ///
+ /// Gets or sets the URI of the mTLS-enabled introspection endpoint.
+ ///
+ public Uri? MtlsIntrospectionEndpoint { get; set; }
+
+ ///
+ /// Gets or sets the URI of the mTLS-enabled revocation endpoint.
+ ///
+ public Uri? MtlsRevocationEndpoint { get; set; }
+
+ ///
+ /// Gets or sets the URI of the mTLS-enabled token endpoint.
+ ///
+ public Uri? MtlsTokenEndpoint { get; set; }
+
+ ///
+ /// Gets or sets the URI of the mTLS-enabled userinfo endpoint.
+ ///
+ public Uri? MtlsUserInfoEndpoint { get; set; }
+
///
/// Gets the additional properties.
///
@@ -111,6 +136,12 @@ public sealed class OpenIddictConfiguration
///
public List SigningKeys { get; } = [];
+ ///
+ /// Gets or sets a boolean indicating whether access tokens issued by the
+ /// authorization server are bound to the client certificate when using mTLS.
+ ///
+ public bool? TlsClientCertificateBoundAccessTokens { get; set; }
+
///
/// Gets or sets the URI of the token endpoint.
///
diff --git a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationConfiguration.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationConfiguration.cs
index 48a8ddfe..247beddf 100644
--- a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationConfiguration.cs
+++ b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationConfiguration.cs
@@ -50,6 +50,9 @@ public sealed class OpenIddictClientSystemIntegrationConfiguration : IConfigureO
// Register the built-in event handlers used by the OpenIddict client system integration components.
options.Handlers.AddRange(OpenIddictClientSystemIntegrationHandlers.DefaultHandlers);
+
+ // Enable response_mode=fragment support by default.
+ options.ResponseModes.Add(ResponseModes.Fragment);
}
///
diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpBuilder.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpBuilder.cs
index f9ac960d..8aa39b44 100644
--- a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpBuilder.cs
+++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpBuilder.cs
@@ -9,6 +9,7 @@ using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Mail;
using System.Reflection;
+using System.Security.Cryptography.X509Certificates;
using OpenIddict.Client;
using OpenIddict.Client.SystemNetHttp;
using Polly;
@@ -339,6 +340,54 @@ public sealed class OpenIddictClientSystemNetHttpBuilder
productVersion: assembly.GetName().Version!.ToString()));
}
+ ///
+ /// Sets the delegate called by OpenIddict when trying to resolve the self-signed
+ /// TLS client authentication certificate that will be used for OAuth 2.0
+ /// mTLS-based client authentication (self_signed_tls_client_auth), if applicable.
+ ///
+ /// The selector delegate.
+ ///
+ /// If no value is explicitly set, OpenIddict automatically tries to resolve the
+ /// X.509 certificate from the signing credentials attached to the client registration
+ /// (in this case, the X.509 certificate MUST include the digital signature and
+ /// client authentication key usages to be automatically selected by OpenIddict).
+ ///
+ /// The instance.
+ public OpenIddictClientSystemNetHttpBuilder SetSelfSignedTlsClientAuthenticationCertificateSelector(
+ Func selector)
+ {
+ if (selector is null)
+ {
+ throw new ArgumentNullException(nameof(selector));
+ }
+
+ return Configure(options => options.SelfSignedTlsClientAuthenticationCertificateSelector = selector);
+ }
+
+ ///
+ /// Sets the delegate called by OpenIddict when trying to resolve the
+ /// TLS client authentication certificate that will be used for OAuth 2.0
+ /// mTLS-based client authentication (tls_client_auth), if applicable.
+ ///
+ /// The selector delegate.
+ ///
+ /// If no value is explicitly set, OpenIddict automatically tries to resolve the
+ /// X.509 certificate from the signing credentials attached to the client registration
+ /// (in this case, the X.509 certificate MUST include the digital signature and
+ /// client authentication key usages to be automatically selected by OpenIddict).
+ ///
+ /// The instance.
+ public OpenIddictClientSystemNetHttpBuilder SetTlsClientAuthenticationCertificateSelector(
+ Func selector)
+ {
+ if (selector is null)
+ {
+ throw new ArgumentNullException(nameof(selector));
+ }
+
+ return Configure(options => options.TlsClientAuthenticationCertificateSelector = selector);
+ }
+
///
[EditorBrowsable(EditorBrowsableState.Never)]
public override bool Equals(object? obj) => base.Equals(obj);
diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs
index 78cf46a3..781f3317 100644
--- a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs
+++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs
@@ -5,12 +5,14 @@
*/
using System.ComponentModel;
-using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Net.Http;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.Tokens;
using Polly;
#if SUPPORTS_HTTP_CLIENT_RESILIENCE
@@ -25,7 +27,8 @@ namespace OpenIddict.Client.SystemNetHttp;
[EditorBrowsable(EditorBrowsableState.Advanced)]
public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptions,
IConfigureNamedOptions,
- IPostConfigureOptions
+ IPostConfigureOptions,
+ IPostConfigureOptions
{
private readonly IServiceProvider _provider;
@@ -46,6 +49,11 @@ public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptio
// Register the built-in event handlers used by the OpenIddict System.Net.Http client components.
options.Handlers.AddRange(OpenIddictClientSystemNetHttpHandlers.DefaultHandlers);
+
+ // Enable client_secret_basic, self_signed_tls_client_auth and tls_client_auth support by default.
+ options.ClientAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretBasic);
+ options.ClientAuthenticationMethods.Add(ClientAuthenticationMethods.SelfSignedTlsClientAuth);
+ options.ClientAuthenticationMethods.Add(ClientAuthenticationMethods.TlsClientAuth);
}
///
@@ -59,8 +67,29 @@ public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptio
throw new ArgumentNullException(nameof(options));
}
+ var assembly = typeof(OpenIddictClientSystemNetHttpOptions).Assembly.GetName();
+
// Only amend the HTTP client factory options if the instance is managed by OpenIddict.
- if (string.IsNullOrEmpty(name) || !TryResolveRegistrationId(name, out string? identifier))
+ if (string.IsNullOrEmpty(name) || !name.StartsWith(assembly.Name!, StringComparison.Ordinal))
+ {
+ return;
+ }
+
+ // Note: HttpClientFactory doesn't support flowing a list of properties that can be
+ // accessed from the HttpClientAction or HttpMessageHandlerBuilderAction delegates
+ // to dynamically amend the resulting HttpClient or HttpClientHandler instance.
+ //
+ // To work around this limitation, the OpenIddict System.Net.Http integration uses
+ // dynamic client names and supports appending a list of key-value pairs to the client
+ // name to flow per-instance properties (e.g the negotiated client authentication method).
+ var properties = name.Length >= assembly.Name!.Length + 1 && name[assembly.Name.Length] is ':' ?
+ name[(assembly.Name.Length + 1)..]
+ .Split(['\u001f'], StringSplitOptions.RemoveEmptyEntries)
+ .Select(static property => property.Split(['\u001e'], StringSplitOptions.RemoveEmptyEntries))
+ .Where(static values => values is [{ Length: > 0 }, { Length: > 0 }])
+ .ToDictionary(static values => values[0], static values => values[1]) : [];
+
+ if (!properties.TryGetValue("RegistrationId", out string? identifier) || string.IsNullOrEmpty(identifier))
{
return;
}
@@ -113,6 +142,32 @@ public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptio
#pragma warning restore EXTEXP0001
}
#endif
+ if (builder.PrimaryHandler is not HttpClientHandler handler)
+ {
+ throw new InvalidOperationException(SR.FormatID0373(typeof(HttpClientHandler).FullName));
+ }
+
+ handler.ClientCertificateOptions = ClientCertificateOption.Manual;
+
+ if (properties.TryGetValue("AttachTlsClientCertificate", out string? value) &&
+ bool.TryParse(value, out bool result) && result)
+ {
+ var certificate = options.CurrentValue.TlsClientAuthenticationCertificateSelector(registration);
+ if (certificate is not null)
+ {
+ handler.ClientCertificates.Add(certificate);
+ }
+ }
+
+ else if (properties.TryGetValue("AttachSelfSignedTlsClientCertificate", out value) &&
+ bool.TryParse(value, out result) && result)
+ {
+ var certificate = options.CurrentValue.SelfSignedTlsClientAuthenticationCertificateSelector(registration);
+ if (certificate is not null)
+ {
+ handler.ClientCertificates.Add(certificate);
+ }
+ }
});
// Register the user-defined HTTP client handler actions.
@@ -132,8 +187,29 @@ public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptio
throw new ArgumentNullException(nameof(options));
}
+ var assembly = typeof(OpenIddictClientSystemNetHttpOptions).Assembly.GetName();
+
// Only amend the HTTP client factory options if the instance is managed by OpenIddict.
- if (string.IsNullOrEmpty(name) || !TryResolveRegistrationId(name, out string? identifier))
+ if (string.IsNullOrEmpty(name) || !name.StartsWith(assembly.Name!, StringComparison.Ordinal))
+ {
+ return;
+ }
+
+ // Note: HttpClientFactory doesn't support flowing a list of properties that can be
+ // accessed from the HttpClientAction or HttpMessageHandlerBuilderAction delegates
+ // to dynamically amend the resulting HttpClient or HttpClientHandler instance.
+ //
+ // To work around this limitation, the OpenIddict System.Net.Http integration uses dynamic
+ // client names and supports appending a list of key-value pairs to the client name to flow
+ // per-instance properties (e.g a flag indicating whether a client certificate should be used).
+ var properties = name.Length >= assembly.Name!.Length + 1 && name[assembly.Name.Length] is ':' ?
+ name[(assembly.Name.Length + 1)..]
+ .Split(['\u001f'], StringSplitOptions.RemoveEmptyEntries)
+ .Select(static property => property.Split(['\u001e'], StringSplitOptions.RemoveEmptyEntries))
+ .Where(static values => values is [{ Length: > 0 }, { Length: > 0 }])
+ .ToDictionary(static values => values[0], static values => values[1]) : [];
+
+ if (!properties.TryGetValue("RegistrationId", out string? identifier) || string.IsNullOrEmpty(identifier))
{
return;
}
@@ -194,19 +270,94 @@ public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptio
});
}
- static bool TryResolveRegistrationId(string name, [NotNullWhen(true)] out string? value)
+ ///
+ public void PostConfigure(string? name, OpenIddictClientSystemNetHttpOptions options)
{
- var assembly = typeof(OpenIddictClientSystemNetHttpOptions).Assembly.GetName();
+ if (options is null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ // If no client authentication certificate selector was provided, use fallback delegates that
+ // automatically use the first X.509 signing certificate attached to the client registration
+ // that is suitable for both digital signature and client authentication.
+
+ options.SelfSignedTlsClientAuthenticationCertificateSelector ??= static registration =>
+ {
+ foreach (var credentials in registration.SigningCredentials)
+ {
+ if (credentials.Key is X509SecurityKey { Certificate: X509Certificate2 certificate } &&
+ certificate.Version is >= 3 && IsSelfIssuedCertificate(certificate) &&
+ HasDigitalSignatureKeyUsage(certificate) &&
+ HasClientAuthenticationExtendedKeyUsage(certificate))
+ {
+ return certificate;
+ }
+ }
+
+ return null;
+ };
+
+ options.TlsClientAuthenticationCertificateSelector ??= static registration =>
+ {
+ foreach (var credentials in registration.SigningCredentials)
+ {
+ if (credentials.Key is X509SecurityKey { Certificate: X509Certificate2 certificate } &&
+ certificate.Version is >= 3 && !IsSelfIssuedCertificate(certificate) &&
+ HasDigitalSignatureKeyUsage(certificate) &&
+ HasClientAuthenticationExtendedKeyUsage(certificate))
+ {
+ return certificate;
+ }
+ }
+
+ return null;
+ };
- if (!name.StartsWith(assembly.Name!, StringComparison.Ordinal) ||
- name.Length < assembly.Name!.Length + 1 ||
- name[assembly.Name.Length] is not ':')
+ static bool HasClientAuthenticationExtendedKeyUsage(X509Certificate2 certificate)
{
- value = null;
+ for (var index = 0; index < certificate.Extensions.Count; index++)
+ {
+ if (certificate.Extensions[index] is X509EnhancedKeyUsageExtension extension &&
+ HasOid(extension.EnhancedKeyUsages, "1.3.6.1.5.5.7.3.2"))
+ {
+ return true;
+ }
+ }
+
+ return false;
+
+ static bool HasOid(OidCollection collection, string value)
+ {
+ for (var index = 0; index < collection.Count; index++)
+ {
+ if (collection[index] is Oid oid && string.Equals(oid.Value, value, StringComparison.Ordinal))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+ static bool HasDigitalSignatureKeyUsage(X509Certificate2 certificate)
+ {
+ for (var index = 0; index < certificate.Extensions.Count; index++)
+ {
+ if (certificate.Extensions[index] is X509KeyUsageExtension extension &&
+ extension.KeyUsages.HasFlag(X509KeyUsageFlags.DigitalSignature))
+ {
+ return true;
+ }
+ }
+
return false;
}
- value = name[(assembly.Name.Length + 1)..];
- return true;
+ // Note: to avoid building and introspecting a X.509 certificate chain and reduce the cost
+ // of this check, a certificate is always assumed to be self-signed when it is self-issued.
+ static bool IsSelfIssuedCertificate(X509Certificate2 certificate)
+ => certificate.SubjectName.RawData.AsSpan().SequenceEqual(certificate.IssuerName.RawData);
}
}
diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpExtensions.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpExtensions.cs
index 544053ec..2273ba1a 100644
--- a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpExtensions.cs
+++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpExtensions.cs
@@ -49,6 +49,9 @@ public static class OpenIddictClientSystemNetHttpExtensions
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
IPostConfigureOptions, OpenIddictClientSystemNetHttpConfiguration>());
+ builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
+ IPostConfigureOptions, OpenIddictClientSystemNetHttpConfiguration>());
+
return new OpenIddictClientSystemNetHttpBuilder(builder.Services);
}
diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Device.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Device.cs
index b0ef42f7..f848d329 100644
--- a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Device.cs
+++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Device.cs
@@ -5,10 +5,6 @@
*/
using System.Collections.Immutable;
-using System.Diagnostics;
-using System.Net.Http;
-using System.Net.Http.Headers;
-using System.Text;
namespace OpenIddict.Client.SystemNetHttp;
@@ -26,7 +22,7 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
AttachJsonAcceptHeaders.Descriptor,
AttachUserAgentHeader.Descriptor,
AttachFromHeader.Descriptor,
- AttachBasicAuthenticationCredentials.Descriptor,
+ AttachBasicAuthenticationCredentials.Descriptor,
AttachHttpParameters.Descriptor,
SendHttpRequest.Descriptor,
DisposeHttpRequest.Descriptor,
@@ -40,94 +36,5 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
ValidateHttpResponse.Descriptor,
DisposeHttpResponse.Descriptor
]);
-
- ///
- /// Contains the logic responsible for attaching the client credentials to the HTTP Authorization header.
- ///
- public sealed class AttachBasicAuthenticationCredentials : IOpenIddictClientHandler
- {
- ///
- /// Gets the default descriptor definition assigned to this handler.
- ///
- public static OpenIddictClientHandlerDescriptor Descriptor { get; }
- = OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
- .UseSingletonHandler()
- .SetOrder(AttachHttpParameters.Descriptor.Order - 500)
- .SetType(OpenIddictClientHandlerType.BuiltIn)
- .Build();
-
- ///
- public ValueTask HandleAsync(PrepareDeviceAuthorizationRequestContext context)
- {
- if (context is null)
- {
- throw new ArgumentNullException(nameof(context));
- }
-
- Debug.Assert(context.Request is not null, SR.GetResourceString(SR.ID4008));
-
- // This handler only applies to System.Net.Http requests. If the HTTP request cannot be resolved,
- // this may indicate that the request was incorrectly processed by another client stack.
- var request = context.Transaction.GetHttpRequestMessage() ??
- throw new InvalidOperationException(SR.GetResourceString(SR.ID0173));
-
- // The OAuth 2.0 specification recommends sending the client credentials using basic authentication.
- // However, this authentication method is known to have severe compatibility/interoperability issues:
- //
- // - While restricted to clients that have been given a secret (i.e confidential clients) by the
- // specification, basic authentication is also sometimes required by server implementations for
- // public clients that don't have a client secret: in this case, an empty password is used and
- // the client identifier is sent alone in the Authorization header (instead of being sent using
- // the standard "client_id" parameter present in the request body).
- //
- // - While the OAuth 2.0 specification requires that the client credentials be formURL-encoded
- // before being base64-encoded, many implementations are known to implement a non-standard
- // encoding scheme, where neither the client_id nor the client_secret are formURL-encoded.
- //
- // To guarantee that the OpenIddict implementation can be used with most servers implementions,
- // basic authentication is only used when a client secret is present and client_secret_post is
- // always preferred when it's explicitly listed as a supported client authentication method.
- // If client_secret_post is not listed or if the server returned an empty methods list,
- // client_secret_basic is always used, as it MUST be implemented by all OAuth 2.0 servers.
- //
- // See https://tools.ietf.org/html/rfc8414#section-2
- // and https://tools.ietf.org/html/rfc6749#section-2.3.1 for more information.
- if (request.Headers.Authorization is null &&
- !string.IsNullOrEmpty(context.Request.ClientId) &&
- !string.IsNullOrEmpty(context.Request.ClientSecret) &&
- UseBasicAuthentication(context.Configuration))
- {
- // Important: the credentials MUST be formURL-encoded before being base64-encoded.
- var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes(new StringBuilder()
- .Append(EscapeDataString(context.Request.ClientId))
- .Append(':')
- .Append(EscapeDataString(context.Request.ClientSecret))
- .ToString()));
-
- // Attach the authorization header containing the client credentials to the HTTP request.
- request.Headers.Authorization = new AuthenticationHeaderValue(Schemes.Basic, credentials);
-
- // Remove the client credentials from the request payload to ensure they are not sent twice.
- context.Request.ClientId = context.Request.ClientSecret = null;
- }
-
- return default;
-
- static bool UseBasicAuthentication(OpenIddictConfiguration configuration)
- => configuration.DeviceAuthorizationEndpointAuthMethodsSupported switch
- {
- // If at least one authentication method was explicit added, only use basic authentication
- // if it's supported AND if client_secret_post is not supported or enabled by the server.
- { Count: > 0 } methods => methods.Contains(ClientAuthenticationMethods.ClientSecretBasic) &&
- !methods.Contains(ClientAuthenticationMethods.ClientSecretPost),
-
- // Otherwise, if no authentication method was explicit added, assume only basic is supported.
- { Count: _ } => true
- };
-
- static string EscapeDataString(string value) => Uri.EscapeDataString(value).Replace("%20", "+");
- }
- }
}
}
diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Exchange.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Exchange.cs
index 11f54ce5..bc024fd8 100644
--- a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Exchange.cs
+++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Exchange.cs
@@ -26,7 +26,7 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
AttachJsonAcceptHeaders.Descriptor,
AttachUserAgentHeader.Descriptor,
AttachFromHeader.Descriptor,
- AttachBasicAuthenticationCredentials.Descriptor,
+ AttachBasicAuthenticationCredentials.Descriptor,
AttachHttpParameters.Descriptor,
SendHttpRequest.Descriptor,
DisposeHttpRequest.Descriptor,
@@ -40,94 +40,5 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
ValidateHttpResponse.Descriptor,
DisposeHttpResponse.Descriptor
]);
-
- ///
- /// Contains the logic responsible for attaching the client credentials to the HTTP Authorization header.
- ///
- public sealed class AttachBasicAuthenticationCredentials : IOpenIddictClientHandler
- {
- ///
- /// Gets the default descriptor definition assigned to this handler.
- ///
- public static OpenIddictClientHandlerDescriptor Descriptor { get; }
- = OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
- .UseSingletonHandler()
- .SetOrder(AttachHttpParameters.Descriptor.Order - 500)
- .SetType(OpenIddictClientHandlerType.BuiltIn)
- .Build();
-
- ///
- public ValueTask HandleAsync(PrepareTokenRequestContext context)
- {
- if (context is null)
- {
- throw new ArgumentNullException(nameof(context));
- }
-
- Debug.Assert(context.Request is not null, SR.GetResourceString(SR.ID4008));
-
- // This handler only applies to System.Net.Http requests. If the HTTP request cannot be resolved,
- // this may indicate that the request was incorrectly processed by another client stack.
- var request = context.Transaction.GetHttpRequestMessage() ??
- throw new InvalidOperationException(SR.GetResourceString(SR.ID0173));
-
- // The OAuth 2.0 specification recommends sending the client credentials using basic authentication.
- // However, this authentication method is known to have severe compatibility/interoperability issues:
- //
- // - While restricted to clients that have been given a secret (i.e confidential clients) by the
- // specification, basic authentication is also sometimes required by server implementations for
- // public clients that don't have a client secret: in this case, an empty password is used and
- // the client identifier is sent alone in the Authorization header (instead of being sent using
- // the standard "client_id" parameter present in the request body).
- //
- // - While the OAuth 2.0 specification requires that the client credentials be formURL-encoded
- // before being base64-encoded, many implementations are known to implement a non-standard
- // encoding scheme, where neither the client_id nor the client_secret are formURL-encoded.
- //
- // To guarantee that the OpenIddict implementation can be used with most servers implementions,
- // basic authentication is only used when a client secret is present and client_secret_post is
- // always preferred when it's explicitly listed as a supported client authentication method.
- // If client_secret_post is not listed or if the server returned an empty methods list,
- // client_secret_basic is always used, as it MUST be implemented by all OAuth 2.0 servers.
- //
- // See https://tools.ietf.org/html/rfc8414#section-2
- // and https://tools.ietf.org/html/rfc6749#section-2.3.1 for more information.
- if (request.Headers.Authorization is null &&
- !string.IsNullOrEmpty(context.Request.ClientId) &&
- !string.IsNullOrEmpty(context.Request.ClientSecret) &&
- UseBasicAuthentication(context.Configuration))
- {
- // Important: the credentials MUST be formURL-encoded before being base64-encoded.
- var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes(new StringBuilder()
- .Append(EscapeDataString(context.Request.ClientId))
- .Append(':')
- .Append(EscapeDataString(context.Request.ClientSecret))
- .ToString()));
-
- // Attach the authorization header containing the client credentials to the HTTP request.
- request.Headers.Authorization = new AuthenticationHeaderValue(Schemes.Basic, credentials);
-
- // Remove the client credentials from the request payload to ensure they are not sent twice.
- context.Request.ClientId = context.Request.ClientSecret = null;
- }
-
- return default;
-
- static bool UseBasicAuthentication(OpenIddictConfiguration configuration)
- => configuration.TokenEndpointAuthMethodsSupported switch
- {
- // If at least one authentication method was explicit added, only use basic authentication
- // if it's supported AND if client_secret_post is not supported or enabled by the server.
- { Count: > 0 } methods => methods.Contains(ClientAuthenticationMethods.ClientSecretBasic) &&
- !methods.Contains(ClientAuthenticationMethods.ClientSecretPost),
-
- // Otherwise, if no authentication method was explicit added, assume only basic is supported.
- { Count: _ } => true
- };
-
- static string EscapeDataString(string value) => Uri.EscapeDataString(value).Replace("%20", "+");
- }
- }
}
}
diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Introspection.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Introspection.cs
index fd4a390d..f9f45e45 100644
--- a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Introspection.cs
+++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Introspection.cs
@@ -26,7 +26,7 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
AttachJsonAcceptHeaders.Descriptor,
AttachUserAgentHeader.Descriptor,
AttachFromHeader.Descriptor,
- AttachBasicAuthenticationCredentials.Descriptor,
+ AttachBasicAuthenticationCredentials.Descriptor,
AttachHttpParameters.Descriptor,
SendHttpRequest.Descriptor,
DisposeHttpRequest.Descriptor,
@@ -40,94 +40,5 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
ValidateHttpResponse.Descriptor,
DisposeHttpResponse.Descriptor
]);
-
- ///
- /// Contains the logic responsible for attaching the client credentials to the HTTP Authorization header.
- ///
- public sealed class AttachBasicAuthenticationCredentials : IOpenIddictClientHandler
- {
- ///
- /// Gets the default descriptor definition assigned to this handler.
- ///
- public static OpenIddictClientHandlerDescriptor Descriptor { get; }
- = OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
- .UseSingletonHandler()
- .SetOrder(AttachHttpParameters.Descriptor.Order - 500)
- .SetType(OpenIddictClientHandlerType.BuiltIn)
- .Build();
-
- ///
- public ValueTask HandleAsync(PrepareIntrospectionRequestContext context)
- {
- if (context is null)
- {
- throw new ArgumentNullException(nameof(context));
- }
-
- Debug.Assert(context.Request is not null, SR.GetResourceString(SR.ID4008));
-
- // This handler only applies to System.Net.Http requests. If the HTTP request cannot be resolved,
- // this may indicate that the request was incorrectly processed by another client stack.
- var request = context.Transaction.GetHttpRequestMessage() ??
- throw new InvalidOperationException(SR.GetResourceString(SR.ID0173));
-
- // The OAuth 2.0 specification recommends sending the client credentials using basic authentication.
- // However, this authentication method is known to have severe compatibility/interoperability issues:
- //
- // - While restricted to clients that have been given a secret (i.e confidential clients) by the
- // specification, basic authentication is also sometimes required by server implementations for
- // public clients that don't have a client secret: in this case, an empty password is used and
- // the client identifier is sent alone in the Authorization header (instead of being sent using
- // the standard "client_id" parameter present in the request body).
- //
- // - While the OAuth 2.0 specification requires that the client credentials be formURL-encoded
- // before being base64-encoded, many implementations are known to implement a non-standard
- // encoding scheme, where neither the client_id nor the client_secret are formURL-encoded.
- //
- // To guarantee that the OpenIddict implementation can be used with most servers implementions,
- // basic authentication is only used when a client secret is present and client_secret_post is
- // always preferred when it's explicitly listed as a supported client authentication method.
- // If client_secret_post is not listed or if the server returned an empty methods list,
- // client_secret_basic is always used, as it MUST be implemented by all OAuth 2.0 servers.
- //
- // See https://tools.ietf.org/html/rfc8414#section-2
- // and https://tools.ietf.org/html/rfc6749#section-2.3.1 for more information.
- if (request.Headers.Authorization is null &&
- !string.IsNullOrEmpty(context.Request.ClientId) &&
- !string.IsNullOrEmpty(context.Request.ClientSecret) &&
- UseBasicAuthentication(context.Configuration))
- {
- // Important: the credentials MUST be formURL-encoded before being base64-encoded.
- var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes(new StringBuilder()
- .Append(EscapeDataString(context.Request.ClientId))
- .Append(':')
- .Append(EscapeDataString(context.Request.ClientSecret))
- .ToString()));
-
- // Attach the authorization header containing the client credentials to the HTTP request.
- request.Headers.Authorization = new AuthenticationHeaderValue(Schemes.Basic, credentials);
-
- // Remove the client credentials from the request payload to ensure they are not sent twice.
- context.Request.ClientId = context.Request.ClientSecret = null;
- }
-
- return default;
-
- static bool UseBasicAuthentication(OpenIddictConfiguration configuration)
- => configuration.IntrospectionEndpointAuthMethodsSupported switch
- {
- // If at least one authentication method was explicit added, only use basic authentication
- // if it's supported AND if client_secret_post is not supported or enabled by the server.
- { Count: > 0 } methods => methods.Contains(ClientAuthenticationMethods.ClientSecretBasic) &&
- !methods.Contains(ClientAuthenticationMethods.ClientSecretPost),
-
- // Otherwise, if no authentication method was explicit added, assume only basic is supported.
- { Count: _ } => true
- };
-
- static string EscapeDataString(string value) => Uri.EscapeDataString(value).Replace("%20", "+");
- }
- }
}
}
diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Revocation.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Revocation.cs
index d8fa2dc2..169543d2 100644
--- a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Revocation.cs
+++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Revocation.cs
@@ -26,7 +26,7 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
AttachJsonAcceptHeaders.Descriptor,
AttachUserAgentHeader.Descriptor,
AttachFromHeader.Descriptor,
- AttachBasicAuthenticationCredentials.Descriptor,
+ AttachBasicAuthenticationCredentials.Descriptor,
AttachHttpParameters.Descriptor,
SendHttpRequest.Descriptor,
DisposeHttpRequest.Descriptor,
@@ -41,94 +41,5 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
ValidateHttpResponse.Descriptor,
DisposeHttpResponse.Descriptor
]);
-
- ///
- /// Contains the logic responsible for attaching the client credentials to the HTTP Authorization header.
- ///
- public sealed class AttachBasicAuthenticationCredentials : IOpenIddictClientHandler
- {
- ///
- /// Gets the default descriptor definition assigned to this handler.
- ///
- public static OpenIddictClientHandlerDescriptor Descriptor { get; }
- = OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
- .UseSingletonHandler()
- .SetOrder(AttachHttpParameters.Descriptor.Order - 500)
- .SetType(OpenIddictClientHandlerType.BuiltIn)
- .Build();
-
- ///
- public ValueTask HandleAsync(PrepareRevocationRequestContext context)
- {
- if (context is null)
- {
- throw new ArgumentNullException(nameof(context));
- }
-
- Debug.Assert(context.Request is not null, SR.GetResourceString(SR.ID4008));
-
- // This handler only applies to System.Net.Http requests. If the HTTP request cannot be resolved,
- // this may indicate that the request was incorrectly processed by another client stack.
- var request = context.Transaction.GetHttpRequestMessage() ??
- throw new InvalidOperationException(SR.GetResourceString(SR.ID0173));
-
- // The OAuth 2.0 specification recommends sending the client credentials using basic authentication.
- // However, this authentication method is known to have severe compatibility/interoperability issues:
- //
- // - While restricted to clients that have been given a secret (i.e confidential clients) by the
- // specification, basic authentication is also sometimes required by server implementations for
- // public clients that don't have a client secret: in this case, an empty password is used and
- // the client identifier is sent alone in the Authorization header (instead of being sent using
- // the standard "client_id" parameter present in the request body).
- //
- // - While the OAuth 2.0 specification requires that the client credentials be formURL-encoded
- // before being base64-encoded, many implementations are known to implement a non-standard
- // encoding scheme, where neither the client_id nor the client_secret are formURL-encoded.
- //
- // To guarantee that the OpenIddict implementation can be used with most servers implementions,
- // basic authentication is only used when a client secret is present and client_secret_post is
- // always preferred when it's explicitly listed as a supported client authentication method.
- // If client_secret_post is not listed or if the server returned an empty methods list,
- // client_secret_basic is always used, as it MUST be implemented by all OAuth 2.0 servers.
- //
- // See https://tools.ietf.org/html/rfc8414#section-2
- // and https://tools.ietf.org/html/rfc6749#section-2.3.1 for more information.
- if (request.Headers.Authorization is null &&
- !string.IsNullOrEmpty(context.Request.ClientId) &&
- !string.IsNullOrEmpty(context.Request.ClientSecret) &&
- UseBasicAuthentication(context.Configuration))
- {
- // Important: the credentials MUST be formURL-encoded before being base64-encoded.
- var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes(new StringBuilder()
- .Append(EscapeDataString(context.Request.ClientId))
- .Append(':')
- .Append(EscapeDataString(context.Request.ClientSecret))
- .ToString()));
-
- // Attach the authorization header containing the client credentials to the HTTP request.
- request.Headers.Authorization = new AuthenticationHeaderValue(Schemes.Basic, credentials);
-
- // Remove the client credentials from the request payload to ensure they are not sent twice.
- context.Request.ClientId = context.Request.ClientSecret = null;
- }
-
- return default;
-
- static bool UseBasicAuthentication(OpenIddictConfiguration configuration)
- => configuration.RevocationEndpointAuthMethodsSupported switch
- {
- // If at least one authentication method was explicit added, only use basic authentication
- // if it's supported AND if client_secret_post is not supported or enabled by the server.
- { Count: > 0 } methods => methods.Contains(ClientAuthenticationMethods.ClientSecretBasic) &&
- !methods.Contains(ClientAuthenticationMethods.ClientSecretPost),
-
- // Otherwise, if no authentication method was explicit added, assume only basic is supported.
- { Count: _ } => true
- };
-
- static string EscapeDataString(string value) => Uri.EscapeDataString(value).Replace("%20", "+");
- }
- }
}
}
diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs
index a6e8e5bb..3625acaf 100644
--- a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs
+++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs
@@ -11,9 +11,12 @@ using System.IO.Compression;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Runtime.InteropServices;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
+using Microsoft.IdentityModel.Tokens;
using OpenIddict.Extensions;
using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpConstants;
@@ -23,6 +26,27 @@ namespace OpenIddict.Client.SystemNetHttp;
public static partial class OpenIddictClientSystemNetHttpHandlers
{
public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create([
+ /*
+ * Authentication processing:
+ */
+ AttachNonDefaultTokenEndpointClientAuthenticationMethod.Descriptor,
+ AttachNonDefaultUserInfoEndpointTokenBindingMethods.Descriptor,
+
+ /*
+ * Challenge processing:
+ */
+ AttachNonDefaultDeviceAuthorizationEndpointClientAuthenticationMethod.Descriptor,
+
+ /*
+ * Introspection processing:
+ */
+ AttachNonDefaultIntrospectionEndpointClientAuthenticationMethod.Descriptor,
+
+ /*
+ * Revocation processing:
+ */
+ AttachNonDefaultRevocationEndpointClientAuthenticationMethod.Descriptor,
+
.. Device.DefaultHandlers,
.. Discovery.DefaultHandlers,
.. Exchange.DefaultHandlers,
@@ -31,6 +55,559 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
.. UserInfo.DefaultHandlers
]);
+ ///
+ /// Contains the logic responsible for negotiating the best token endpoint client
+ /// authentication method supported by both the client and the authorization server.
+ ///
+ public sealed class AttachNonDefaultTokenEndpointClientAuthenticationMethod : IOpenIddictClientHandler
+ {
+ private readonly IOptionsMonitor _options;
+
+ public AttachNonDefaultTokenEndpointClientAuthenticationMethod(
+ IOptionsMonitor options)
+ => _options = options ?? throw new ArgumentNullException(nameof(options));
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(AttachTokenEndpointClientAuthenticationMethod.Descriptor.Order - 500)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(ProcessAuthenticationContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ // If an explicit client authentication method was attached, don't overwrite it.
+ if (!string.IsNullOrEmpty(context.TokenEndpointClientAuthenticationMethod))
+ {
+ return default;
+ }
+
+ context.TokenEndpointClientAuthenticationMethod = (
+ // Note: if client authentication methods are explicitly listed in the client registration, only use
+ // the client authentication methods that are both listed and enabled in the global client options.
+ // Otherwise, always default to the client authentication methods that have been enabled globally.
+ Client: context.Registration.ClientAuthenticationMethods.Count switch
+ {
+ 0 => context.Options.ClientAuthenticationMethods as ICollection,
+ _ => context.Options.ClientAuthenticationMethods.Intersect(context.Registration.ClientAuthenticationMethods, StringComparer.Ordinal).ToList()
+ },
+
+ Server: context.Configuration.TokenEndpointAuthMethodsSupported) switch
+ {
+ // If a TLS client authentication certificate could be resolved and both the
+ // client and the server explicitly support tls_client_auth, always prefer it.
+ ({ Count: > 0 } client, { Count: > 0 } server) when
+ client.Contains(ClientAuthenticationMethods.TlsClientAuth) &&
+ server.Contains(ClientAuthenticationMethods.TlsClientAuth) &&
+ (context.Configuration.MtlsTokenEndpoint ?? context.Configuration.TokenEndpoint) is Uri endpoint &&
+ string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) &&
+ _options.CurrentValue.TlsClientAuthenticationCertificateSelector(context.Registration) is not null
+ => ClientAuthenticationMethods.TlsClientAuth,
+
+ // If a self-signed TLS client authentication certificate could be resolved and both
+ // the client and the server explicitly support self_signed_tls_client_auth, use it.
+ ({ Count: > 0 } client, { Count: > 0 } server) when
+ client.Contains(ClientAuthenticationMethods.SelfSignedTlsClientAuth) &&
+ server.Contains(ClientAuthenticationMethods.SelfSignedTlsClientAuth) &&
+ (context.Configuration.MtlsTokenEndpoint ?? context.Configuration.TokenEndpoint) is Uri endpoint &&
+ string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) &&
+ _options.CurrentValue.SelfSignedTlsClientAuthenticationCertificateSelector(context.Registration) is not null
+ => ClientAuthenticationMethods.SelfSignedTlsClientAuth,
+
+ // If at least one asymmetric signing key was attached to the client registration
+ // and both the client and the server explicitly support private_key_jwt, use it.
+ ({ Count: > 0 } client, { Count: > 0 } server) when
+ client.Contains(ClientAuthenticationMethods.PrivateKeyJwt) &&
+ server.Contains(ClientAuthenticationMethods.PrivateKeyJwt) &&
+ context.Registration.SigningCredentials.Exists(static credentials => credentials.Key is AsymmetricSecurityKey)
+ => ClientAuthenticationMethods.PrivateKeyJwt,
+
+ // If a client secret was attached to the client registration and both the client and
+ // the server explicitly support client_secret_post, prefer it to basic authentication.
+ ({ Count: > 0 } client, { Count: > 0 } server) when !string.IsNullOrEmpty(context.Registration.ClientSecret) &&
+ client.Contains(ClientAuthenticationMethods.ClientSecretPost) &&
+ server.Contains(ClientAuthenticationMethods.ClientSecretPost)
+ => ClientAuthenticationMethods.ClientSecretPost,
+
+ // The OAuth 2.0 specification recommends sending the client credentials using basic authentication.
+ // However, this authentication method is known to have severe compatibility/interoperability issues:
+ //
+ // - While restricted to clients that have been given a secret (i.e confidential clients) by the
+ // specification, basic authentication is also sometimes required by server implementations for
+ // public clients that don't have a client secret: in this case, an empty password is used and
+ // the client identifier is sent alone in the Authorization header (instead of being sent using
+ // the standard "client_id" parameter present in the request body).
+ //
+ // - While the OAuth 2.0 specification requires that the client credentials be formURL-encoded
+ // before being base64-encoded, many implementations are known to implement a non-standard
+ // encoding scheme, where neither the client_id nor the client_secret are formURL-encoded.
+ //
+ // To guarantee that the OpenIddict implementation can be used with most servers implementions,
+ // basic authentication is only used when a client secret is present and the server configuration
+ // doesn't list any supported client authentication method or doesn't support client_secret_post.
+ //
+ // If client_secret_post is not listed or if the server returned an empty methods list,
+ // client_secret_basic is always used, as it MUST be implemented by all OAuth 2.0 servers.
+ //
+ // See https://tools.ietf.org/html/rfc8414#section-2
+ // and https://tools.ietf.org/html/rfc6749#section-2.3.1 for more information.
+ ({ Count: > 0 } client, { Count: > 0 } server) when !string.IsNullOrEmpty(context.Registration.ClientSecret) &&
+ client.Contains(ClientAuthenticationMethods.ClientSecretBasic) &&
+ server.Contains(ClientAuthenticationMethods.ClientSecretBasic)
+ => ClientAuthenticationMethods.ClientSecretBasic,
+
+ ({ Count: > 0 } client, { Count: 0 }) when !string.IsNullOrEmpty(context.Registration.ClientSecret) &&
+ client.Contains(ClientAuthenticationMethods.ClientSecretBasic)
+ => ClientAuthenticationMethods.ClientSecretBasic,
+
+ _ => null
+ };
+
+ return default;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for negotiating the best token binding
+ /// methods supported by both the client and the authorization server.
+ ///
+ public sealed class AttachNonDefaultUserInfoEndpointTokenBindingMethods : IOpenIddictClientHandler
+ {
+ private readonly IOptionsMonitor _options;
+
+ public AttachNonDefaultUserInfoEndpointTokenBindingMethods(
+ IOptionsMonitor options)
+ => _options = options ?? throw new ArgumentNullException(nameof(options));
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(AttachUserInfoEndpointTokenBindingMethods.Descriptor.Order - 500)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(ProcessAuthenticationContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ // Unlike DPoP, the mTLS specification doesn't use a specific token type to represent
+ // certificate-bound tokens. As such, most implementations (e.g Keycloak) simply return
+ // the "Bearer" value even if the access token is - by definition - not a bearer token
+ // and requires using the same X.509 certificate that was used for client authentication.
+ //
+ // Since the token type cannot be trusted in this case, OpenIddict assumes that the access
+ // token used in the userinfo request is certificate-bound if the server configuration
+ // indicates that the server supports certificate-bound access tokens and if either
+ // tls_client_auth or self_signed_tls_client_auth was used for the token request.
+
+ if (context.Configuration.TlsClientCertificateBoundAccessTokens is not true ||
+ !context.SendTokenRequest || string.IsNullOrEmpty(context.BackchannelAccessToken) ||
+ (context.Configuration.MtlsUserInfoEndpoint ?? context.Configuration.UserInfoEndpoint) is not Uri endpoint ||
+ !string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
+ {
+ return default;
+ }
+
+ if (context.TokenEndpointClientAuthenticationMethod is ClientAuthenticationMethods.TlsClientAuth &&
+ _options.CurrentValue.TlsClientAuthenticationCertificateSelector(context.Registration) is not null)
+ {
+ context.UserInfoEndpointTokenBindingMethods.Add(TokenBindingMethods.TlsClientCertificate);
+ }
+
+ else if (context.TokenEndpointClientAuthenticationMethod is ClientAuthenticationMethods.SelfSignedTlsClientAuth &&
+ _options.CurrentValue.SelfSignedTlsClientAuthenticationCertificateSelector(context.Registration) is not null)
+ {
+ context.UserInfoEndpointTokenBindingMethods.Add(TokenBindingMethods.SelfSignedTlsClientCertificate);
+ }
+
+ return default;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for negotiating the best device authorization endpoint
+ /// client authentication method supported by both the client and the authorization server.
+ ///
+ public sealed class AttachNonDefaultDeviceAuthorizationEndpointClientAuthenticationMethod : IOpenIddictClientHandler
+ {
+ private readonly IOptionsMonitor _options;
+
+ public AttachNonDefaultDeviceAuthorizationEndpointClientAuthenticationMethod(
+ IOptionsMonitor options)
+ => _options = options ?? throw new ArgumentNullException(nameof(options));
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(AttachDeviceAuthorizationEndpointClientAuthenticationMethod.Descriptor.Order - 500)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(ProcessChallengeContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ // If an explicit client authentication method was attached, don't overwrite it.
+ if (!string.IsNullOrEmpty(context.DeviceAuthorizationEndpointClientAuthenticationMethod))
+ {
+ return default;
+ }
+
+ context.DeviceAuthorizationEndpointClientAuthenticationMethod = (
+ // Note: if client authentication methods are explicitly listed in the client registration, only use
+ // the client authentication methods that are both listed and enabled in the global client options.
+ // Otherwise, always default to the client authentication methods that have been enabled globally.
+ Client: context.Registration.ClientAuthenticationMethods.Count switch
+ {
+ 0 => context.Options.ClientAuthenticationMethods as ICollection,
+ _ => context.Options.ClientAuthenticationMethods.Intersect(context.Registration.ClientAuthenticationMethods, StringComparer.Ordinal).ToList()
+ },
+
+ Server: context.Configuration.DeviceAuthorizationEndpointAuthMethodsSupported) switch
+ {
+ // If a TLS client authentication certificate could be resolved and both the
+ // client and the server explicitly support tls_client_auth, always prefer it.
+ ({ Count: > 0 } client, { Count: > 0 } server) when
+ client.Contains(ClientAuthenticationMethods.TlsClientAuth) &&
+ server.Contains(ClientAuthenticationMethods.TlsClientAuth) &&
+ (context.Configuration.MtlsDeviceAuthorizationEndpoint ?? context.Configuration.DeviceAuthorizationEndpoint) is Uri endpoint &&
+ string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) &&
+ _options.CurrentValue.TlsClientAuthenticationCertificateSelector(context.Registration) is not null
+ => ClientAuthenticationMethods.TlsClientAuth,
+
+ // If a self-signed TLS client authentication certificate could be resolved and both
+ // the client and the server explicitly support self_signed_tls_client_auth, use it.
+ ({ Count: > 0 } client, { Count: > 0 } server) when
+ client.Contains(ClientAuthenticationMethods.SelfSignedTlsClientAuth) &&
+ server.Contains(ClientAuthenticationMethods.SelfSignedTlsClientAuth) &&
+ (context.Configuration.MtlsDeviceAuthorizationEndpoint ?? context.Configuration.DeviceAuthorizationEndpoint) is Uri endpoint &&
+ string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) &&
+ _options.CurrentValue.SelfSignedTlsClientAuthenticationCertificateSelector(context.Registration) is not null
+ => ClientAuthenticationMethods.SelfSignedTlsClientAuth,
+
+ // If at least one asymmetric signing key was attached to the client registration
+ // and both the client and the server explicitly support private_key_jwt, use it.
+ ({ Count: > 0 } client, { Count: > 0 } server) when
+ client.Contains(ClientAuthenticationMethods.PrivateKeyJwt) &&
+ server.Contains(ClientAuthenticationMethods.PrivateKeyJwt) &&
+ context.Registration.SigningCredentials.Exists(static credentials => credentials.Key is AsymmetricSecurityKey)
+ => ClientAuthenticationMethods.PrivateKeyJwt,
+
+ // If a client secret was attached to the client registration and both the client and
+ // the server explicitly support client_secret_post, prefer it to basic authentication.
+ ({ Count: > 0 } client, { Count: > 0 } server) when !string.IsNullOrEmpty(context.Registration.ClientSecret) &&
+ client.Contains(ClientAuthenticationMethods.ClientSecretPost) &&
+ server.Contains(ClientAuthenticationMethods.ClientSecretPost)
+ => ClientAuthenticationMethods.ClientSecretPost,
+
+ // The OAuth 2.0 specification recommends sending the client credentials using basic authentication.
+ // However, this authentication method is known to have severe compatibility/interoperability issues:
+ //
+ // - While restricted to clients that have been given a secret (i.e confidential clients) by the
+ // specification, basic authentication is also sometimes required by server implementations for
+ // public clients that don't have a client secret: in this case, an empty password is used and
+ // the client identifier is sent alone in the Authorization header (instead of being sent using
+ // the standard "client_id" parameter present in the request body).
+ //
+ // - While the OAuth 2.0 specification requires that the client credentials be formURL-encoded
+ // before being base64-encoded, many implementations are known to implement a non-standard
+ // encoding scheme, where neither the client_id nor the client_secret are formURL-encoded.
+ //
+ // To guarantee that the OpenIddict implementation can be used with most servers implementions,
+ // basic authentication is only used when a client secret is present and the server configuration
+ // doesn't list any supported client authentication method or doesn't support client_secret_post.
+ //
+ // If client_secret_post is not listed or if the server returned an empty methods list,
+ // client_secret_basic is always used, as it MUST be implemented by all OAuth 2.0 servers.
+ //
+ // See https://tools.ietf.org/html/rfc8414#section-2
+ // and https://tools.ietf.org/html/rfc6749#section-2.3.1 for more information.
+ ({ Count: > 0 } client, { Count: > 0 } server) when !string.IsNullOrEmpty(context.Registration.ClientSecret) &&
+ client.Contains(ClientAuthenticationMethods.ClientSecretBasic) &&
+ server.Contains(ClientAuthenticationMethods.ClientSecretBasic)
+ => ClientAuthenticationMethods.ClientSecretBasic,
+
+ ({ Count: > 0 } client, { Count: 0 }) when !string.IsNullOrEmpty(context.Registration.ClientSecret) &&
+ client.Contains(ClientAuthenticationMethods.ClientSecretBasic)
+ => ClientAuthenticationMethods.ClientSecretBasic,
+
+ _ => null
+ };
+
+ return default;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for negotiating the best introspection endpoint client
+ /// authentication method supported by both the client and the authorization server.
+ ///
+ public sealed class AttachNonDefaultIntrospectionEndpointClientAuthenticationMethod : IOpenIddictClientHandler
+ {
+ private readonly IOptionsMonitor _options;
+
+ public AttachNonDefaultIntrospectionEndpointClientAuthenticationMethod(
+ IOptionsMonitor options)
+ => _options = options ?? throw new ArgumentNullException(nameof(options));
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(AttachIntrospectionEndpointClientAuthenticationMethod.Descriptor.Order - 500)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(ProcessIntrospectionContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ // If an explicit client authentication method was attached, don't overwrite it.
+ if (!string.IsNullOrEmpty(context.IntrospectionEndpointClientAuthenticationMethod))
+ {
+ return default;
+ }
+
+ context.IntrospectionEndpointClientAuthenticationMethod = (
+ // Note: if client authentication methods are explicitly listed in the client registration, only use
+ // the client authentication methods that are both listed and enabled in the global client options.
+ // Otherwise, always default to the client authentication methods that have been enabled globally.
+ Client: context.Registration.ClientAuthenticationMethods.Count switch
+ {
+ 0 => context.Options.ClientAuthenticationMethods as ICollection,
+ _ => context.Options.ClientAuthenticationMethods.Intersect(context.Registration.ClientAuthenticationMethods, StringComparer.Ordinal).ToList()
+ },
+
+ Server: context.Configuration.IntrospectionEndpointAuthMethodsSupported) switch
+ {
+ // If a TLS client authentication certificate could be resolved and both the
+ // client and the server explicitly support tls_client_auth, always prefer it.
+ ({ Count: > 0 } client, { Count: > 0 } server) when
+ client.Contains(ClientAuthenticationMethods.TlsClientAuth) &&
+ server.Contains(ClientAuthenticationMethods.TlsClientAuth) &&
+ (context.Configuration.MtlsIntrospectionEndpoint ?? context.Configuration.IntrospectionEndpoint) is Uri endpoint &&
+ string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) &&
+ _options.CurrentValue.TlsClientAuthenticationCertificateSelector(context.Registration) is not null
+ => ClientAuthenticationMethods.TlsClientAuth,
+
+ // If a self-signed TLS client authentication certificate could be resolved and both
+ // the client and the server explicitly support self_signed_tls_client_auth, use it.
+ ({ Count: > 0 } client, { Count: > 0 } server) when
+ client.Contains(ClientAuthenticationMethods.SelfSignedTlsClientAuth) &&
+ server.Contains(ClientAuthenticationMethods.SelfSignedTlsClientAuth) &&
+ (context.Configuration.MtlsIntrospectionEndpoint ?? context.Configuration.IntrospectionEndpoint) is Uri endpoint &&
+ string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) &&
+ _options.CurrentValue.SelfSignedTlsClientAuthenticationCertificateSelector(context.Registration) is not null
+ => ClientAuthenticationMethods.SelfSignedTlsClientAuth,
+
+ // If at least one asymmetric signing key was attached to the client registration
+ // and both the client and the server explicitly support private_key_jwt, use it.
+ ({ Count: > 0 } client, { Count: > 0 } server) when
+ client.Contains(ClientAuthenticationMethods.PrivateKeyJwt) &&
+ server.Contains(ClientAuthenticationMethods.PrivateKeyJwt) &&
+ context.Registration.SigningCredentials.Exists(static credentials => credentials.Key is AsymmetricSecurityKey)
+ => ClientAuthenticationMethods.PrivateKeyJwt,
+
+ // If a client secret was attached to the client registration and both the client and
+ // the server explicitly support client_secret_post, prefer it to basic authentication.
+ ({ Count: > 0 } client, { Count: > 0 } server) when !string.IsNullOrEmpty(context.Registration.ClientSecret) &&
+ client.Contains(ClientAuthenticationMethods.ClientSecretPost) &&
+ server.Contains(ClientAuthenticationMethods.ClientSecretPost)
+ => ClientAuthenticationMethods.ClientSecretPost,
+
+ // The OAuth 2.0 specification recommends sending the client credentials using basic authentication.
+ // However, this authentication method is known to have severe compatibility/interoperability issues:
+ //
+ // - While restricted to clients that have been given a secret (i.e confidential clients) by the
+ // specification, basic authentication is also sometimes required by server implementations for
+ // public clients that don't have a client secret: in this case, an empty password is used and
+ // the client identifier is sent alone in the Authorization header (instead of being sent using
+ // the standard "client_id" parameter present in the request body).
+ //
+ // - While the OAuth 2.0 specification requires that the client credentials be formURL-encoded
+ // before being base64-encoded, many implementations are known to implement a non-standard
+ // encoding scheme, where neither the client_id nor the client_secret are formURL-encoded.
+ //
+ // To guarantee that the OpenIddict implementation can be used with most servers implementions,
+ // basic authentication is only used when a client secret is present and the server configuration
+ // doesn't list any supported client authentication method or doesn't support client_secret_post.
+ //
+ // If client_secret_post is not listed or if the server returned an empty methods list,
+ // client_secret_basic is always used, as it MUST be implemented by all OAuth 2.0 servers.
+ //
+ // See https://tools.ietf.org/html/rfc8414#section-2
+ // and https://tools.ietf.org/html/rfc6749#section-2.3.1 for more information.
+ ({ Count: > 0 } client, { Count: > 0 } server) when !string.IsNullOrEmpty(context.Registration.ClientSecret) &&
+ client.Contains(ClientAuthenticationMethods.ClientSecretBasic) &&
+ server.Contains(ClientAuthenticationMethods.ClientSecretBasic)
+ => ClientAuthenticationMethods.ClientSecretBasic,
+
+ ({ Count: > 0 } client, { Count: 0 }) when !string.IsNullOrEmpty(context.Registration.ClientSecret) &&
+ client.Contains(ClientAuthenticationMethods.ClientSecretBasic)
+ => ClientAuthenticationMethods.ClientSecretBasic,
+
+ _ => null
+ };
+
+ return default;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for negotiating the best revocation endpoint client
+ /// authentication method supported by both the client and the authorization server.
+ ///
+ public sealed class AttachNonDefaultRevocationEndpointClientAuthenticationMethod : IOpenIddictClientHandler
+ {
+ private readonly IOptionsMonitor _options;
+
+ public AttachNonDefaultRevocationEndpointClientAuthenticationMethod(
+ IOptionsMonitor options)
+ => _options = options ?? throw new ArgumentNullException(nameof(options));
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(AttachRevocationEndpointClientAuthenticationMethod.Descriptor.Order - 500)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(ProcessRevocationContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ // If an explicit client authentication method was attached, don't overwrite it.
+ if (!string.IsNullOrEmpty(context.RevocationEndpointClientAuthenticationMethod))
+ {
+ return default;
+ }
+
+ context.RevocationEndpointClientAuthenticationMethod = (
+ // Note: if client authentication methods are explicitly listed in the client registration, only use
+ // the client authentication methods that are both listed and enabled in the global client options.
+ // Otherwise, always default to the client authentication methods that have been enabled globally.
+ Client: context.Registration.ClientAuthenticationMethods.Count switch
+ {
+ 0 => context.Options.ClientAuthenticationMethods as ICollection,
+ _ => context.Options.ClientAuthenticationMethods.Intersect(context.Registration.ClientAuthenticationMethods, StringComparer.Ordinal).ToList()
+ },
+
+ Server: context.Configuration.RevocationEndpointAuthMethodsSupported) switch
+ {
+ // If a TLS client authentication certificate could be resolved and both the
+ // client and the server explicitly support tls_client_auth, always prefer it.
+ ({ Count: > 0 } client, { Count: > 0 } server) when
+ client.Contains(ClientAuthenticationMethods.TlsClientAuth) &&
+ server.Contains(ClientAuthenticationMethods.TlsClientAuth) &&
+ (context.Configuration.MtlsRevocationEndpoint ?? context.Configuration.RevocationEndpoint) is Uri endpoint &&
+ string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) &&
+ _options.CurrentValue.TlsClientAuthenticationCertificateSelector(context.Registration) is not null
+ => ClientAuthenticationMethods.TlsClientAuth,
+
+ // If a self-signed TLS client authentication certificate could be resolved and both
+ // the client and the server explicitly support self_signed_tls_client_auth, use it.
+ ({ Count: > 0 } client, { Count: > 0 } server) when
+ client.Contains(ClientAuthenticationMethods.SelfSignedTlsClientAuth) &&
+ server.Contains(ClientAuthenticationMethods.SelfSignedTlsClientAuth) &&
+ (context.Configuration.MtlsRevocationEndpoint ?? context.Configuration.RevocationEndpoint) is Uri endpoint &&
+ string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) &&
+ _options.CurrentValue.SelfSignedTlsClientAuthenticationCertificateSelector(context.Registration) is not null
+ => ClientAuthenticationMethods.SelfSignedTlsClientAuth,
+
+ // If at least one asymmetric signing key was attached to the client registration
+ // and both the client and the server explicitly support private_key_jwt, use it.
+ ({ Count: > 0 } client, { Count: > 0 } server) when
+ client.Contains(ClientAuthenticationMethods.PrivateKeyJwt) &&
+ server.Contains(ClientAuthenticationMethods.PrivateKeyJwt) &&
+ context.Registration.SigningCredentials.Exists(static credentials => credentials.Key is AsymmetricSecurityKey)
+ => ClientAuthenticationMethods.PrivateKeyJwt,
+
+ // If a client secret was attached to the client registration and both the client and
+ // the server explicitly support client_secret_post, prefer it to basic authentication.
+ ({ Count: > 0 } client, { Count: > 0 } server) when !string.IsNullOrEmpty(context.Registration.ClientSecret) &&
+ client.Contains(ClientAuthenticationMethods.ClientSecretPost) &&
+ server.Contains(ClientAuthenticationMethods.ClientSecretPost)
+ => ClientAuthenticationMethods.ClientSecretPost,
+
+ // The OAuth 2.0 specification recommends sending the client credentials using basic authentication.
+ // However, this authentication method is known to have severe compatibility/interoperability issues:
+ //
+ // - While restricted to clients that have been given a secret (i.e confidential clients) by the
+ // specification, basic authentication is also sometimes required by server implementations for
+ // public clients that don't have a client secret: in this case, an empty password is used and
+ // the client identifier is sent alone in the Authorization header (instead of being sent using
+ // the standard "client_id" parameter present in the request body).
+ //
+ // - While the OAuth 2.0 specification requires that the client credentials be formURL-encoded
+ // before being base64-encoded, many implementations are known to implement a non-standard
+ // encoding scheme, where neither the client_id nor the client_secret are formURL-encoded.
+ //
+ // To guarantee that the OpenIddict implementation can be used with most servers implementions,
+ // basic authentication is only used when a client secret is present and the server configuration
+ // doesn't list any supported client authentication method or doesn't support client_secret_post.
+ //
+ // If client_secret_post is not listed or if the server returned an empty methods list,
+ // client_secret_basic is always used, as it MUST be implemented by all OAuth 2.0 servers.
+ //
+ // See https://tools.ietf.org/html/rfc8414#section-2
+ // and https://tools.ietf.org/html/rfc6749#section-2.3.1 for more information.
+ ({ Count: > 0 } client, { Count: > 0 } server) when !string.IsNullOrEmpty(context.Registration.ClientSecret) &&
+ client.Contains(ClientAuthenticationMethods.ClientSecretBasic) &&
+ server.Contains(ClientAuthenticationMethods.ClientSecretBasic)
+ => ClientAuthenticationMethods.ClientSecretBasic,
+
+ ({ Count: > 0 } client, { Count: 0 }) when !string.IsNullOrEmpty(context.Registration.ClientSecret) &&
+ client.Contains(ClientAuthenticationMethods.ClientSecretBasic)
+ => ClientAuthenticationMethods.ClientSecretBasic,
+
+ _ => null
+ };
+
+ return default;
+ }
+ }
+
///
/// Contains the logic responsible for creating and attaching a .
///
@@ -60,11 +637,60 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
throw new ArgumentNullException(nameof(context));
}
- var assembly = typeof(OpenIddictClientSystemNetHttpOptions).Assembly.GetName();
- var name = $"{assembly.Name}:{context.Registration.RegistrationId}";
+ // Note: HttpClientFactory doesn't support flowing a list of properties that can be
+ // accessed from the HttpClientAction or HttpMessageHandlerBuilderAction delegates
+ // to dynamically amend the resulting HttpClient or HttpClientHandler instance.
+ //
+ // To work around this limitation, the OpenIddict System.Net.Http integration
+ // uses dynamic client names and supports appending a list of key-value pairs
+ // to the client name to flow per-instance properties.
+
+ var builder = new StringBuilder();
+
+ // Always prefix the HTTP client name with the assembly name of the System.Net.Http package.
+ builder.Append(typeof(OpenIddictClientSystemNetHttpOptions).Assembly.GetName().Name);
+
+ builder.Append(':');
+
+ // Attach the registration identifier.
+ builder.Append("RegistrationId")
+ .Append('\u001e')
+ .Append(context.Registration.RegistrationId);
+
+ // If both a client authentication method and one or multiple token binding methods were negotiated,
+ // make sure they are compatible (e.g that they all use a CA-issued or self-signed X.509 certificate).
+ if ((context.ClientAuthenticationMethod is ClientAuthenticationMethods.TlsClientAuth &&
+ context.TokenBindingMethods.Contains(TokenBindingMethods.SelfSignedTlsClientCertificate)) ||
+ (context.ClientAuthenticationMethod is ClientAuthenticationMethods.SelfSignedTlsClientAuth &&
+ context.TokenBindingMethods.Contains(TokenBindingMethods.TlsClientCertificate)))
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0456));
+ }
+
+ // Attach a flag indicating that a client certificate should be used in the TLS handshake.
+ if (context.ClientAuthenticationMethod is ClientAuthenticationMethods.TlsClientAuth ||
+ context.TokenBindingMethods.Contains(TokenBindingMethods.TlsClientCertificate))
+ {
+ builder.Append('\u001f');
+
+ builder.Append("AttachTlsClientCertificate")
+ .Append('\u001e')
+ .Append(bool.TrueString);
+ }
+
+ // Attach a flag indicating that a self-signed client certificate should be used in the TLS handshake.
+ else if (context.ClientAuthenticationMethod is ClientAuthenticationMethods.SelfSignedTlsClientAuth ||
+ context.TokenBindingMethods.Contains(TokenBindingMethods.SelfSignedTlsClientCertificate))
+ {
+ builder.Append('\u001f');
+
+ builder.Append("AttachSelfSignedTlsClientCertificate")
+ .Append('\u001e')
+ .Append(bool.TrueString);
+ }
// Create and store the HttpClient in the transaction properties.
- context.Transaction.SetProperty(typeof(HttpClient).FullName!, _factory.CreateClient(name) ??
+ context.Transaction.SetProperty(typeof(HttpClient).FullName!, _factory.CreateClient(builder.ToString()) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0174)));
return default;
@@ -318,6 +944,63 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
}
}
+ ///
+ /// Contains the logic responsible for attaching the client credentials to the HTTP Authorization header.
+ ///
+ public sealed class AttachBasicAuthenticationCredentials : IOpenIddictClientHandler where TContext : BaseExternalContext
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler>()
+ .SetOrder(AttachHttpParameters.Descriptor.Order - 500)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(TContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ Debug.Assert(context.Transaction.Request is not null, SR.GetResourceString(SR.ID4008));
+
+ // This handler only applies to System.Net.Http requests. If the HTTP request cannot be resolved,
+ // this may indicate that the request was incorrectly processed by another client stack.
+ var request = context.Transaction.GetHttpRequestMessage() ??
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0173));
+
+ // Note: don't overwrite the authorization header if one was already set by another handler.
+ if (request.Headers.Authorization is null &&
+ context.ClientAuthenticationMethod is ClientAuthenticationMethods.ClientSecretBasic &&
+ !string.IsNullOrEmpty(context.Transaction.Request.ClientId))
+ {
+ // Important: the credentials MUST be formURL-encoded before being base64-encoded.
+ var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes(new StringBuilder()
+ .Append(EscapeDataString(context.Transaction.Request.ClientId))
+ .Append(':')
+ .Append(EscapeDataString(context.Transaction.Request.ClientSecret))
+ .ToString()));
+
+ // Attach the authorization header containing the client credentials to the HTTP request.
+ request.Headers.Authorization = new AuthenticationHeaderValue(Schemes.Basic, credentials);
+
+ // Remove the client credentials from the request payload to ensure they are not sent twice.
+ context.Transaction.Request.ClientId = context.Transaction.Request.ClientSecret = null;
+ }
+
+ return default;
+
+ static string? EscapeDataString(string? value)
+ => value is not null ? Uri.EscapeDataString(value).Replace("%20", "+") : null;
+ }
+ }
+
///
/// Contains the logic responsible for attaching the parameters to the HTTP request.
///
diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpOptions.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpOptions.cs
index 4547a9b8..dce47330 100644
--- a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpOptions.cs
+++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpOptions.cs
@@ -4,10 +4,12 @@
* the license and the contributors participating to this project.
*/
+using System.ComponentModel;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Mail;
+using System.Security.Cryptography.X509Certificates;
using Polly;
using Polly.Extensions.Http;
@@ -80,4 +82,32 @@ public sealed class OpenIddictClientSystemNetHttpOptions
/// instances created by the OpenIddict client/System.Net.Http integration.
///
public List> HttpClientHandlerActions { get; } = [];
+
+ ///
+ /// Gets or sets the delegate called by OpenIddict when trying to resolve the
+ /// self-signed TLS client authentication certificate that will be used for OAuth 2.0
+ /// mTLS-based client authentication (self_signed_tls_client_auth), if applicable.
+ ///
+ ///
+ /// If no value is explicitly set, OpenIddict automatically tries to resolve the
+ /// X.509 certificate from the signing credentials attached to the client registration
+ /// (in this case, the X.509 certificate MUST include the digital signature and
+ /// client authentication key usages to be automatically selected by OpenIddict).
+ ///
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public Func SelfSignedTlsClientAuthenticationCertificateSelector { get; set; } = default!;
+
+ ///
+ /// Gets or sets the delegate called by OpenIddict when trying to resolve the TLS
+ /// client authentication certificate that will be used for OAuth 2.0 mTLS-based
+ /// client authentication (tls_client_auth), if applicable.
+ ///
+ ///
+ /// If no value is explicitly set, OpenIddict automatically tries to resolve the
+ /// X.509 certificate from the signing credentials attached to the client registration
+ /// (in this case, the X.509 certificate MUST include the digital signature and
+ /// client authentication key usages to be automatically selected by OpenIddict).
+ ///
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public Func TlsClientAuthenticationCertificateSelector { get; set; } = default!;
}
diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationConfiguration.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationConfiguration.cs
index 25b9e2d7..85d5b74e 100644
--- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationConfiguration.cs
+++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationConfiguration.cs
@@ -5,10 +5,7 @@
*/
using System.ComponentModel;
-using System.Net.Http;
using Microsoft.Extensions.Options;
-using OpenIddict.Client.SystemNetHttp;
-using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants;
namespace OpenIddict.Client.WebIntegration;
@@ -17,7 +14,6 @@ namespace OpenIddict.Client.WebIntegration;
///
[EditorBrowsable(EditorBrowsableState.Advanced)]
public sealed partial class OpenIddictClientWebIntegrationConfiguration : IConfigureOptions,
- IConfigureOptions,
IPostConfigureOptions
{
///
@@ -32,36 +28,6 @@ public sealed partial class OpenIddictClientWebIntegrationConfiguration : IConfi
options.Handlers.AddRange(OpenIddictClientWebIntegrationHandlers.DefaultHandlers);
}
- ///
- public void Configure(OpenIddictClientSystemNetHttpOptions options)
- {
- if (options is null)
- {
- throw new ArgumentNullException(nameof(options));
- }
-
- options.HttpClientHandlerActions.Add(static (registration, handler) =>
- {
- var certificate = registration.ProviderType switch
- {
- // Note: while not enforced yet, Pro Santé Connect's specification requires sending a TLS
- // client certificate when communicating with its backchannel OpenID Connect endpoints.
- //
- // For more information, see EXI PSC 24 in the annex part of
- // https://www.legifrance.gouv.fr/jorf/id/JORFTEXT000045551195.
- ProviderTypes.ProSantéConnect => registration.GetProSantéConnectSettings().ClientCertificate,
-
- _ => null
- };
-
- if (certificate is not null)
- {
- handler.ClientCertificateOptions = ClientCertificateOption.Manual;
- handler.ClientCertificates.Add(certificate);
- }
- });
- }
-
///
public void PostConfigure(string? name, OpenIddictClientOptions options)
{
diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationExtensions.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationExtensions.cs
index b062de02..3b78e5d0 100644
--- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationExtensions.cs
+++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationExtensions.cs
@@ -7,7 +7,6 @@
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using OpenIddict.Client;
-using OpenIddict.Client.SystemNetHttp;
using OpenIddict.Client.WebIntegration;
namespace Microsoft.Extensions.DependencyInjection;
@@ -42,9 +41,6 @@ public static partial class OpenIddictClientWebIntegrationExtensions
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
IConfigureOptions, OpenIddictClientWebIntegrationConfiguration>());
- builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
- IConfigureOptions, OpenIddictClientWebIntegrationConfiguration>());
-
// Note: the IPostConfigureOptions service responsible for populating
// the client registrations MUST be registered before OpenIddictClientConfiguration to ensure
// the registrations are correctly populated before being validated.
diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Discovery.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Discovery.cs
index 83ef00a5..c7aab55a 100644
--- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Discovery.cs
+++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Discovery.cs
@@ -338,6 +338,24 @@ public static partial class OpenIddictClientWebIntegrationHandlers
ClientAuthenticationMethods.ClientSecretPost);
}
+ // Pro Santé Connect lists private_key_jwt as a supported client authentication method but
+ // only supports client_secret_basic/client_secret_post and tls_client_auth and plans to
+ // remove secret-based authentication support in late 2024 to force clients to use mTLS.
+ else if (context.Registration.ProviderType is ProviderTypes.ProSantéConnect)
+ {
+ context.Configuration.DeviceAuthorizationEndpointAuthMethodsSupported.Remove(
+ ClientAuthenticationMethods.PrivateKeyJwt);
+
+ context.Configuration.IntrospectionEndpointAuthMethodsSupported.Remove(
+ ClientAuthenticationMethods.PrivateKeyJwt);
+
+ context.Configuration.RevocationEndpointAuthMethodsSupported.Remove(
+ ClientAuthenticationMethods.PrivateKeyJwt);
+
+ context.Configuration.TokenEndpointAuthMethodsSupported.Remove(
+ ClientAuthenticationMethods.PrivateKeyJwt);
+ }
+
return default;
}
}
diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Exchange.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Exchange.cs
index 73e36441..0e2dcab1 100644
--- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Exchange.cs
+++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Exchange.cs
@@ -16,7 +16,6 @@ using OpenIddict.Extensions;
using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpConstants;
using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpHandlerFilters;
using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpHandlers;
-using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpHandlers.Exchange;
using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants;
namespace OpenIddict.Client.WebIntegration;
@@ -131,7 +130,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
= OpenIddictClientHandlerDescriptor.CreateBuilder()
.AddFilter()
.UseSingletonHandler()
- .SetOrder(AttachBasicAuthenticationCredentials.Descriptor.Order - 500)
+ .SetOrder(AttachBasicAuthenticationCredentials.Descriptor.Order - 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@@ -155,49 +154,10 @@ public static partial class OpenIddictClientWebIntegrationHandlers
var request = context.Transaction.GetHttpRequestMessage() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0173));
- // Note: Bitly only supports using "client_secret_post" for the authorization code grant but not for
- // the resource owner password credentials grant, that requires using "client_secret_basic" instead.
- if (context.Registration.ProviderType is ProviderTypes.Bitly &&
- context.GrantType is GrantTypes.Password &&
- !string.IsNullOrEmpty(context.Request.ClientId) &&
- !string.IsNullOrEmpty(context.Request.ClientSecret))
- {
- // Important: the credentials MUST be formURL-encoded before being base64-encoded.
- var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes(new StringBuilder()
- .Append(EscapeDataString(context.Request.ClientId))
- .Append(':')
- .Append(EscapeDataString(context.Request.ClientSecret))
- .ToString()));
-
- // Attach the authorization header containing the client credentials to the HTTP request.
- request.Headers.Authorization = new AuthenticationHeaderValue(Schemes.Basic, credentials);
-
- // Remove the client credentials from the request payload to ensure they are not sent twice.
- context.Request.ClientId = context.Request.ClientSecret = null;
- }
-
- // These providers require using basic authentication to flow the client_id
- // for all types of client applications, even when there's no client_secret.
- if (context.Registration.ProviderType is ProviderTypes.Reddit &&
- !string.IsNullOrEmpty(context.Request.ClientId))
- {
- // Important: the credentials MUST be formURL-encoded before being base64-encoded.
- var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes(new StringBuilder()
- .Append(EscapeDataString(context.Request.ClientId))
- .Append(':')
- .Append(EscapeDataString(context.Request.ClientSecret))
- .ToString()));
-
- // Attach the authorization header containing the client identifier to the HTTP request.
- request.Headers.Authorization = new AuthenticationHeaderValue(Schemes.Basic, credentials);
-
- // Remove the client credentials from the request payload to ensure they are not sent twice.
- context.Request.ClientId = context.Request.ClientSecret = null;
- }
-
// These providers don't implement the standard version of the client_secret_basic
// authentication method as they don't support formURL-encoding the client credentials.
- else if (context.Registration.ProviderType is ProviderTypes.EpicGames &&
+ if (context.Registration.ProviderType is ProviderTypes.EpicGames &&
+ context.ClientAuthenticationMethod is ClientAuthenticationMethods.ClientSecretBasic &&
!string.IsNullOrEmpty(context.Request.ClientId) &&
!string.IsNullOrEmpty(context.Request.ClientSecret))
{
@@ -215,9 +175,6 @@ public static partial class OpenIddictClientWebIntegrationHandlers
}
return default;
-
- static string? EscapeDataString(string? value)
- => value is not null ? Uri.EscapeDataString(value).Replace("%20", "+") : null;
}
}
diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Revocation.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Revocation.cs
index 3d22205f..d3c614fb 100644
--- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Revocation.cs
+++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Revocation.cs
@@ -5,13 +5,9 @@
*/
using System.Collections.Immutable;
-using System.Diagnostics;
using System.Net.Http;
-using System.Net.Http.Headers;
-using System.Text;
using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpHandlerFilters;
using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpHandlers;
-using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpHandlers.Exchange;
using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants;
namespace OpenIddict.Client.WebIntegration;
@@ -21,80 +17,12 @@ public static partial class OpenIddictClientWebIntegrationHandlers
public static class Revocation
{
public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create([
- /*
- * Revocation request preparation:
- */
- AttachNonStandardBasicAuthenticationCredentials.Descriptor,
-
/*
* Revocation response extraction:
*/
NormalizeContentType.Descriptor
]);
- ///
- /// Contains the logic responsible for attaching the client credentials to the HTTP Authorization
- /// header using a non-standard construction logic for the providers that require it.
- ///
- public sealed class AttachNonStandardBasicAuthenticationCredentials : IOpenIddictClientHandler
- {
- ///
- /// Gets the default descriptor definition assigned to this handler.
- ///
- public static OpenIddictClientHandlerDescriptor Descriptor { get; }
- = OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
- .UseSingletonHandler()
- .SetOrder(AttachBasicAuthenticationCredentials.Descriptor.Order - 500)
- .SetType(OpenIddictClientHandlerType.BuiltIn)
- .Build();
-
- ///
- public ValueTask HandleAsync(PrepareRevocationRequestContext context)
- {
- if (context is null)
- {
- throw new ArgumentNullException(nameof(context));
- }
-
- // Some providers are known to incorrectly implement basic authentication support, either because
- // an incorrect encoding scheme is used (e.g the credentials are not formURL-encoded as required
- // by the OAuth 2.0 specification) or because basic authentication is required even for public
- // clients, even though these clients don't have a secret (which requires using an empty password).
-
- Debug.Assert(context.Request is not null, SR.GetResourceString(SR.ID4008));
-
- // This handler only applies to System.Net.Http requests. If the HTTP request cannot be resolved,
- // this may indicate that the request was incorrectly processed by another client stack.
- var request = context.Transaction.GetHttpRequestMessage() ??
- throw new InvalidOperationException(SR.GetResourceString(SR.ID0173));
-
- // These providers require using basic authentication to flow the client_id
- // for all types of client applications, even when there's no client_secret.
- if (context.Registration.ProviderType is ProviderTypes.Reddit &&
- !string.IsNullOrEmpty(context.Request.ClientId))
- {
- // Important: the credentials MUST be formURL-encoded before being base64-encoded.
- var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes(new StringBuilder()
- .Append(EscapeDataString(context.Request.ClientId))
- .Append(':')
- .Append(EscapeDataString(context.Request.ClientSecret))
- .ToString()));
-
- // Attach the authorization header containing the client identifier to the HTTP request.
- request.Headers.Authorization = new AuthenticationHeaderValue(Schemes.Basic, credentials);
-
- // Remove the client credentials from the request payload to ensure they are not sent twice.
- context.Request.ClientId = context.Request.ClientSecret = null;
- }
-
- return default;
-
- static string? EscapeDataString(string? value)
- => value is not null ? Uri.EscapeDataString(value).Replace("%20", "+") : null;
- }
- }
-
///
/// Contains the logic responsible for normalizing the returned content
/// type of revocation responses for the providers that require it.
diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs
index e306ac55..d040b712 100644
--- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs
+++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs
@@ -25,6 +25,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
ValidateRedirectionRequestSignature.Descriptor,
HandleNonStandardFrontchannelErrorResponse.Descriptor,
ValidateNonStandardParameters.Descriptor,
+ OverrideTokenEndpointClientAuthenticationMethod.Descriptor,
OverrideTokenEndpoint.Descriptor,
AttachNonStandardClientAssertionClaims.Descriptor,
AttachAdditionalTokenRequestParameters.Descriptor,
@@ -32,9 +33,9 @@ public static partial class OpenIddictClientWebIntegrationHandlers
AdjustRedirectUriInTokenRequest.Descriptor,
OverrideValidatedBackchannelTokens.Descriptor,
DisableBackchannelIdentityTokenNonceValidation.Descriptor,
+ OverrideUserInfoRetrieval.Descriptor,
+ OverrideUserInfoValidation.Descriptor,
OverrideUserInfoEndpoint.Descriptor,
- DisableUserInfoRetrieval.Descriptor,
- DisableUserInfoValidation.Descriptor,
AttachAdditionalUserInfoRequestParameters.Descriptor,
PopulateUserInfoTokenPrincipalFromTokenResponse.Descriptor,
MapCustomWebServicesFederationClaims.Descriptor,
@@ -52,6 +53,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
/*
* Revocation processing:
*/
+ OverrideRevocationEndpointClientAuthenticationMethod.Descriptor,
AttachNonStandardRevocationClientAssertionClaims.Descriptor,
AttachRevocationRequestNonStandardClientCredentials.Descriptor,
@@ -424,6 +426,50 @@ public static partial class OpenIddictClientWebIntegrationHandlers
}
}
+ ///
+ /// Contains the logic responsible for overriding the negotiated token
+ /// endpoint client authentication method for the providers that require it.
+ ///
+ public sealed class OverrideTokenEndpointClientAuthenticationMethod : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(AttachTokenEndpointClientAuthenticationMethod.Descriptor.Order + 500)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(ProcessAuthenticationContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ context.TokenEndpointClientAuthenticationMethod = context.Registration.ProviderType switch
+ {
+ // Note: Bitly only supports using "client_secret_post" for the authorization code
+ // grant but not for the resource owner password credentials grant, that requires
+ // using the "client_secret_basic" method instead.
+ ProviderTypes.Bitly when context.GrantType is GrantTypes.Password
+ => ClientAuthenticationMethods.ClientSecretBasic,
+
+ // Note: Reddit requires using basic authentication to flow the client_id for all types
+ // of client applications, even when there's no client_secret assigned or attached.
+ ProviderTypes.Reddit => ClientAuthenticationMethods.ClientSecretBasic,
+
+ _ => context.TokenEndpointClientAuthenticationMethod
+ };
+
+ return default;
+ }
+ }
+
///
/// Contains the logic responsible for overriding the address
/// of the token endpoint for the providers that require it.
@@ -794,102 +840,16 @@ public static partial class OpenIddictClientWebIntegrationHandlers
}
///
- /// Contains the logic responsible for overriding the address
- /// of the userinfo endpoint for the providers that require it.
- ///
- public sealed class OverrideUserInfoEndpoint : IOpenIddictClientHandler
- {
- ///
- /// Gets the default descriptor definition assigned to this handler.
- ///
- public static OpenIddictClientHandlerDescriptor Descriptor { get; }
- = OpenIddictClientHandlerDescriptor.CreateBuilder()
- .UseSingletonHandler()
- .SetOrder(ResolveUserInfoEndpoint.Descriptor.Order + 500)
- .SetType(OpenIddictClientHandlerType.BuiltIn)
- .Build();
-
- ///
- public ValueTask HandleAsync(ProcessAuthenticationContext context)
- {
- if (context is null)
- {
- throw new ArgumentNullException(nameof(context));
- }
-
- context.UserInfoEndpoint = context.Registration.ProviderType switch
- {
- // Dailymotion's userinfo endpoint requires sending the user identifier in the URI path.
- ProviderTypes.Dailymotion when (string?) context.TokenResponse?["uid"] is string identifier
- => OpenIddictHelpers.CreateAbsoluteUri(
- left : new Uri("https://api.dailymotion.com/user", UriKind.Absolute),
- right: new Uri(identifier, UriKind.Relative)),
-
- // HubSpot doesn't have a static userinfo endpoint but allows retrieving basic information
- // by using an access token info endpoint that requires sending the token in the URI path.
- ProviderTypes.HubSpot when
- (context.BackchannelAccessToken ?? context.FrontchannelAccessToken) is { Length: > 0 } token
- => OpenIddictHelpers.CreateAbsoluteUri(
- left : new Uri("https://api.hubapi.com/oauth/v1/access-tokens", UriKind.Absolute),
- right: new Uri(token, UriKind.Relative)),
-
- // SuperOffice doesn't expose a static OpenID Connect userinfo endpoint but offers an API whose
- // absolute URI needs to be computed based on a special claim returned in the identity token.
- ProviderTypes.SuperOffice when
- (context.BackchannelIdentityTokenPrincipal ?? // Always prefer the backchannel identity token when available.
- context.FrontchannelIdentityTokenPrincipal) is ClaimsPrincipal principal &&
- Uri.TryCreate(principal.GetClaim("http://schemes.superoffice.net/identity/webapi_url"), UriKind.Absolute, out Uri? uri)
- => OpenIddictHelpers.CreateAbsoluteUri(uri, new Uri("v1/user/currentPrincipal", UriKind.Relative)),
-
- // Zoho requires using a region-specific userinfo endpoint determined using
- // the "location" parameter returned from the authorization endpoint.
- //
- // For more information, see
- // https://www.zoho.com/accounts/protocol/oauth/multi-dc/client-authorization.html.
- ProviderTypes.Zoho when context.GrantType is GrantTypes.AuthorizationCode
- => ((string?) context.Request?["location"])?.ToUpperInvariant() switch
- {
- "AU" => new Uri("https://accounts.zoho.com.au/oauth/user/info", UriKind.Absolute),
- "CA" => new Uri("https://accounts.zohocloud.ca/oauth/user/info", UriKind.Absolute),
- "EU" => new Uri("https://accounts.zoho.eu/oauth/user/info", UriKind.Absolute),
- "IN" => new Uri("https://accounts.zoho.in/oauth/user/info", UriKind.Absolute),
- "JP" => new Uri("https://accounts.zoho.jp/oauth/user/info", UriKind.Absolute),
- "SA" => new Uri("https://accounts.zoho.sa/oauth/user/info", UriKind.Absolute),
- _ => new Uri("https://accounts.zoho.com/oauth/user/info", UriKind.Absolute)
- },
-
- ProviderTypes.Zoho when context.GrantType is GrantTypes.RefreshToken
- => !context.Properties.TryGetValue(Zoho.Properties.Location, out string? location) ||
- string.IsNullOrEmpty(location) ? throw new InvalidOperationException(SR.GetResourceString(SR.ID0451)) :
- location?.ToUpperInvariant() switch
- {
- "AU" => new Uri("https://accounts.zoho.com.au/oauth/user/info", UriKind.Absolute),
- "CA" => new Uri("https://accounts.zohocloud.ca/oauth/user/info", UriKind.Absolute),
- "EU" => new Uri("https://accounts.zoho.eu/oauth/user/info", UriKind.Absolute),
- "IN" => new Uri("https://accounts.zoho.in/oauth/user/info", UriKind.Absolute),
- "JP" => new Uri("https://accounts.zoho.jp/oauth/user/info", UriKind.Absolute),
- "SA" => new Uri("https://accounts.zoho.sa/oauth/user/info", UriKind.Absolute),
- _ => new Uri("https://accounts.zoho.com/oauth/user/info", UriKind.Absolute)
- },
-
- _ => context.UserInfoEndpoint
- };
-
- return default;
- }
- }
-
- ///
- /// Contains the logic responsible for disabling the userinfo retrieval for the providers that require it.
+ /// Contains the logic responsible for overriding the userinfo retrieval for the providers that require it.
///
- public sealed class DisableUserInfoRetrieval : IOpenIddictClientHandler
+ public sealed class OverrideUserInfoRetrieval : IOpenIddictClientHandler
{
///
/// Gets the default descriptor definition assigned to this handler.
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
- .UseSingletonHandler()
+ .UseSingletonHandler()
.SetOrder(EvaluateUserInfoRequest.Descriptor.Order + 250)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@@ -912,6 +872,21 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// userinfo retrieval is always disabled for the ADFS provider.
ProviderTypes.ActiveDirectoryFederationServices => false,
+ // Note: these providers don't have a static userinfo endpoint attached to their configuration
+ // so OpenIddict doesn't, by default, send a userinfo request. Since a dynamic endpoint is later
+ // computed and attached to the context, the default value MUST be overridden to send a request.
+ ProviderTypes.Dailymotion or ProviderTypes.HubSpot or
+ ProviderTypes.SuperOffice or ProviderTypes.Zoho
+ when context.GrantType is GrantTypes.AuthorizationCode or GrantTypes.DeviceCode or
+ GrantTypes.Implicit or GrantTypes.Password or
+ GrantTypes.RefreshToken or
+ // Apply the same logic for custom grant types.
+ (not null and not (GrantTypes.AuthorizationCode or GrantTypes.ClientCredentials or
+ GrantTypes.DeviceCode or GrantTypes.Implicit or
+ GrantTypes.Password or GrantTypes.RefreshToken)) &&
+ !context.DisableUserInfoRetrieval && (!string.IsNullOrEmpty(context.BackchannelAccessToken) ||
+ !string.IsNullOrEmpty(context.FrontchannelAccessToken)) => true,
+
// Note: the frontchannel or backchannel access tokens returned by Microsoft Entra ID
// when a Xbox scope is requested cannot be used with the userinfo endpoint as they use
// a legacy format that is not supported by the Microsoft Entra ID userinfo implementation.
@@ -956,17 +931,17 @@ public static partial class OpenIddictClientWebIntegrationHandlers
}
///
- /// Contains the logic responsible for disabling the userinfo validation for the providers that require it.
+ /// Contains the logic responsible for overriding the userinfo validation for the providers that require it.
///
- public sealed class DisableUserInfoValidation : IOpenIddictClientHandler
+ public sealed class OverrideUserInfoValidation : IOpenIddictClientHandler
{
///
/// Gets the default descriptor definition assigned to this handler.
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
- .UseSingletonHandler()
- .SetOrder(DisableUserInfoRetrieval.Descriptor.Order + 250)
+ .UseSingletonHandler()
+ .SetOrder(OverrideUserInfoRetrieval.Descriptor.Order + 250)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@@ -995,6 +970,93 @@ public static partial class OpenIddictClientWebIntegrationHandlers
}
}
+ ///
+ /// Contains the logic responsible for overriding the address
+ /// of the userinfo endpoint for the providers that require it.
+ ///
+ public sealed class OverrideUserInfoEndpoint : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(ResolveUserInfoEndpoint.Descriptor.Order + 500)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(ProcessAuthenticationContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ context.UserInfoEndpoint = context.Registration.ProviderType switch
+ {
+ // Dailymotion's userinfo endpoint requires sending the user identifier in the URI path.
+ ProviderTypes.Dailymotion when (string?) context.TokenResponse?["uid"] is string identifier
+ => OpenIddictHelpers.CreateAbsoluteUri(
+ left : new Uri("https://api.dailymotion.com/user", UriKind.Absolute),
+ right: new Uri(identifier, UriKind.Relative)),
+
+ // HubSpot doesn't have a static userinfo endpoint but allows retrieving basic information
+ // by using an access token info endpoint that requires sending the token in the URI path.
+ ProviderTypes.HubSpot when
+ (context.BackchannelAccessToken ?? context.FrontchannelAccessToken) is { Length: > 0 } token
+ => OpenIddictHelpers.CreateAbsoluteUri(
+ left : new Uri("https://api.hubapi.com/oauth/v1/access-tokens", UriKind.Absolute),
+ right: new Uri(token, UriKind.Relative)),
+
+ // SuperOffice doesn't expose a static OpenID Connect userinfo endpoint but offers an API whose
+ // absolute URI needs to be computed based on a special claim returned in the identity token.
+ ProviderTypes.SuperOffice when
+ (context.BackchannelIdentityTokenPrincipal ?? // Always prefer the backchannel identity token when available.
+ context.FrontchannelIdentityTokenPrincipal) is ClaimsPrincipal principal &&
+ Uri.TryCreate(principal.GetClaim("http://schemes.superoffice.net/identity/webapi_url"), UriKind.Absolute, out Uri? uri)
+ => OpenIddictHelpers.CreateAbsoluteUri(uri, new Uri("v1/user/currentPrincipal", UriKind.Relative)),
+
+ // Zoho requires using a region-specific userinfo endpoint determined using
+ // the "location" parameter returned from the authorization endpoint.
+ //
+ // For more information, see
+ // https://www.zoho.com/accounts/protocol/oauth/multi-dc/client-authorization.html.
+ ProviderTypes.Zoho when context.GrantType is GrantTypes.AuthorizationCode
+ => ((string?) context.Request?["location"])?.ToUpperInvariant() switch
+ {
+ "AU" => new Uri("https://accounts.zoho.com.au/oauth/user/info", UriKind.Absolute),
+ "CA" => new Uri("https://accounts.zohocloud.ca/oauth/user/info", UriKind.Absolute),
+ "EU" => new Uri("https://accounts.zoho.eu/oauth/user/info", UriKind.Absolute),
+ "IN" => new Uri("https://accounts.zoho.in/oauth/user/info", UriKind.Absolute),
+ "JP" => new Uri("https://accounts.zoho.jp/oauth/user/info", UriKind.Absolute),
+ "SA" => new Uri("https://accounts.zoho.sa/oauth/user/info", UriKind.Absolute),
+ _ => new Uri("https://accounts.zoho.com/oauth/user/info", UriKind.Absolute)
+ },
+
+ ProviderTypes.Zoho when context.GrantType is GrantTypes.RefreshToken
+ => !context.Properties.TryGetValue(Zoho.Properties.Location, out string? location) ||
+ string.IsNullOrEmpty(location) ? throw new InvalidOperationException(SR.GetResourceString(SR.ID0451)) :
+ location?.ToUpperInvariant() switch
+ {
+ "AU" => new Uri("https://accounts.zoho.com.au/oauth/user/info", UriKind.Absolute),
+ "CA" => new Uri("https://accounts.zohocloud.ca/oauth/user/info", UriKind.Absolute),
+ "EU" => new Uri("https://accounts.zoho.eu/oauth/user/info", UriKind.Absolute),
+ "IN" => new Uri("https://accounts.zoho.in/oauth/user/info", UriKind.Absolute),
+ "JP" => new Uri("https://accounts.zoho.jp/oauth/user/info", UriKind.Absolute),
+ "SA" => new Uri("https://accounts.zoho.sa/oauth/user/info", UriKind.Absolute),
+ _ => new Uri("https://accounts.zoho.com/oauth/user/info", UriKind.Absolute)
+ },
+
+ _ => context.UserInfoEndpoint
+ };
+
+ return default;
+ }
+ }
+
///
/// Contains the logic responsible for attaching additional parameters
/// to the userinfo request for the providers that require it.
@@ -1864,6 +1926,44 @@ public static partial class OpenIddictClientWebIntegrationHandlers
}
}
+ ///
+ /// Contains the logic responsible for overriding the negotiated revocation
+ /// endpoint client authentication method for the providers that require it.
+ ///
+ public sealed class OverrideRevocationEndpointClientAuthenticationMethod : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(AttachRevocationEndpointClientAuthenticationMethod.Descriptor.Order + 500)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(ProcessRevocationContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ context.RevocationEndpointClientAuthenticationMethod = context.Registration.ProviderType switch
+ {
+ // Note: Reddit requires using basic authentication to flow the client_id for all types
+ // of client applications, even when there's no client_secret assigned or attached.
+ ProviderTypes.Reddit => ClientAuthenticationMethods.ClientSecretBasic,
+
+ _ => context.RevocationEndpointClientAuthenticationMethod
+ };
+
+ return default;
+ }
+ }
+
///
/// Contains the logic responsible for adding non-standard claims to the client
/// assertions used for the revocation endpoint for the providers that require it.
diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml
index 42f5a1ca..99f22ae2 100644
--- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml
+++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml
@@ -877,6 +877,12 @@
+
+
+
+