Browse Source

Implement a new client authentication method negotiation logic and introduce mTLS support in the client stack

pull/2190/head
Kévin Chalet 1 year ago
parent
commit
9f613b1332
  1. 163
      gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs
  2. 10
      src/OpenIddict.Abstractions/OpenIddictConstants.cs
  3. 6
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  4. 31
      src/OpenIddict.Abstractions/Primitives/OpenIddictConfiguration.cs
  5. 3
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationConfiguration.cs
  6. 49
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpBuilder.cs
  7. 175
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs
  8. 3
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpExtensions.cs
  9. 95
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Device.cs
  10. 91
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Exchange.cs
  11. 91
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Introspection.cs
  12. 91
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Revocation.cs
  13. 689
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs
  14. 30
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpOptions.cs
  15. 34
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationConfiguration.cs
  16. 4
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationExtensions.cs
  17. 18
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Discovery.cs
  18. 49
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Exchange.cs
  19. 72
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Revocation.cs
  20. 290
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs
  21. 15
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml
  22. 2
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xsd
  23. 25
      src/OpenIddict.Client/OpenIddictClientBuilder.cs
  24. 7
      src/OpenIddict.Client/OpenIddictClientConfiguration.cs
  25. 41
      src/OpenIddict.Client/OpenIddictClientEvents.cs
  26. 247
      src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs
  27. 666
      src/OpenIddict.Client/OpenIddictClientHandlers.cs
  28. 35
      src/OpenIddict.Client/OpenIddictClientOptions.cs
  29. 10
      src/OpenIddict.Client/OpenIddictClientRegistration.cs
  30. 21
      src/OpenIddict.Client/OpenIddictClientService.cs
  31. 25
      src/OpenIddict.Server/OpenIddictServerBuilder.cs
  32. 52
      src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs
  33. 13
      src/OpenIddict.Server/OpenIddictServerOptions.cs
  34. 49
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpBuilder.cs
  35. 167
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConfiguration.cs
  36. 3
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpExtensions.cs
  37. 95
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Introspection.cs
  38. 221
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs
  39. 30
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpOptions.cs
  40. 12
      src/OpenIddict.Validation/OpenIddictValidationEvents.cs
  41. 45
      src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs
  42. 119
      src/OpenIddict.Validation/OpenIddictValidationHandlers.cs
  43. 16
      src/OpenIddict.Validation/OpenIddictValidationOptions.cs
  44. 4
      src/OpenIddict.Validation/OpenIddictValidationService.cs
  45. 14
      test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs

163
gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs

@ -124,6 +124,23 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder
[EditorBrowsable(EditorBrowsableState.Never)] [EditorBrowsable(EditorBrowsableState.Never)]
public OpenIddictClientRegistration Registration { get; } public OpenIddictClientRegistration Registration { get; }
/// <summary>
/// Adds one or more client authentication methods to the list of client authentication methods that can be negotiated for this provider.
/// </summary>
/// <param name=""methods"">The client authentication methods.</param>
/// <remarks>Note: explicitly configuring the allowed client authentication methods is NOT recommended in most cases.</remarks>
/// <returns>The <see cref=""OpenIddictClientWebIntegrationBuilder.{{ provider.name }}""/> instance.</returns>
[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));
}
/// <summary> /// <summary>
/// Adds one or more code challenge methods to the list of code challenge methods that can be negotiated for this provider. /// Adds one or more code challenge methods to the list of code challenge methods that can be negotiated for this provider.
/// </summary> /// </summary>
@ -791,17 +808,23 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder
ClrType = (string) setting.Attribute("Type") switch ClrType = (string) setting.Attribute("Type") switch
{ {
"Boolean" => "bool", "Boolean" => "bool",
"EncryptionKey" when (string) setting.Element("EncryptionAlgorithm").Attribute("Value") "EncryptionKey" => (string?) setting.Element("EncryptionAlgorithm")?.Attribute("Value") switch
is "RS256" or "RS384" or "RS512" => "RsaSecurityKey", {
"RS256" or "RS384" or "RS512" => "RsaSecurityKey",
"SigningKey" when (string) setting.Element("SigningAlgorithm").Attribute("Value") _ => "SecurityKey"
is "ES256" or "ES384" or "ES512" => "ECDsaSecurityKey", },
"SigningKey" when (string) setting.Element("SigningAlgorithm").Attribute("Value") "SigningCertificate" => "X509Certificate2",
is "PS256" or "PS384" or "PS512" or
"RS256" or "RS384" or "RS512" => "RsaSecurityKey", "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", "String" => "string",
"StringHashSet" => "HashSet<string>", "StringHashSet" => "HashSet<string>",
"Uri" => "Uri", "Uri" => "Uri",
@ -1121,13 +1144,111 @@ public sealed partial class OpenIddictClientWebIntegrationConfiguration
{{~ for setting in provider.settings ~}} {{~ for setting in provider.settings ~}}
{{~ if setting.type == 'EncryptionKey' ~}} {{~ 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 ~}}
{{~ end ~}} {{~ end ~}}
{{~ for setting in provider.settings ~}} {{~ 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' ~}} {{~ 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 ~}}
{{~ end ~}} {{~ end ~}}
} }
@ -1379,17 +1500,23 @@ public sealed partial class OpenIddictClientWebIntegrationSettings
ClrType = (string) setting.Attribute("Type") switch ClrType = (string) setting.Attribute("Type") switch
{ {
"Boolean" => "bool", "Boolean" => "bool",
"EncryptionKey" when (string) setting.Element("EncryptionAlgorithm").Attribute("Value") "EncryptionKey" => (string?) setting.Element("EncryptionAlgorithm")?.Attribute("Value") switch
is "RS256" or "RS384" or "RS512" => "RsaSecurityKey", {
"RS256" or "RS384" or "RS512" => "RsaSecurityKey",
"SigningKey" when (string) setting.Element("SigningAlgorithm").Attribute("Value") _ => "SecurityKey"
is "ES256" or "ES384" or "ES512" => "ECDsaSecurityKey", },
"SigningKey" when (string) setting.Element("SigningAlgorithm").Attribute("Value") "SigningCertificate" => "X509Certificate2",
is "PS256" or "PS384" or "PS512" or
"RS256" or "RS384" or "RS512" => "RsaSecurityKey", "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", "String" => "string",
"StringHashSet" => "HashSet<string>", "StringHashSet" => "HashSet<string>",
"Uri" => "Uri", "Uri" => "Uri",

10
src/OpenIddict.Abstractions/OpenIddictConstants.cs

@ -178,6 +178,8 @@ public static class OpenIddictConstants
public const string ClientSecretPost = "client_secret_post"; public const string ClientSecretPost = "client_secret_post";
public const string None = "none"; public const string None = "none";
public const string PrivateKeyJwt = "private_key_jwt"; 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 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 IntrospectionEndpointAuthSigningAlgValuesSupported = "introspection_endpoint_auth_signing_alg_values_supported";
public const string Issuer = "issuer"; public const string Issuer = "issuer";
public const string JwksUri = "jwks_uri"; public const string JwksUri = "jwks_uri";
public const string MtlsEndpointAliases = "mtls_endpoint_aliases";
public const string OpPolicyUri = "op_policy_uri"; public const string OpPolicyUri = "op_policy_uri";
public const string OpTosUri = "op_tos_uri"; public const string OpTosUri = "op_tos_uri";
public const string RequestObjectEncryptionAlgValuesSupported = "request_object_encryption_alg_values_supported"; 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 ScopesSupported = "scopes_supported";
public const string ServiceDocumentation = "service_documentation"; public const string ServiceDocumentation = "service_documentation";
public const string SubjectTypesSupported = "subject_types_supported"; 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 TokenEndpoint = "token_endpoint";
public const string TokenEndpointAuthMethodsSupported = "token_endpoint_auth_methods_supported"; public const string TokenEndpointAuthMethodsSupported = "token_endpoint_auth_methods_supported";
public const string TokenEndpointAuthSigningAlgValuesSupported = "token_endpoint_auth_signing_alg_values_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 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 static class TokenFormats
{ {
public const string Jwt = "urn:ietf:params:oauth:token-type:jwt"; public const string Jwt = "urn:ietf:params:oauth:token-type:jwt";

6
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -1698,6 +1698,12 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
<data name="ID0454" xml:space="preserve"> <data name="ID0454" xml:space="preserve">
<value>The format of the specified certificate is not supported.</value> <value>The format of the specified certificate is not supported.</value>
</data> </data>
<data name="ID0455" xml:space="preserve">
<value>Registration identifiers cannot contain U+001E or U+001F characters.</value>
</data>
<data name="ID0456" xml:space="preserve">
<value>The specified client authentication method/token binding methods combination is not valid.</value>
</data>
<data name="ID2000" xml:space="preserve"> <data name="ID2000" xml:space="preserve">
<value>The security token is missing.</value> <value>The security token is missing.</value>
</data> </data>

31
src/OpenIddict.Abstractions/Primitives/OpenIddictConfiguration.cs

@ -76,6 +76,31 @@ public sealed class OpenIddictConfiguration
/// </summary> /// </summary>
public Uri? JwksUri { get; set; } public Uri? JwksUri { get; set; }
/// <summary>
/// Gets or sets the URI of the mTLS-enabled device authorization endpoint.
/// </summary>
public Uri? MtlsDeviceAuthorizationEndpoint { get; set; }
/// <summary>
/// Gets or sets the URI of the mTLS-enabled introspection endpoint.
/// </summary>
public Uri? MtlsIntrospectionEndpoint { get; set; }
/// <summary>
/// Gets or sets the URI of the mTLS-enabled revocation endpoint.
/// </summary>
public Uri? MtlsRevocationEndpoint { get; set; }
/// <summary>
/// Gets or sets the URI of the mTLS-enabled token endpoint.
/// </summary>
public Uri? MtlsTokenEndpoint { get; set; }
/// <summary>
/// Gets or sets the URI of the mTLS-enabled userinfo endpoint.
/// </summary>
public Uri? MtlsUserInfoEndpoint { get; set; }
/// <summary> /// <summary>
/// Gets the additional properties. /// Gets the additional properties.
/// </summary> /// </summary>
@ -111,6 +136,12 @@ public sealed class OpenIddictConfiguration
/// </summary> /// </summary>
public List<SecurityKey> SigningKeys { get; } = []; public List<SecurityKey> SigningKeys { get; } = [];
/// <summary>
/// Gets or sets a boolean indicating whether access tokens issued by the
/// authorization server are bound to the client certificate when using mTLS.
/// </summary>
public bool? TlsClientCertificateBoundAccessTokens { get; set; }
/// <summary> /// <summary>
/// Gets or sets the URI of the token endpoint. /// Gets or sets the URI of the token endpoint.
/// </summary> /// </summary>

3
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. // Register the built-in event handlers used by the OpenIddict client system integration components.
options.Handlers.AddRange(OpenIddictClientSystemIntegrationHandlers.DefaultHandlers); options.Handlers.AddRange(OpenIddictClientSystemIntegrationHandlers.DefaultHandlers);
// Enable response_mode=fragment support by default.
options.ResponseModes.Add(ResponseModes.Fragment);
} }
/// <inheritdoc/> /// <inheritdoc/>

49
src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpBuilder.cs

@ -9,6 +9,7 @@ using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Net.Mail; using System.Net.Mail;
using System.Reflection; using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using OpenIddict.Client; using OpenIddict.Client;
using OpenIddict.Client.SystemNetHttp; using OpenIddict.Client.SystemNetHttp;
using Polly; using Polly;
@ -339,6 +340,54 @@ public sealed class OpenIddictClientSystemNetHttpBuilder
productVersion: assembly.GetName().Version!.ToString())); productVersion: assembly.GetName().Version!.ToString()));
} }
/// <summary>
/// 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.
/// </summary>
/// <param name="selector">The selector delegate.</param>
/// <remarks>
/// 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).
/// </remarks>
/// <returns>The <see cref="OpenIddictClientSystemNetHttpBuilder"/> instance.</returns>
public OpenIddictClientSystemNetHttpBuilder SetSelfSignedTlsClientAuthenticationCertificateSelector(
Func<OpenIddictClientRegistration, X509Certificate2?> selector)
{
if (selector is null)
{
throw new ArgumentNullException(nameof(selector));
}
return Configure(options => options.SelfSignedTlsClientAuthenticationCertificateSelector = selector);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="selector">The selector delegate.</param>
/// <remarks>
/// 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).
/// </remarks>
/// <returns>The <see cref="OpenIddictClientSystemNetHttpBuilder"/> instance.</returns>
public OpenIddictClientSystemNetHttpBuilder SetTlsClientAuthenticationCertificateSelector(
Func<OpenIddictClientRegistration, X509Certificate2?> selector)
{
if (selector is null)
{
throw new ArgumentNullException(nameof(selector));
}
return Configure(options => options.TlsClientAuthenticationCertificateSelector = selector);
}
/// <inheritdoc/> /// <inheritdoc/>
[EditorBrowsable(EditorBrowsableState.Never)] [EditorBrowsable(EditorBrowsableState.Never)]
public override bool Equals(object? obj) => base.Equals(obj); public override bool Equals(object? obj) => base.Equals(obj);

175
src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs

@ -5,12 +5,14 @@
*/ */
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http; using Microsoft.Extensions.Http;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Polly; using Polly;
#if SUPPORTS_HTTP_CLIENT_RESILIENCE #if SUPPORTS_HTTP_CLIENT_RESILIENCE
@ -25,7 +27,8 @@ namespace OpenIddict.Client.SystemNetHttp;
[EditorBrowsable(EditorBrowsableState.Advanced)] [EditorBrowsable(EditorBrowsableState.Advanced)]
public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptions<OpenIddictClientOptions>, public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptions<OpenIddictClientOptions>,
IConfigureNamedOptions<HttpClientFactoryOptions>, IConfigureNamedOptions<HttpClientFactoryOptions>,
IPostConfigureOptions<HttpClientFactoryOptions> IPostConfigureOptions<HttpClientFactoryOptions>,
IPostConfigureOptions<OpenIddictClientSystemNetHttpOptions>
{ {
private readonly IServiceProvider _provider; 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. // Register the built-in event handlers used by the OpenIddict System.Net.Http client components.
options.Handlers.AddRange(OpenIddictClientSystemNetHttpHandlers.DefaultHandlers); 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);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -59,8 +67,29 @@ public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptio
throw new ArgumentNullException(nameof(options)); 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. // 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; return;
} }
@ -113,6 +142,32 @@ public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptio
#pragma warning restore EXTEXP0001 #pragma warning restore EXTEXP0001
} }
#endif #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. // Register the user-defined HTTP client handler actions.
@ -132,8 +187,29 @@ public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptio
throw new ArgumentNullException(nameof(options)); 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. // 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; return;
} }
@ -194,19 +270,94 @@ public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptio
}); });
} }
static bool TryResolveRegistrationId(string name, [NotNullWhen(true)] out string? value) /// <inheritdoc/>
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) || static bool HasClientAuthenticationExtendedKeyUsage(X509Certificate2 certificate)
name.Length < assembly.Name!.Length + 1 ||
name[assembly.Name.Length] is not ':')
{ {
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; return false;
} }
value = name[(assembly.Name.Length + 1)..]; // Note: to avoid building and introspecting a X.509 certificate chain and reduce the cost
return true; // 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);
} }
} }

3
src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpExtensions.cs

@ -49,6 +49,9 @@ public static class OpenIddictClientSystemNetHttpExtensions
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
IPostConfigureOptions<HttpClientFactoryOptions>, OpenIddictClientSystemNetHttpConfiguration>()); IPostConfigureOptions<HttpClientFactoryOptions>, OpenIddictClientSystemNetHttpConfiguration>());
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
IPostConfigureOptions<OpenIddictClientSystemNetHttpOptions>, OpenIddictClientSystemNetHttpConfiguration>());
return new OpenIddictClientSystemNetHttpBuilder(builder.Services); return new OpenIddictClientSystemNetHttpBuilder(builder.Services);
} }

95
src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Device.cs

@ -5,10 +5,6 @@
*/ */
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Diagnostics;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
namespace OpenIddict.Client.SystemNetHttp; namespace OpenIddict.Client.SystemNetHttp;
@ -26,7 +22,7 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
AttachJsonAcceptHeaders<PrepareDeviceAuthorizationRequestContext>.Descriptor, AttachJsonAcceptHeaders<PrepareDeviceAuthorizationRequestContext>.Descriptor,
AttachUserAgentHeader<PrepareDeviceAuthorizationRequestContext>.Descriptor, AttachUserAgentHeader<PrepareDeviceAuthorizationRequestContext>.Descriptor,
AttachFromHeader<PrepareDeviceAuthorizationRequestContext>.Descriptor, AttachFromHeader<PrepareDeviceAuthorizationRequestContext>.Descriptor,
AttachBasicAuthenticationCredentials.Descriptor, AttachBasicAuthenticationCredentials<PrepareDeviceAuthorizationRequestContext>.Descriptor,
AttachHttpParameters<PrepareDeviceAuthorizationRequestContext>.Descriptor, AttachHttpParameters<PrepareDeviceAuthorizationRequestContext>.Descriptor,
SendHttpRequest<ApplyDeviceAuthorizationRequestContext>.Descriptor, SendHttpRequest<ApplyDeviceAuthorizationRequestContext>.Descriptor,
DisposeHttpRequest<ApplyDeviceAuthorizationRequestContext>.Descriptor, DisposeHttpRequest<ApplyDeviceAuthorizationRequestContext>.Descriptor,
@ -40,94 +36,5 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
ValidateHttpResponse<ExtractDeviceAuthorizationResponseContext>.Descriptor, ValidateHttpResponse<ExtractDeviceAuthorizationResponseContext>.Descriptor,
DisposeHttpResponse<ExtractDeviceAuthorizationResponseContext>.Descriptor DisposeHttpResponse<ExtractDeviceAuthorizationResponseContext>.Descriptor
]); ]);
/// <summary>
/// Contains the logic responsible for attaching the client credentials to the HTTP Authorization header.
/// </summary>
public sealed class AttachBasicAuthenticationCredentials : IOpenIddictClientHandler<PrepareDeviceAuthorizationRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<PrepareDeviceAuthorizationRequestContext>()
.AddFilter<RequireHttpUri>()
.UseSingletonHandler<AttachBasicAuthenticationCredentials>()
.SetOrder(AttachHttpParameters<PrepareDeviceAuthorizationRequestContext>.Descriptor.Order - 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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", "+");
}
}
} }
} }

91
src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Exchange.cs

@ -26,7 +26,7 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
AttachJsonAcceptHeaders<PrepareTokenRequestContext>.Descriptor, AttachJsonAcceptHeaders<PrepareTokenRequestContext>.Descriptor,
AttachUserAgentHeader<PrepareTokenRequestContext>.Descriptor, AttachUserAgentHeader<PrepareTokenRequestContext>.Descriptor,
AttachFromHeader<PrepareTokenRequestContext>.Descriptor, AttachFromHeader<PrepareTokenRequestContext>.Descriptor,
AttachBasicAuthenticationCredentials.Descriptor, AttachBasicAuthenticationCredentials<PrepareTokenRequestContext>.Descriptor,
AttachHttpParameters<PrepareTokenRequestContext>.Descriptor, AttachHttpParameters<PrepareTokenRequestContext>.Descriptor,
SendHttpRequest<ApplyTokenRequestContext>.Descriptor, SendHttpRequest<ApplyTokenRequestContext>.Descriptor,
DisposeHttpRequest<ApplyTokenRequestContext>.Descriptor, DisposeHttpRequest<ApplyTokenRequestContext>.Descriptor,
@ -40,94 +40,5 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
ValidateHttpResponse<ExtractTokenResponseContext>.Descriptor, ValidateHttpResponse<ExtractTokenResponseContext>.Descriptor,
DisposeHttpResponse<ExtractTokenResponseContext>.Descriptor DisposeHttpResponse<ExtractTokenResponseContext>.Descriptor
]); ]);
/// <summary>
/// Contains the logic responsible for attaching the client credentials to the HTTP Authorization header.
/// </summary>
public sealed class AttachBasicAuthenticationCredentials : IOpenIddictClientHandler<PrepareTokenRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<PrepareTokenRequestContext>()
.AddFilter<RequireHttpUri>()
.UseSingletonHandler<AttachBasicAuthenticationCredentials>()
.SetOrder(AttachHttpParameters<PrepareTokenRequestContext>.Descriptor.Order - 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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", "+");
}
}
} }
} }

91
src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Introspection.cs

@ -26,7 +26,7 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
AttachJsonAcceptHeaders<PrepareIntrospectionRequestContext>.Descriptor, AttachJsonAcceptHeaders<PrepareIntrospectionRequestContext>.Descriptor,
AttachUserAgentHeader<PrepareIntrospectionRequestContext>.Descriptor, AttachUserAgentHeader<PrepareIntrospectionRequestContext>.Descriptor,
AttachFromHeader<PrepareIntrospectionRequestContext>.Descriptor, AttachFromHeader<PrepareIntrospectionRequestContext>.Descriptor,
AttachBasicAuthenticationCredentials.Descriptor, AttachBasicAuthenticationCredentials<PrepareIntrospectionRequestContext>.Descriptor,
AttachHttpParameters<PrepareIntrospectionRequestContext>.Descriptor, AttachHttpParameters<PrepareIntrospectionRequestContext>.Descriptor,
SendHttpRequest<ApplyIntrospectionRequestContext>.Descriptor, SendHttpRequest<ApplyIntrospectionRequestContext>.Descriptor,
DisposeHttpRequest<ApplyIntrospectionRequestContext>.Descriptor, DisposeHttpRequest<ApplyIntrospectionRequestContext>.Descriptor,
@ -40,94 +40,5 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
ValidateHttpResponse<ExtractIntrospectionResponseContext>.Descriptor, ValidateHttpResponse<ExtractIntrospectionResponseContext>.Descriptor,
DisposeHttpResponse<ExtractIntrospectionResponseContext>.Descriptor DisposeHttpResponse<ExtractIntrospectionResponseContext>.Descriptor
]); ]);
/// <summary>
/// Contains the logic responsible for attaching the client credentials to the HTTP Authorization header.
/// </summary>
public sealed class AttachBasicAuthenticationCredentials : IOpenIddictClientHandler<PrepareIntrospectionRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<PrepareIntrospectionRequestContext>()
.AddFilter<RequireHttpUri>()
.UseSingletonHandler<AttachBasicAuthenticationCredentials>()
.SetOrder(AttachHttpParameters<PrepareIntrospectionRequestContext>.Descriptor.Order - 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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", "+");
}
}
} }
} }

91
src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Revocation.cs

@ -26,7 +26,7 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
AttachJsonAcceptHeaders<PrepareRevocationRequestContext>.Descriptor, AttachJsonAcceptHeaders<PrepareRevocationRequestContext>.Descriptor,
AttachUserAgentHeader<PrepareRevocationRequestContext>.Descriptor, AttachUserAgentHeader<PrepareRevocationRequestContext>.Descriptor,
AttachFromHeader<PrepareRevocationRequestContext>.Descriptor, AttachFromHeader<PrepareRevocationRequestContext>.Descriptor,
AttachBasicAuthenticationCredentials.Descriptor, AttachBasicAuthenticationCredentials<PrepareRevocationRequestContext>.Descriptor,
AttachHttpParameters<PrepareRevocationRequestContext>.Descriptor, AttachHttpParameters<PrepareRevocationRequestContext>.Descriptor,
SendHttpRequest<ApplyRevocationRequestContext>.Descriptor, SendHttpRequest<ApplyRevocationRequestContext>.Descriptor,
DisposeHttpRequest<ApplyRevocationRequestContext>.Descriptor, DisposeHttpRequest<ApplyRevocationRequestContext>.Descriptor,
@ -41,94 +41,5 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
ValidateHttpResponse<ExtractRevocationResponseContext>.Descriptor, ValidateHttpResponse<ExtractRevocationResponseContext>.Descriptor,
DisposeHttpResponse<ExtractRevocationResponseContext>.Descriptor DisposeHttpResponse<ExtractRevocationResponseContext>.Descriptor
]); ]);
/// <summary>
/// Contains the logic responsible for attaching the client credentials to the HTTP Authorization header.
/// </summary>
public sealed class AttachBasicAuthenticationCredentials : IOpenIddictClientHandler<PrepareRevocationRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<PrepareRevocationRequestContext>()
.AddFilter<RequireHttpUri>()
.UseSingletonHandler<AttachBasicAuthenticationCredentials>()
.SetOrder(AttachHttpParameters<PrepareRevocationRequestContext>.Descriptor.Order - 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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", "+");
}
}
} }
} }

689
src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs

@ -11,9 +11,12 @@ using System.IO.Compression;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Extensions; using OpenIddict.Extensions;
using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpConstants; using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpConstants;
@ -23,6 +26,27 @@ namespace OpenIddict.Client.SystemNetHttp;
public static partial class OpenIddictClientSystemNetHttpHandlers public static partial class OpenIddictClientSystemNetHttpHandlers
{ {
public static ImmutableArray<OpenIddictClientHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create([ public static ImmutableArray<OpenIddictClientHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create([
/*
* Authentication processing:
*/
AttachNonDefaultTokenEndpointClientAuthenticationMethod.Descriptor,
AttachNonDefaultUserInfoEndpointTokenBindingMethods.Descriptor,
/*
* Challenge processing:
*/
AttachNonDefaultDeviceAuthorizationEndpointClientAuthenticationMethod.Descriptor,
/*
* Introspection processing:
*/
AttachNonDefaultIntrospectionEndpointClientAuthenticationMethod.Descriptor,
/*
* Revocation processing:
*/
AttachNonDefaultRevocationEndpointClientAuthenticationMethod.Descriptor,
.. Device.DefaultHandlers, .. Device.DefaultHandlers,
.. Discovery.DefaultHandlers, .. Discovery.DefaultHandlers,
.. Exchange.DefaultHandlers, .. Exchange.DefaultHandlers,
@ -31,6 +55,559 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
.. UserInfo.DefaultHandlers .. UserInfo.DefaultHandlers
]); ]);
/// <summary>
/// Contains the logic responsible for negotiating the best token endpoint client
/// authentication method supported by both the client and the authorization server.
/// </summary>
public sealed class AttachNonDefaultTokenEndpointClientAuthenticationMethod : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
private readonly IOptionsMonitor<OpenIddictClientSystemNetHttpOptions> _options;
public AttachNonDefaultTokenEndpointClientAuthenticationMethod(
IOptionsMonitor<OpenIddictClientSystemNetHttpOptions> options)
=> _options = options ?? throw new ArgumentNullException(nameof(options));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireTokenRequest>()
.UseSingletonHandler<AttachNonDefaultTokenEndpointClientAuthenticationMethod>()
.SetOrder(AttachTokenEndpointClientAuthenticationMethod.Descriptor.Order - 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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<string>,
_ => 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;
}
}
/// <summary>
/// Contains the logic responsible for negotiating the best token binding
/// methods supported by both the client and the authorization server.
/// </summary>
public sealed class AttachNonDefaultUserInfoEndpointTokenBindingMethods : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
private readonly IOptionsMonitor<OpenIddictClientSystemNetHttpOptions> _options;
public AttachNonDefaultUserInfoEndpointTokenBindingMethods(
IOptionsMonitor<OpenIddictClientSystemNetHttpOptions> options)
=> _options = options ?? throw new ArgumentNullException(nameof(options));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireUserInfoRequest>()
.UseSingletonHandler<AttachNonDefaultUserInfoEndpointTokenBindingMethods>()
.SetOrder(AttachUserInfoEndpointTokenBindingMethods.Descriptor.Order - 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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;
}
}
/// <summary>
/// Contains the logic responsible for negotiating the best device authorization endpoint
/// client authentication method supported by both the client and the authorization server.
/// </summary>
public sealed class AttachNonDefaultDeviceAuthorizationEndpointClientAuthenticationMethod : IOpenIddictClientHandler<ProcessChallengeContext>
{
private readonly IOptionsMonitor<OpenIddictClientSystemNetHttpOptions> _options;
public AttachNonDefaultDeviceAuthorizationEndpointClientAuthenticationMethod(
IOptionsMonitor<OpenIddictClientSystemNetHttpOptions> options)
=> _options = options ?? throw new ArgumentNullException(nameof(options));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireDeviceAuthorizationRequest>()
.UseSingletonHandler<AttachNonDefaultDeviceAuthorizationEndpointClientAuthenticationMethod>()
.SetOrder(AttachDeviceAuthorizationEndpointClientAuthenticationMethod.Descriptor.Order - 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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<string>,
_ => 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;
}
}
/// <summary>
/// Contains the logic responsible for negotiating the best introspection endpoint client
/// authentication method supported by both the client and the authorization server.
/// </summary>
public sealed class AttachNonDefaultIntrospectionEndpointClientAuthenticationMethod : IOpenIddictClientHandler<ProcessIntrospectionContext>
{
private readonly IOptionsMonitor<OpenIddictClientSystemNetHttpOptions> _options;
public AttachNonDefaultIntrospectionEndpointClientAuthenticationMethod(
IOptionsMonitor<OpenIddictClientSystemNetHttpOptions> options)
=> _options = options ?? throw new ArgumentNullException(nameof(options));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessIntrospectionContext>()
.AddFilter<RequireIntrospectionRequest>()
.UseSingletonHandler<AttachNonDefaultIntrospectionEndpointClientAuthenticationMethod>()
.SetOrder(AttachIntrospectionEndpointClientAuthenticationMethod.Descriptor.Order - 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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<string>,
_ => 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;
}
}
/// <summary>
/// Contains the logic responsible for negotiating the best revocation endpoint client
/// authentication method supported by both the client and the authorization server.
/// </summary>
public sealed class AttachNonDefaultRevocationEndpointClientAuthenticationMethod : IOpenIddictClientHandler<ProcessRevocationContext>
{
private readonly IOptionsMonitor<OpenIddictClientSystemNetHttpOptions> _options;
public AttachNonDefaultRevocationEndpointClientAuthenticationMethod(
IOptionsMonitor<OpenIddictClientSystemNetHttpOptions> options)
=> _options = options ?? throw new ArgumentNullException(nameof(options));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessRevocationContext>()
.AddFilter<RequireRevocationRequest>()
.UseSingletonHandler<AttachNonDefaultRevocationEndpointClientAuthenticationMethod>()
.SetOrder(AttachRevocationEndpointClientAuthenticationMethod.Descriptor.Order - 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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<string>,
_ => 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;
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for creating and attaching a <see cref="HttpClient"/>. /// Contains the logic responsible for creating and attaching a <see cref="HttpClient"/>.
/// </summary> /// </summary>
@ -60,11 +637,60 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
var assembly = typeof(OpenIddictClientSystemNetHttpOptions).Assembly.GetName(); // Note: HttpClientFactory doesn't support flowing a list of properties that can be
var name = $"{assembly.Name}:{context.Registration.RegistrationId}"; // 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. // 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))); throw new InvalidOperationException(SR.GetResourceString(SR.ID0174)));
return default; return default;
@ -318,6 +944,63 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
} }
} }
/// <summary>
/// Contains the logic responsible for attaching the client credentials to the HTTP Authorization header.
/// </summary>
public sealed class AttachBasicAuthenticationCredentials<TContext> : IOpenIddictClientHandler<TContext> where TContext : BaseExternalContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpUri>()
.UseSingletonHandler<AttachBasicAuthenticationCredentials<TContext>>()
.SetOrder(AttachHttpParameters<TContext>.Descriptor.Order - 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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;
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for attaching the parameters to the HTTP request. /// Contains the logic responsible for attaching the parameters to the HTTP request.
/// </summary> /// </summary>

30
src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpOptions.cs

@ -4,10 +4,12 @@
* the license and the contributors participating to this project. * the license and the contributors participating to this project.
*/ */
using System.ComponentModel;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Net.Mail; using System.Net.Mail;
using System.Security.Cryptography.X509Certificates;
using Polly; using Polly;
using Polly.Extensions.Http; using Polly.Extensions.Http;
@ -80,4 +82,32 @@ public sealed class OpenIddictClientSystemNetHttpOptions
/// instances created by the OpenIddict client/System.Net.Http integration. /// instances created by the OpenIddict client/System.Net.Http integration.
/// </summary> /// </summary>
public List<Action<OpenIddictClientRegistration, HttpClientHandler>> HttpClientHandlerActions { get; } = []; public List<Action<OpenIddictClientRegistration, HttpClientHandler>> HttpClientHandlerActions { get; } = [];
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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).
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public Func<OpenIddictClientRegistration, X509Certificate2?> SelfSignedTlsClientAuthenticationCertificateSelector { get; set; } = default!;
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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).
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public Func<OpenIddictClientRegistration, X509Certificate2?> TlsClientAuthenticationCertificateSelector { get; set; } = default!;
} }

34
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationConfiguration.cs

@ -5,10 +5,7 @@
*/ */
using System.ComponentModel; using System.ComponentModel;
using System.Net.Http;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using OpenIddict.Client.SystemNetHttp;
using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants;
namespace OpenIddict.Client.WebIntegration; namespace OpenIddict.Client.WebIntegration;
@ -17,7 +14,6 @@ namespace OpenIddict.Client.WebIntegration;
/// </summary> /// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)] [EditorBrowsable(EditorBrowsableState.Advanced)]
public sealed partial class OpenIddictClientWebIntegrationConfiguration : IConfigureOptions<OpenIddictClientOptions>, public sealed partial class OpenIddictClientWebIntegrationConfiguration : IConfigureOptions<OpenIddictClientOptions>,
IConfigureOptions<OpenIddictClientSystemNetHttpOptions>,
IPostConfigureOptions<OpenIddictClientOptions> IPostConfigureOptions<OpenIddictClientOptions>
{ {
/// <inheritdoc/> /// <inheritdoc/>
@ -32,36 +28,6 @@ public sealed partial class OpenIddictClientWebIntegrationConfiguration : IConfi
options.Handlers.AddRange(OpenIddictClientWebIntegrationHandlers.DefaultHandlers); options.Handlers.AddRange(OpenIddictClientWebIntegrationHandlers.DefaultHandlers);
} }
/// <inheritdoc/>
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);
}
});
}
/// <inheritdoc/> /// <inheritdoc/>
public void PostConfigure(string? name, OpenIddictClientOptions options) public void PostConfigure(string? name, OpenIddictClientOptions options)
{ {

4
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationExtensions.cs

@ -7,7 +7,6 @@
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using OpenIddict.Client; using OpenIddict.Client;
using OpenIddict.Client.SystemNetHttp;
using OpenIddict.Client.WebIntegration; using OpenIddict.Client.WebIntegration;
namespace Microsoft.Extensions.DependencyInjection; namespace Microsoft.Extensions.DependencyInjection;
@ -42,9 +41,6 @@ public static partial class OpenIddictClientWebIntegrationExtensions
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
IConfigureOptions<OpenIddictClientOptions>, OpenIddictClientWebIntegrationConfiguration>()); IConfigureOptions<OpenIddictClientOptions>, OpenIddictClientWebIntegrationConfiguration>());
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
IConfigureOptions<OpenIddictClientSystemNetHttpOptions>, OpenIddictClientWebIntegrationConfiguration>());
// Note: the IPostConfigureOptions<OpenIddictClientOptions> service responsible for populating // Note: the IPostConfigureOptions<OpenIddictClientOptions> service responsible for populating
// the client registrations MUST be registered before OpenIddictClientConfiguration to ensure // the client registrations MUST be registered before OpenIddictClientConfiguration to ensure
// the registrations are correctly populated before being validated. // the registrations are correctly populated before being validated.

18
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Discovery.cs

@ -338,6 +338,24 @@ public static partial class OpenIddictClientWebIntegrationHandlers
ClientAuthenticationMethods.ClientSecretPost); 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; return default;
} }
} }

49
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.OpenIddictClientSystemNetHttpConstants;
using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpHandlerFilters; using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpHandlerFilters;
using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpHandlers; using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpHandlers;
using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpHandlers.Exchange;
using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants; using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants;
namespace OpenIddict.Client.WebIntegration; namespace OpenIddict.Client.WebIntegration;
@ -131,7 +130,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
= OpenIddictClientHandlerDescriptor.CreateBuilder<PrepareTokenRequestContext>() = OpenIddictClientHandlerDescriptor.CreateBuilder<PrepareTokenRequestContext>()
.AddFilter<RequireHttpUri>() .AddFilter<RequireHttpUri>()
.UseSingletonHandler<AttachNonStandardBasicAuthenticationCredentials>() .UseSingletonHandler<AttachNonStandardBasicAuthenticationCredentials>()
.SetOrder(AttachBasicAuthenticationCredentials.Descriptor.Order - 500) .SetOrder(AttachBasicAuthenticationCredentials<PrepareTokenRequestContext>.Descriptor.Order - 500)
.SetType(OpenIddictClientHandlerType.BuiltIn) .SetType(OpenIddictClientHandlerType.BuiltIn)
.Build(); .Build();
@ -155,49 +154,10 @@ public static partial class OpenIddictClientWebIntegrationHandlers
var request = context.Transaction.GetHttpRequestMessage() ?? var request = context.Transaction.GetHttpRequestMessage() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0173)); 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 // 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. // 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.ClientId) &&
!string.IsNullOrEmpty(context.Request.ClientSecret)) !string.IsNullOrEmpty(context.Request.ClientSecret))
{ {
@ -215,9 +175,6 @@ public static partial class OpenIddictClientWebIntegrationHandlers
} }
return default; return default;
static string? EscapeDataString(string? value)
=> value is not null ? Uri.EscapeDataString(value).Replace("%20", "+") : null;
} }
} }

72
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Revocation.cs

@ -5,13 +5,9 @@
*/ */
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Diagnostics;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpHandlerFilters; using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpHandlerFilters;
using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpHandlers; using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpHandlers;
using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpHandlers.Exchange;
using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants; using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants;
namespace OpenIddict.Client.WebIntegration; namespace OpenIddict.Client.WebIntegration;
@ -21,80 +17,12 @@ public static partial class OpenIddictClientWebIntegrationHandlers
public static class Revocation public static class Revocation
{ {
public static ImmutableArray<OpenIddictClientHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create([ public static ImmutableArray<OpenIddictClientHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create([
/*
* Revocation request preparation:
*/
AttachNonStandardBasicAuthenticationCredentials.Descriptor,
/* /*
* Revocation response extraction: * Revocation response extraction:
*/ */
NormalizeContentType.Descriptor NormalizeContentType.Descriptor
]); ]);
/// <summary>
/// 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.
/// </summary>
public sealed class AttachNonStandardBasicAuthenticationCredentials : IOpenIddictClientHandler<PrepareRevocationRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<PrepareRevocationRequestContext>()
.AddFilter<RequireHttpUri>()
.UseSingletonHandler<AttachNonStandardBasicAuthenticationCredentials>()
.SetOrder(AttachBasicAuthenticationCredentials.Descriptor.Order - 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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;
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for normalizing the returned content /// Contains the logic responsible for normalizing the returned content
/// type of revocation responses for the providers that require it. /// type of revocation responses for the providers that require it.

290
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs

@ -25,6 +25,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
ValidateRedirectionRequestSignature.Descriptor, ValidateRedirectionRequestSignature.Descriptor,
HandleNonStandardFrontchannelErrorResponse.Descriptor, HandleNonStandardFrontchannelErrorResponse.Descriptor,
ValidateNonStandardParameters.Descriptor, ValidateNonStandardParameters.Descriptor,
OverrideTokenEndpointClientAuthenticationMethod.Descriptor,
OverrideTokenEndpoint.Descriptor, OverrideTokenEndpoint.Descriptor,
AttachNonStandardClientAssertionClaims.Descriptor, AttachNonStandardClientAssertionClaims.Descriptor,
AttachAdditionalTokenRequestParameters.Descriptor, AttachAdditionalTokenRequestParameters.Descriptor,
@ -32,9 +33,9 @@ public static partial class OpenIddictClientWebIntegrationHandlers
AdjustRedirectUriInTokenRequest.Descriptor, AdjustRedirectUriInTokenRequest.Descriptor,
OverrideValidatedBackchannelTokens.Descriptor, OverrideValidatedBackchannelTokens.Descriptor,
DisableBackchannelIdentityTokenNonceValidation.Descriptor, DisableBackchannelIdentityTokenNonceValidation.Descriptor,
OverrideUserInfoRetrieval.Descriptor,
OverrideUserInfoValidation.Descriptor,
OverrideUserInfoEndpoint.Descriptor, OverrideUserInfoEndpoint.Descriptor,
DisableUserInfoRetrieval.Descriptor,
DisableUserInfoValidation.Descriptor,
AttachAdditionalUserInfoRequestParameters.Descriptor, AttachAdditionalUserInfoRequestParameters.Descriptor,
PopulateUserInfoTokenPrincipalFromTokenResponse.Descriptor, PopulateUserInfoTokenPrincipalFromTokenResponse.Descriptor,
MapCustomWebServicesFederationClaims.Descriptor, MapCustomWebServicesFederationClaims.Descriptor,
@ -52,6 +53,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
/* /*
* Revocation processing: * Revocation processing:
*/ */
OverrideRevocationEndpointClientAuthenticationMethod.Descriptor,
AttachNonStandardRevocationClientAssertionClaims.Descriptor, AttachNonStandardRevocationClientAssertionClaims.Descriptor,
AttachRevocationRequestNonStandardClientCredentials.Descriptor, AttachRevocationRequestNonStandardClientCredentials.Descriptor,
@ -424,6 +426,50 @@ public static partial class OpenIddictClientWebIntegrationHandlers
} }
} }
/// <summary>
/// Contains the logic responsible for overriding the negotiated token
/// endpoint client authentication method for the providers that require it.
/// </summary>
public sealed class OverrideTokenEndpointClientAuthenticationMethod : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireTokenRequest>()
.UseSingletonHandler<OverrideTokenEndpointClientAuthenticationMethod>()
.SetOrder(AttachTokenEndpointClientAuthenticationMethod.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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;
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for overriding the address /// Contains the logic responsible for overriding the address
/// of the token endpoint for the providers that require it. /// of the token endpoint for the providers that require it.
@ -794,102 +840,16 @@ public static partial class OpenIddictClientWebIntegrationHandlers
} }
/// <summary> /// <summary>
/// Contains the logic responsible for overriding the address /// Contains the logic responsible for overriding the userinfo retrieval for the providers that require it.
/// of the userinfo endpoint for the providers that require it.
/// </summary>
public sealed class OverrideUserInfoEndpoint : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<OverrideUserInfoEndpoint>()
.SetOrder(ResolveUserInfoEndpoint.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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;
}
}
/// <summary>
/// Contains the logic responsible for disabling the userinfo retrieval for the providers that require it.
/// </summary> /// </summary>
public sealed class DisableUserInfoRetrieval : IOpenIddictClientHandler<ProcessAuthenticationContext> public sealed class OverrideUserInfoRetrieval : IOpenIddictClientHandler<ProcessAuthenticationContext>
{ {
/// <summary> /// <summary>
/// Gets the default descriptor definition assigned to this handler. /// Gets the default descriptor definition assigned to this handler.
/// </summary> /// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; } public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>() = OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<DisableUserInfoRetrieval>() .UseSingletonHandler<OverrideUserInfoRetrieval>()
.SetOrder(EvaluateUserInfoRequest.Descriptor.Order + 250) .SetOrder(EvaluateUserInfoRequest.Descriptor.Order + 250)
.SetType(OpenIddictClientHandlerType.BuiltIn) .SetType(OpenIddictClientHandlerType.BuiltIn)
.Build(); .Build();
@ -912,6 +872,21 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// userinfo retrieval is always disabled for the ADFS provider. // userinfo retrieval is always disabled for the ADFS provider.
ProviderTypes.ActiveDirectoryFederationServices => false, 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 // 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 // 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. // a legacy format that is not supported by the Microsoft Entra ID userinfo implementation.
@ -956,17 +931,17 @@ public static partial class OpenIddictClientWebIntegrationHandlers
} }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
public sealed class DisableUserInfoValidation : IOpenIddictClientHandler<ProcessAuthenticationContext> public sealed class OverrideUserInfoValidation : IOpenIddictClientHandler<ProcessAuthenticationContext>
{ {
/// <summary> /// <summary>
/// Gets the default descriptor definition assigned to this handler. /// Gets the default descriptor definition assigned to this handler.
/// </summary> /// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; } public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>() = OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<DisableUserInfoValidation>() .UseSingletonHandler<OverrideUserInfoValidation>()
.SetOrder(DisableUserInfoRetrieval.Descriptor.Order + 250) .SetOrder(OverrideUserInfoRetrieval.Descriptor.Order + 250)
.SetType(OpenIddictClientHandlerType.BuiltIn) .SetType(OpenIddictClientHandlerType.BuiltIn)
.Build(); .Build();
@ -995,6 +970,93 @@ public static partial class OpenIddictClientWebIntegrationHandlers
} }
} }
/// <summary>
/// Contains the logic responsible for overriding the address
/// of the userinfo endpoint for the providers that require it.
/// </summary>
public sealed class OverrideUserInfoEndpoint : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireUserInfoRequest>()
.UseSingletonHandler<OverrideUserInfoEndpoint>()
.SetOrder(ResolveUserInfoEndpoint.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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;
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for attaching additional parameters /// Contains the logic responsible for attaching additional parameters
/// to the userinfo request for the providers that require it. /// to the userinfo request for the providers that require it.
@ -1864,6 +1926,44 @@ public static partial class OpenIddictClientWebIntegrationHandlers
} }
} }
/// <summary>
/// Contains the logic responsible for overriding the negotiated revocation
/// endpoint client authentication method for the providers that require it.
/// </summary>
public sealed class OverrideRevocationEndpointClientAuthenticationMethod : IOpenIddictClientHandler<ProcessRevocationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessRevocationContext>()
.AddFilter<RequireRevocationRequest>()
.UseSingletonHandler<OverrideRevocationEndpointClientAuthenticationMethod>()
.SetOrder(AttachRevocationEndpointClientAuthenticationMethod.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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;
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for adding non-standard claims to the client /// Contains the logic responsible for adding non-standard claims to the client
/// assertions used for the revocation endpoint for the providers that require it. /// assertions used for the revocation endpoint for the providers that require it.

15
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml

@ -877,6 +877,12 @@
<Setting PropertyName="Issuer" ParameterName="issuer" Type="Uri" Required="true" <Setting PropertyName="Issuer" ParameterName="issuer" Type="Uri" Required="true"
Description="The URI used to access the Keycloak identity provider (including the realm, if applicable)" /> Description="The URI used to access the Keycloak identity provider (including the realm, if applicable)" />
<Setting PropertyName="SigningCertificate" ParameterName="certificate" Type="SigningCertificate" Required="false"
Description="The X.509 signing certificate that will be used to authenticate the client when communicating with backchannel endpoints" />
<Setting PropertyName="SigningKey" ParameterName="key" Type="SigningKey" Required="false"
Description="The signing key that will be used to authenticate the client when communicating with backchannel endpoints" />
</Provider> </Provider>
<!-- <!--
@ -1116,7 +1122,10 @@
<Environment Issuer="https://login.microsoftonline.com/{settings.Tenant}/v2.0" /> <Environment Issuer="https://login.microsoftonline.com/{settings.Tenant}/v2.0" />
<Setting PropertyName="Tenant" ParameterName="tenant" Type="String" Required="false" DefaultValue="common" <Setting PropertyName="SigningCertificate" ParameterName="certificate" Type="SigningCertificate" Required="false"
Description="The X.509 signing certificate that will be used to authenticate the client when communicating with backchannel endpoints" />
<Setting PropertyName="Tenant" ParameterName="tenant" Type="String" Required="true" DefaultValue="common"
Description="The tenant used to identify the Microsoft Entra instance (by default, the common tenant is used)" /> Description="The tenant used to identify the Microsoft Entra instance (by default, the common tenant is used)" />
</Provider> </Provider>
@ -1389,8 +1398,8 @@
<Setting PropertyName="AuthenticationLevel" ParameterName="level" Type="String" Required="true" DefaultValue="eidas1" <Setting PropertyName="AuthenticationLevel" ParameterName="level" Type="String" Required="true" DefaultValue="eidas1"
Description="The level of authentication requested, sent as part of the 'acr_values' parameter (by default, 'eidas1')" /> Description="The level of authentication requested, sent as part of the 'acr_values' parameter (by default, 'eidas1')" />
<Setting PropertyName="ClientCertificate" ParameterName="certificate" Type="Certificate" Required="false" <Setting PropertyName="SigningCertificate" ParameterName="certificate" Type="SigningCertificate" Required="false"
Description="The TLS client certificate that will be used with the backchannel endpoints (while not enforced yet, its use is strongly recommended)" /> Description="The X.509 signing certificate that will be used to authenticate the client when communicating with backchannel endpoints" />
</Provider> </Provider>
<!-- <!--

2
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xsd

@ -503,8 +503,8 @@
<xs:simpleType> <xs:simpleType>
<xs:restriction base="xs:string"> <xs:restriction base="xs:string">
<xs:enumeration value="Boolean" /> <xs:enumeration value="Boolean" />
<xs:enumeration value="Certificate" />
<xs:enumeration value="EncryptionKey" /> <xs:enumeration value="EncryptionKey" />
<xs:enumeration value="SigningCertificate" />
<xs:enumeration value="SigningKey" /> <xs:enumeration value="SigningKey" />
<xs:enumeration value="String" /> <xs:enumeration value="String" />
<xs:enumeration value="Uri" /> <xs:enumeration value="Uri" />

25
src/OpenIddict.Client/OpenIddictClientBuilder.cs

@ -960,15 +960,8 @@ public sealed class OpenIddictClientBuilder
public OpenIddictClientBuilder AllowAuthorizationCodeFlow() public OpenIddictClientBuilder AllowAuthorizationCodeFlow()
=> Configure(options => => Configure(options =>
{ {
options.CodeChallengeMethods.Add(CodeChallengeMethods.Plain);
options.CodeChallengeMethods.Add(CodeChallengeMethods.Sha256);
options.GrantTypes.Add(GrantTypes.AuthorizationCode); options.GrantTypes.Add(GrantTypes.AuthorizationCode);
options.ResponseModes.Add(ResponseModes.FormPost);
options.ResponseModes.Add(ResponseModes.Fragment);
options.ResponseModes.Add(ResponseModes.Query);
options.ResponseTypes.Add(ResponseTypes.Code); options.ResponseTypes.Add(ResponseTypes.Code);
}); });
@ -1013,15 +1006,9 @@ public sealed class OpenIddictClientBuilder
public OpenIddictClientBuilder AllowHybridFlow() public OpenIddictClientBuilder AllowHybridFlow()
=> Configure(options => => Configure(options =>
{ {
options.CodeChallengeMethods.Add(CodeChallengeMethods.Plain);
options.CodeChallengeMethods.Add(CodeChallengeMethods.Sha256);
options.GrantTypes.Add(GrantTypes.AuthorizationCode); options.GrantTypes.Add(GrantTypes.AuthorizationCode);
options.GrantTypes.Add(GrantTypes.Implicit); options.GrantTypes.Add(GrantTypes.Implicit);
options.ResponseModes.Add(ResponseModes.FormPost);
options.ResponseModes.Add(ResponseModes.Fragment);
options.ResponseTypes.Add(ResponseTypes.Code + ' ' + ResponseTypes.IdToken); options.ResponseTypes.Add(ResponseTypes.Code + ' ' + ResponseTypes.IdToken);
options.ResponseTypes.Add(ResponseTypes.Code + ' ' + ResponseTypes.IdToken + ' ' + ResponseTypes.Token); options.ResponseTypes.Add(ResponseTypes.Code + ' ' + ResponseTypes.IdToken + ' ' + ResponseTypes.Token);
options.ResponseTypes.Add(ResponseTypes.Code + ' ' + ResponseTypes.Token); options.ResponseTypes.Add(ResponseTypes.Code + ' ' + ResponseTypes.Token);
@ -1039,9 +1026,6 @@ public sealed class OpenIddictClientBuilder
{ {
options.GrantTypes.Add(GrantTypes.Implicit); options.GrantTypes.Add(GrantTypes.Implicit);
options.ResponseModes.Add(ResponseModes.FormPost);
options.ResponseModes.Add(ResponseModes.Fragment);
// Note: response_type=token is not considered secure enough as it allows malicious // Note: response_type=token is not considered secure enough as it allows malicious
// actors to inject access tokens that were initially issued to a different client. // actors to inject access tokens that were initially issued to a different client.
// As such, while OpenIddict-based servers allow using response_type=token for backward // As such, while OpenIddict-based servers allow using response_type=token for backward
@ -1061,14 +1045,7 @@ public sealed class OpenIddictClientBuilder
/// </summary> /// </summary>
/// <returns>The <see cref="OpenIddictClientBuilder"/> instance.</returns> /// <returns>The <see cref="OpenIddictClientBuilder"/> instance.</returns>
public OpenIddictClientBuilder AllowNoneFlow() public OpenIddictClientBuilder AllowNoneFlow()
=> Configure(options => => Configure(options => options.ResponseTypes.Add(ResponseTypes.None));
{
options.ResponseModes.Add(ResponseModes.FormPost);
options.ResponseModes.Add(ResponseModes.Fragment);
options.ResponseModes.Add(ResponseModes.Query);
options.ResponseTypes.Add(ResponseTypes.None);
});
/// <summary> /// <summary>
/// Enables password flow support. For more information about this specific /// Enables password flow support. For more information about this specific

7
src/OpenIddict.Client/OpenIddictClientConfiguration.cs

@ -97,6 +97,13 @@ public sealed class OpenIddictClientConfiguration : IPostConfigureOptions<OpenId
registration.RegistrationId = Base64UrlEncoder.Encode(algorithm.Hash); registration.RegistrationId = Base64UrlEncoder.Encode(algorithm.Hash);
} }
// Ensure the registration identifier doesn't contain U+001E or U+001F separators as they are
// used by the System.Net.Http integration to separate properties in the HTTP client names.
else if (registration.RegistrationId.Any(static character => character is '\u001e' or '\u001f'))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0455));
}
if (registration.ConfigurationManager is null) if (registration.ConfigurationManager is null)
{ {
if (registration.Configuration is not null) if (registration.Configuration is not null)

41
src/OpenIddict.Client/OpenIddictClientEvents.cs

@ -158,6 +158,17 @@ public static partial class OpenIddictClientEvents
/// Gets or sets the URI of the external endpoint to communicate with. /// Gets or sets the URI of the external endpoint to communicate with.
/// </summary> /// </summary>
public Uri? RemoteUri { get; set; } public Uri? RemoteUri { get; set; }
/// <summary>
/// Gets or sets the client authentication method used
/// when communicating with the external endpoint, if applicable.
/// </summary>
public string? ClientAuthenticationMethod { get; set; }
/// <summary>
/// Gets or sets the token binding method used when communicating with the external endpoint, if applicable.
/// </summary>
public HashSet<string> TokenBindingMethods { get; } = new(StringComparer.Ordinal);
} }
/// <summary> /// <summary>
@ -370,11 +381,23 @@ public static partial class OpenIddictClientEvents
/// </summary> /// </summary>
public Uri? TokenEndpoint { get; set; } public Uri? TokenEndpoint { get; set; }
/// <summary>
/// Gets or sets the client authentication method used
/// when communicating with the token endpoint, if applicable.
/// </summary>
public string? TokenEndpointClientAuthenticationMethod { get; set; }
/// <summary> /// <summary>
/// Gets or sets the URI of the userinfo endpoint, if applicable. /// Gets or sets the URI of the userinfo endpoint, if applicable.
/// </summary> /// </summary>
public Uri? UserInfoEndpoint { get; set; } public Uri? UserInfoEndpoint { get; set; }
/// <summary>
/// Gets or sets the token binding methods used when
/// communicating with the userinfo endpoint, if applicable.
/// </summary>
public HashSet<string> UserInfoEndpointTokenBindingMethods { get; } = new(StringComparer.Ordinal);
/// <summary> /// <summary>
/// Gets or sets a boolean indicating whether a token request should be sent. /// Gets or sets a boolean indicating whether a token request should be sent.
/// </summary> /// </summary>
@ -1023,6 +1046,12 @@ public static partial class OpenIddictClientEvents
/// </summary> /// </summary>
public Uri? DeviceAuthorizationEndpoint { get; set; } public Uri? DeviceAuthorizationEndpoint { get; set; }
/// <summary>
/// Gets or sets the client authentication method used when
/// communicating with the device authorization endpoint, if applicable.
/// </summary>
public string? DeviceAuthorizationEndpointClientAuthenticationMethod { get; set; }
/// <summary> /// <summary>
/// Gets or sets a boolean indicating whether a state token /// Gets or sets a boolean indicating whether a state token
/// should be generated (and optionally included in the request). /// should be generated (and optionally included in the request).
@ -1258,6 +1287,12 @@ public static partial class OpenIddictClientEvents
/// </summary> /// </summary>
public Uri? IntrospectionEndpoint { get; set; } public Uri? IntrospectionEndpoint { get; set; }
/// <summary>
/// Gets or sets the client authentication method used when
/// communicating with the introspection endpoint, if applicable.
/// </summary>
public string? IntrospectionEndpointClientAuthenticationMethod { get; set; }
/// <summary> /// <summary>
/// Gets or sets the client identifier that will be used for the introspection demand. /// Gets or sets the client identifier that will be used for the introspection demand.
/// </summary> /// </summary>
@ -1385,6 +1420,12 @@ public static partial class OpenIddictClientEvents
/// </summary> /// </summary>
public Uri? RevocationEndpoint { get; set; } public Uri? RevocationEndpoint { get; set; }
/// <summary>
/// Gets or sets the client authentication method used when
/// communicating with the revocation endpoint, if applicable.
/// </summary>
public string? RevocationEndpointClientAuthenticationMethod { get; set; }
/// <summary> /// <summary>
/// Gets or sets the client identifier that will be used for the revocation demand. /// Gets or sets the client identifier that will be used for the revocation demand.
/// </summary> /// </summary>

247
src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs

@ -5,6 +5,7 @@
*/ */
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Reflection;
using System.Text.Json; using System.Text.Json;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
@ -28,6 +29,11 @@ public static partial class OpenIddictClientHandlers
ExtractDeviceAuthorizationEndpoint.Descriptor, ExtractDeviceAuthorizationEndpoint.Descriptor,
ExtractIntrospectionEndpoint.Descriptor, ExtractIntrospectionEndpoint.Descriptor,
ExtractEndSessionEndpoint.Descriptor, ExtractEndSessionEndpoint.Descriptor,
ExtractMtlsDeviceAuthorizationEndpoint.Descriptor,
ExtractMtlsIntrospectionEndpoint.Descriptor,
ExtractMtlsRevocationEndpoint.Descriptor,
ExtractMtlsTokenEndpoint.Descriptor,
ExtractMtlsUserInfoEndpoint.Descriptor,
ExtractRevocationEndpoint.Descriptor, ExtractRevocationEndpoint.Descriptor,
ExtractTokenEndpoint.Descriptor, ExtractTokenEndpoint.Descriptor,
ExtractUserInfoEndpoint.Descriptor, ExtractUserInfoEndpoint.Descriptor,
@ -37,6 +43,7 @@ public static partial class OpenIddictClientHandlers
ExtractCodeChallengeMethods.Descriptor, ExtractCodeChallengeMethods.Descriptor,
ExtractScopes.Descriptor, ExtractScopes.Descriptor,
ExtractIssuerParameterRequirement.Descriptor, ExtractIssuerParameterRequirement.Descriptor,
ExtractTlsClientCertificateAccessTokenBindingRequirement.Descriptor,
ExtractDeviceAuthorizationEndpointClientAuthenticationMethods.Descriptor, ExtractDeviceAuthorizationEndpointClientAuthenticationMethods.Descriptor,
ExtractIntrospectionEndpointClientAuthenticationMethods.Descriptor, ExtractIntrospectionEndpointClientAuthenticationMethods.Descriptor,
ExtractRevocationEndpointClientAuthenticationMethods.Descriptor, ExtractRevocationEndpointClientAuthenticationMethods.Descriptor,
@ -473,6 +480,213 @@ public static partial class OpenIddictClientHandlers
} }
} }
/// <summary>
/// Contains the logic responsible for extracting the mTLS-enabled
/// device authorization endpoint URI from the discovery document.
/// </summary>
public sealed class ExtractMtlsDeviceAuthorizationEndpoint : IOpenIddictClientHandler<HandleConfigurationResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>()
.UseSingletonHandler<ExtractMtlsDeviceAuthorizationEndpoint>()
.SetOrder(ExtractEndSessionEndpoint.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(HandleConfigurationResponseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var aliases = context.Response[Metadata.MtlsEndpointAliases]?.GetNamedParameters();
if (aliases is not { Count: > 0 })
{
return default;
}
// Note: as recommended by the specification, values present in the "mtls_endpoint_aliases" node
// that can't be recognized as OAuth 2.0 endpoints or are not valid URIs are simply ignored.
var endpoint = (string?) aliases[Metadata.DeviceAuthorizationEndpoint];
if (Uri.TryCreate(endpoint, UriKind.Absolute, out Uri? uri) && !OpenIddictHelpers.IsImplicitFileUri(uri))
{
context.Configuration.MtlsDeviceAuthorizationEndpoint = uri;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for extracting the mTLS-enabled
/// introspection endpoint URI from the discovery document.
/// </summary>
public sealed class ExtractMtlsIntrospectionEndpoint : IOpenIddictClientHandler<HandleConfigurationResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>()
.UseSingletonHandler<ExtractMtlsIntrospectionEndpoint>()
.SetOrder(ExtractMtlsDeviceAuthorizationEndpoint.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(HandleConfigurationResponseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var aliases = context.Response[Metadata.MtlsEndpointAliases]?.GetNamedParameters();
if (aliases is not { Count: > 0 })
{
return default;
}
// Note: as recommended by the specification, values present in the "mtls_endpoint_aliases" node
// that can't be recognized as OAuth 2.0 endpoints or are not valid URIs are simply ignored.
var endpoint = (string?) aliases[Metadata.IntrospectionEndpoint];
if (Uri.TryCreate(endpoint, UriKind.Absolute, out Uri? uri) && !OpenIddictHelpers.IsImplicitFileUri(uri))
{
context.Configuration.MtlsIntrospectionEndpoint = uri;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for extracting the mTLS-enabled revocation endpoint URI from the discovery document.
/// </summary>
public sealed class ExtractMtlsRevocationEndpoint : IOpenIddictClientHandler<HandleConfigurationResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>()
.UseSingletonHandler<ExtractMtlsRevocationEndpoint>()
.SetOrder(ExtractMtlsIntrospectionEndpoint.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(HandleConfigurationResponseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var aliases = context.Response[Metadata.MtlsEndpointAliases]?.GetNamedParameters();
if (aliases is not { Count: > 0 })
{
return default;
}
// Note: as recommended by the specification, values present in the "mtls_endpoint_aliases" node
// that can't be recognized as OAuth 2.0 endpoints or are not valid URIs are simply ignored.
var endpoint = (string?) aliases[Metadata.RevocationEndpoint];
if (Uri.TryCreate(endpoint, UriKind.Absolute, out Uri? uri) && !OpenIddictHelpers.IsImplicitFileUri(uri))
{
context.Configuration.MtlsRevocationEndpoint = uri;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for extracting the mTLS-enabled token endpoint URI from the discovery document.
/// </summary>
public sealed class ExtractMtlsTokenEndpoint : IOpenIddictClientHandler<HandleConfigurationResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>()
.UseSingletonHandler<ExtractMtlsTokenEndpoint>()
.SetOrder(ExtractMtlsRevocationEndpoint.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(HandleConfigurationResponseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var aliases = context.Response[Metadata.MtlsEndpointAliases]?.GetNamedParameters();
if (aliases is not { Count: > 0 })
{
return default;
}
// Note: as recommended by the specification, values present in the "mtls_endpoint_aliases" node
// that can't be recognized as OAuth 2.0 endpoints or are not valid URIs are simply ignored.
var endpoint = (string?) aliases[Metadata.TokenEndpoint];
if (Uri.TryCreate(endpoint, UriKind.Absolute, out Uri? uri) && !OpenIddictHelpers.IsImplicitFileUri(uri))
{
context.Configuration.MtlsTokenEndpoint = uri;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for extracting the mTLS-enabled userinfo endpoint URI from the discovery document.
/// </summary>
public sealed class ExtractMtlsUserInfoEndpoint : IOpenIddictClientHandler<HandleConfigurationResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>()
.UseSingletonHandler<ExtractMtlsUserInfoEndpoint>()
.SetOrder(ExtractMtlsTokenEndpoint.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(HandleConfigurationResponseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var aliases = context.Response[Metadata.MtlsEndpointAliases]?.GetNamedParameters();
if (aliases is not { Count: > 0 })
{
return default;
}
// Note: as recommended by the specification, values present in the "mtls_endpoint_aliases" node
// that can't be recognized as OAuth 2.0 endpoints or are not valid URIs are simply ignored.
var endpoint = (string?) aliases[Metadata.UserInfoEndpoint];
if (Uri.TryCreate(endpoint, UriKind.Absolute, out Uri? uri) && !OpenIddictHelpers.IsImplicitFileUri(uri))
{
context.Configuration.MtlsUserInfoEndpoint = uri;
}
return default;
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for extracting the revocation endpoint URI from the discovery document. /// Contains the logic responsible for extracting the revocation endpoint URI from the discovery document.
/// </summary> /// </summary>
@ -843,6 +1057,37 @@ public static partial class OpenIddictClientHandlers
} }
} }
/// <summary>
/// Contains the logic responsible for extracting the flag indicating whether client
/// certificate-bound access tokens are supported from the discovery document.
/// </summary>
public sealed class ExtractTlsClientCertificateAccessTokenBindingRequirement : IOpenIddictClientHandler<HandleConfigurationResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>()
.UseSingletonHandler<ExtractTlsClientCertificateAccessTokenBindingRequirement>()
.SetOrder(ExtractIssuerParameterRequirement.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(HandleConfigurationResponseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
context.Configuration.TlsClientCertificateBoundAccessTokens = (bool?)
context.Response[Metadata.TlsClientCertificateBoundAccessTokens];
return default;
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for extracting the authentication methods /// Contains the logic responsible for extracting the authentication methods
/// supported by the device authorization endpoint from the discovery document. /// supported by the device authorization endpoint from the discovery document.
@ -855,7 +1100,7 @@ public static partial class OpenIddictClientHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; } public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>() = OpenIddictClientHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>()
.UseSingletonHandler<ExtractDeviceAuthorizationEndpointClientAuthenticationMethods>() .UseSingletonHandler<ExtractDeviceAuthorizationEndpointClientAuthenticationMethods>()
.SetOrder(ExtractIssuerParameterRequirement.Descriptor.Order + 1_000) .SetOrder(ExtractTlsClientCertificateAccessTokenBindingRequirement.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn) .SetType(OpenIddictClientHandlerType.BuiltIn)
.Build(); .Build();

666
src/OpenIddict.Client/OpenIddictClientHandlers.cs

File diff suppressed because it is too large

35
src/OpenIddict.Client/OpenIddictClientOptions.cs

@ -136,10 +136,30 @@ public sealed class OpenIddictClientOptions
/// </remarks> /// </remarks>
public bool DisableWebServicesFederationClaimMapping { get; set; } public bool DisableWebServicesFederationClaimMapping { get; set; }
/// <summary>
/// Gets the OAuth 2.0 client authentication methods enabled for this application.
/// </summary>
public HashSet<string> ClientAuthenticationMethods { get; } = new(StringComparer.Ordinal)
{
// Note: client_secret_basic is deliberately not added here as it requires
// a dedicated event handler (typically provided by the HTTP integration)
// to attach the client credentials to the standard Authorization header.
//
// The System.Net.Http integration supports the client_secret_basic,
// self_signed_tls_client_auth and tls_client_auth authentication
// methods and automatically add them to this list at runtime.
OpenIddictConstants.ClientAuthenticationMethods.ClientSecretPost,
OpenIddictConstants.ClientAuthenticationMethods.PrivateKeyJwt
};
/// <summary> /// <summary>
/// Gets the OAuth 2.0 code challenge methods enabled for this application. /// Gets the OAuth 2.0 code challenge methods enabled for this application.
/// </summary> /// </summary>
public HashSet<string> CodeChallengeMethods { get; } = new(StringComparer.Ordinal); public HashSet<string> CodeChallengeMethods { get; } = new(StringComparer.Ordinal)
{
OpenIddictConstants.CodeChallengeMethods.Plain,
OpenIddictConstants.CodeChallengeMethods.Sha256
};
/// <summary> /// <summary>
/// Gets the OAuth 2.0/OpenID Connect flows enabled for this application. /// Gets the OAuth 2.0/OpenID Connect flows enabled for this application.
@ -150,7 +170,18 @@ public sealed class OpenIddictClientOptions
/// Gets the OAuth 2.0/OpenID Connect response modes enabled for this application. /// Gets the OAuth 2.0/OpenID Connect response modes enabled for this application.
/// </summary> /// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)] [EditorBrowsable(EditorBrowsableState.Advanced)]
public HashSet<string> ResponseModes { get; } = new(StringComparer.Ordinal); public HashSet<string> ResponseModes { get; } = new(StringComparer.Ordinal)
{
// Note: fragment is deliberately not added here as it typically doesn't work
// with server-based applications without offering an HTML/JS page extracting
// the parameters from the URI fragment and flowing them differently.
//
// The system integration package supports the fragment response mode in
// specific cases (e.g when using the UWP Web Authentication Broker API)
// and automatically adds fragment to this list when it is enabled.
OpenIddictConstants.ResponseModes.FormPost,
OpenIddictConstants.ResponseModes.Query
};
/// <summary> /// <summary>
/// Gets the OAuth 2.0/OpenID Connect response types enabled for this application. /// Gets the OAuth 2.0/OpenID Connect response types enabled for this application.

10
src/OpenIddict.Client/OpenIddictClientRegistration.cs

@ -63,6 +63,16 @@ public sealed class OpenIddictClientRegistration
/// </summary> /// </summary>
public List<SigningCredentials> SigningCredentials { get; } = []; public List<SigningCredentials> SigningCredentials { get; } = [];
/// <summary>
/// Gets the client authentication methods allowed by the client instance.
/// If no value is explicitly set, all the methods enabled in the client options can be used.
/// </summary>
/// <remarks>
/// The final client authentication method used in backchannel requests is chosen by OpenIddict based
/// on the client options, the server configuration and the values registered in this property.
/// </remarks>
public HashSet<string> ClientAuthenticationMethods { get; } = new(StringComparer.Ordinal);
/// <summary> /// <summary>
/// Gets the code challenge methods allowed by the client instance. /// Gets the code challenge methods allowed by the client instance.
/// If no value is explicitly set, all the methods enabled in the client options can be used. /// If no value is explicitly set, all the methods enabled in the client options can be used.

21
src/OpenIddict.Client/OpenIddictClientService.cs

@ -1616,11 +1616,12 @@ public class OpenIddictClientService
/// <param name="configuration">The server configuration.</param> /// <param name="configuration">The server configuration.</param>
/// <param name="request">The device authorization request.</param> /// <param name="request">The device authorization request.</param>
/// <param name="uri">The uri of the remote device authorization endpoint.</param> /// <param name="uri">The uri of the remote device authorization endpoint.</param>
/// <param name="method">The client authentication method, if applicable.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The token response.</returns> /// <returns>The token response.</returns>
internal async ValueTask<OpenIddictResponse> SendDeviceAuthorizationRequestAsync( internal async ValueTask<OpenIddictResponse> SendDeviceAuthorizationRequestAsync(
OpenIddictClientRegistration registration, OpenIddictConfiguration configuration, OpenIddictClientRegistration registration, OpenIddictConfiguration configuration,
OpenIddictRequest request, Uri? uri = null, CancellationToken cancellationToken = default) OpenIddictRequest request, Uri uri, string? method, CancellationToken cancellationToken = default)
{ {
if (registration is null) if (registration is null)
{ {
@ -1674,6 +1675,7 @@ public class OpenIddictClientService
var context = new PrepareDeviceAuthorizationRequestContext(transaction) var context = new PrepareDeviceAuthorizationRequestContext(transaction)
{ {
CancellationToken = cancellationToken, CancellationToken = cancellationToken,
ClientAuthenticationMethod = method,
RemoteUri = uri, RemoteUri = uri,
Configuration = configuration, Configuration = configuration,
Registration = registration, Registration = registration,
@ -1790,11 +1792,12 @@ public class OpenIddictClientService
/// <param name="configuration">The server configuration.</param> /// <param name="configuration">The server configuration.</param>
/// <param name="request">The token request.</param> /// <param name="request">The token request.</param>
/// <param name="uri">The uri of the remote token endpoint.</param> /// <param name="uri">The uri of the remote token endpoint.</param>
/// <param name="method">The client authentication method, if applicable.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The response and the principal extracted from the introspection response.</returns> /// <returns>The response and the principal extracted from the introspection response.</returns>
internal async ValueTask<(OpenIddictResponse, ClaimsPrincipal)> SendIntrospectionRequestAsync( internal async ValueTask<(OpenIddictResponse, ClaimsPrincipal)> SendIntrospectionRequestAsync(
OpenIddictClientRegistration registration, OpenIddictConfiguration configuration, OpenIddictClientRegistration registration, OpenIddictConfiguration configuration,
OpenIddictRequest request, Uri uri, CancellationToken cancellationToken = default) OpenIddictRequest request, Uri uri, string? method, CancellationToken cancellationToken = default)
{ {
if (configuration is null) if (configuration is null)
{ {
@ -1843,6 +1846,7 @@ public class OpenIddictClientService
var context = new PrepareIntrospectionRequestContext(transaction) var context = new PrepareIntrospectionRequestContext(transaction)
{ {
CancellationToken = cancellationToken, CancellationToken = cancellationToken,
ClientAuthenticationMethod = method,
Configuration = configuration, Configuration = configuration,
Registration = registration, Registration = registration,
RemoteUri = uri, RemoteUri = uri,
@ -1961,11 +1965,12 @@ public class OpenIddictClientService
/// <param name="configuration">The server configuration.</param> /// <param name="configuration">The server configuration.</param>
/// <param name="request">The token request.</param> /// <param name="request">The token request.</param>
/// <param name="uri">The uri of the remote token endpoint.</param> /// <param name="uri">The uri of the remote token endpoint.</param>
/// <param name="method">The client authentication method, if applicable.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The response extracted from the revocation response.</returns> /// <returns>The response extracted from the revocation response.</returns>
internal async ValueTask<OpenIddictResponse> SendRevocationRequestAsync( internal async ValueTask<OpenIddictResponse> SendRevocationRequestAsync(
OpenIddictClientRegistration registration, OpenIddictConfiguration configuration, OpenIddictClientRegistration registration, OpenIddictConfiguration configuration,
OpenIddictRequest request, Uri uri, CancellationToken cancellationToken = default) OpenIddictRequest request, Uri uri, string? method, CancellationToken cancellationToken = default)
{ {
if (configuration is null) if (configuration is null)
{ {
@ -2014,6 +2019,7 @@ public class OpenIddictClientService
var context = new PrepareRevocationRequestContext(transaction) var context = new PrepareRevocationRequestContext(transaction)
{ {
CancellationToken = cancellationToken, CancellationToken = cancellationToken,
ClientAuthenticationMethod = method,
Configuration = configuration, Configuration = configuration,
Registration = registration, Registration = registration,
RemoteUri = uri, RemoteUri = uri,
@ -2130,11 +2136,12 @@ public class OpenIddictClientService
/// <param name="configuration">The server configuration.</param> /// <param name="configuration">The server configuration.</param>
/// <param name="request">The token request.</param> /// <param name="request">The token request.</param>
/// <param name="uri">The uri of the remote token endpoint.</param> /// <param name="uri">The uri of the remote token endpoint.</param>
/// <param name="method">The client authentication method, if applicable.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The token response.</returns> /// <returns>The token response.</returns>
internal async ValueTask<OpenIddictResponse> SendTokenRequestAsync( internal async ValueTask<OpenIddictResponse> SendTokenRequestAsync(
OpenIddictClientRegistration registration, OpenIddictConfiguration configuration, OpenIddictClientRegistration registration, OpenIddictConfiguration configuration,
OpenIddictRequest request, Uri uri, CancellationToken cancellationToken = default) OpenIddictRequest request, Uri uri, string? method, CancellationToken cancellationToken = default)
{ {
if (registration is null) if (registration is null)
{ {
@ -2188,6 +2195,7 @@ public class OpenIddictClientService
var context = new PrepareTokenRequestContext(transaction) var context = new PrepareTokenRequestContext(transaction)
{ {
CancellationToken = cancellationToken, CancellationToken = cancellationToken,
ClientAuthenticationMethod = method,
Configuration = configuration, Configuration = configuration,
Registration = registration, Registration = registration,
RemoteUri = uri, RemoteUri = uri,
@ -2304,11 +2312,12 @@ public class OpenIddictClientService
/// <param name="configuration">The server configuration.</param> /// <param name="configuration">The server configuration.</param>
/// <param name="request">The userinfo request.</param> /// <param name="request">The userinfo request.</param>
/// <param name="uri">The uri of the remote userinfo endpoint.</param> /// <param name="uri">The uri of the remote userinfo endpoint.</param>
/// <param name="methods">The token binding methods to use, if applicable.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The response and the principal extracted from the userinfo response or the userinfo token.</returns> /// <returns>The response and the principal extracted from the userinfo response or the userinfo token.</returns>
internal async ValueTask<(OpenIddictResponse Response, (ClaimsPrincipal? Principal, string? Token))> SendUserInfoRequestAsync( internal async ValueTask<(OpenIddictResponse Response, (ClaimsPrincipal? Principal, string? Token))> SendUserInfoRequestAsync(
OpenIddictClientRegistration registration, OpenIddictConfiguration configuration, OpenIddictClientRegistration registration, OpenIddictConfiguration configuration,
OpenIddictRequest request, Uri uri, CancellationToken cancellationToken = default) OpenIddictRequest request, Uri uri, HashSet<string> methods, CancellationToken cancellationToken = default)
{ {
if (registration is null) if (registration is null)
{ {
@ -2363,6 +2372,8 @@ public class OpenIddictClientService
Request = request Request = request
}; };
context.TokenBindingMethods.UnionWith(methods);
await dispatcher.DispatchAsync(context); await dispatcher.DispatchAsync(context);
if (context.IsRejected) if (context.IsRejected)

25
src/OpenIddict.Server/OpenIddictServerBuilder.cs

@ -931,15 +931,8 @@ public sealed class OpenIddictServerBuilder
public OpenIddictServerBuilder AllowAuthorizationCodeFlow() public OpenIddictServerBuilder AllowAuthorizationCodeFlow()
=> Configure(options => => Configure(options =>
{ {
options.CodeChallengeMethods.Add(CodeChallengeMethods.Plain);
options.CodeChallengeMethods.Add(CodeChallengeMethods.Sha256);
options.GrantTypes.Add(GrantTypes.AuthorizationCode); options.GrantTypes.Add(GrantTypes.AuthorizationCode);
options.ResponseModes.Add(ResponseModes.FormPost);
options.ResponseModes.Add(ResponseModes.Fragment);
options.ResponseModes.Add(ResponseModes.Query);
options.ResponseTypes.Add(ResponseTypes.Code); options.ResponseTypes.Add(ResponseTypes.Code);
}); });
@ -984,15 +977,9 @@ public sealed class OpenIddictServerBuilder
public OpenIddictServerBuilder AllowHybridFlow() public OpenIddictServerBuilder AllowHybridFlow()
=> Configure(options => => Configure(options =>
{ {
options.CodeChallengeMethods.Add(CodeChallengeMethods.Plain);
options.CodeChallengeMethods.Add(CodeChallengeMethods.Sha256);
options.GrantTypes.Add(GrantTypes.AuthorizationCode); options.GrantTypes.Add(GrantTypes.AuthorizationCode);
options.GrantTypes.Add(GrantTypes.Implicit); options.GrantTypes.Add(GrantTypes.Implicit);
options.ResponseModes.Add(ResponseModes.FormPost);
options.ResponseModes.Add(ResponseModes.Fragment);
options.ResponseTypes.Add(ResponseTypes.Code + ' ' + ResponseTypes.IdToken); options.ResponseTypes.Add(ResponseTypes.Code + ' ' + ResponseTypes.IdToken);
options.ResponseTypes.Add(ResponseTypes.Code + ' ' + ResponseTypes.IdToken + ' ' + ResponseTypes.Token); options.ResponseTypes.Add(ResponseTypes.Code + ' ' + ResponseTypes.IdToken + ' ' + ResponseTypes.Token);
options.ResponseTypes.Add(ResponseTypes.Code + ' ' + ResponseTypes.Token); options.ResponseTypes.Add(ResponseTypes.Code + ' ' + ResponseTypes.Token);
@ -1010,9 +997,6 @@ public sealed class OpenIddictServerBuilder
{ {
options.GrantTypes.Add(GrantTypes.Implicit); options.GrantTypes.Add(GrantTypes.Implicit);
options.ResponseModes.Add(ResponseModes.FormPost);
options.ResponseModes.Add(ResponseModes.Fragment);
options.ResponseTypes.Add(ResponseTypes.IdToken); options.ResponseTypes.Add(ResponseTypes.IdToken);
options.ResponseTypes.Add(ResponseTypes.IdToken + ' ' + ResponseTypes.Token); options.ResponseTypes.Add(ResponseTypes.IdToken + ' ' + ResponseTypes.Token);
options.ResponseTypes.Add(ResponseTypes.Token); options.ResponseTypes.Add(ResponseTypes.Token);
@ -1024,14 +1008,7 @@ public sealed class OpenIddictServerBuilder
/// </summary> /// </summary>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns> /// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
public OpenIddictServerBuilder AllowNoneFlow() public OpenIddictServerBuilder AllowNoneFlow()
=> Configure(options => => Configure(options => options.ResponseTypes.Add(ResponseTypes.None));
{
options.ResponseModes.Add(ResponseModes.FormPost);
options.ResponseModes.Add(ResponseModes.Fragment);
options.ResponseModes.Add(ResponseModes.Query);
options.ResponseTypes.Add(ResponseTypes.None);
});
/// <summary> /// <summary>
/// Enables password flow support. For more information about this specific /// Enables password flow support. For more information about this specific

52
src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs

@ -34,8 +34,8 @@ public static partial class OpenIddictServerHandlers
AttachIssuer.Descriptor, AttachIssuer.Descriptor,
AttachEndpoints.Descriptor, AttachEndpoints.Descriptor,
AttachGrantTypes.Descriptor, AttachGrantTypes.Descriptor,
AttachResponseModes.Descriptor,
AttachResponseTypes.Descriptor, AttachResponseTypes.Descriptor,
AttachResponseModes.Descriptor,
AttachClientAuthenticationMethods.Descriptor, AttachClientAuthenticationMethods.Descriptor,
AttachCodeChallengeMethods.Descriptor, AttachCodeChallengeMethods.Descriptor,
AttachScopes.Descriptor, AttachScopes.Descriptor,
@ -426,16 +426,16 @@ public static partial class OpenIddictServerHandlers
} }
/// <summary> /// <summary>
/// Contains the logic responsible for attaching the supported response modes to the provider discovery document. /// Contains the logic responsible for attaching the supported response types to the provider discovery document.
/// </summary> /// </summary>
public sealed class AttachResponseModes : IOpenIddictServerHandler<HandleConfigurationRequestContext> public sealed class AttachResponseTypes : IOpenIddictServerHandler<HandleConfigurationRequestContext>
{ {
/// <summary> /// <summary>
/// Gets the default descriptor definition assigned to this handler. /// Gets the default descriptor definition assigned to this handler.
/// </summary> /// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; } public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<HandleConfigurationRequestContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<HandleConfigurationRequestContext>()
.UseSingletonHandler<AttachResponseModes>() .UseSingletonHandler<AttachResponseTypes>()
.SetOrder(AttachGrantTypes.Descriptor.Order + 1_000) .SetOrder(AttachGrantTypes.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
.Build(); .Build();
@ -448,24 +448,24 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
context.ResponseModes.UnionWith(context.Options.ResponseModes); context.ResponseTypes.UnionWith(context.Options.ResponseTypes);
return default; return default;
} }
} }
/// <summary> /// <summary>
/// Contains the logic responsible for attaching the supported response types to the provider discovery document. /// Contains the logic responsible for attaching the supported response modes to the provider discovery document.
/// </summary> /// </summary>
public sealed class AttachResponseTypes : IOpenIddictServerHandler<HandleConfigurationRequestContext> public sealed class AttachResponseModes : IOpenIddictServerHandler<HandleConfigurationRequestContext>
{ {
/// <summary> /// <summary>
/// Gets the default descriptor definition assigned to this handler. /// Gets the default descriptor definition assigned to this handler.
/// </summary> /// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; } public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<HandleConfigurationRequestContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<HandleConfigurationRequestContext>()
.UseSingletonHandler<AttachResponseTypes>() .UseSingletonHandler<AttachResponseModes>()
.SetOrder(AttachResponseModes.Descriptor.Order + 1_000) .SetOrder(AttachResponseTypes.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
.Build(); .Build();
@ -477,7 +477,26 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
context.ResponseTypes.UnionWith(context.Options.ResponseTypes); // Only include the response modes if at least one response type is returned.
if (context.ResponseTypes.Count is 0)
{
return default;
}
// Note: returning an access or identity token using the query response mode is explicitly disallowed.
//
// To ensure the query response mode is not returned unless at least one response type that doesn't
// include id_token or token is included in the server configuration, a manual check is done here.
if (context.Options.ResponseModes.Contains(ResponseModes.Query) &&
context.ResponseTypes
.Select(static types => types.Split(Separators.Space, StringSplitOptions.None).ToHashSet(StringComparer.Ordinal))
.Any(static types => !types.Contains(ResponseTypes.IdToken) && !types.Contains(ResponseTypes.Token)))
{
context.ResponseModes.Add(ResponseModes.Query);
}
context.ResponseModes.UnionWith(context.Options.ResponseModes.Where(
static mode => mode is not ResponseModes.Query));
return default; return default;
} }
@ -495,7 +514,7 @@ public static partial class OpenIddictServerHandlers
public static OpenIddictServerHandlerDescriptor Descriptor { get; } public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<HandleConfigurationRequestContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<HandleConfigurationRequestContext>()
.UseSingletonHandler<AttachClientAuthenticationMethods>() .UseSingletonHandler<AttachClientAuthenticationMethods>()
.SetOrder(AttachResponseTypes.Descriptor.Order + 1_000) .SetOrder(AttachResponseModes.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
.Build(); .Build();
@ -557,7 +576,11 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
context.CodeChallengeMethods.UnionWith(context.Options.CodeChallengeMethods); // Only include the code challenge methods if the authorization code grant type is enabled.
if (context.GrantTypes.Contains(GrantTypes.AuthorizationCode))
{
context.CodeChallengeMethods.UnionWith(context.Options.CodeChallengeMethods);
}
return default; return default;
} }
@ -739,11 +762,12 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
// Note: the optional claims/request/request_uri parameters are not yet supported // Note: these optional features are not yet supported by OpenIddict,
// by OpenIddict, so "false" is returned to encourage clients not to use them. // so "false" is returned to encourage clients not to use them.
context.Metadata[Metadata.ClaimsParameterSupported] = false; context.Metadata[Metadata.ClaimsParameterSupported] = false;
context.Metadata[Metadata.RequestParameterSupported] = false; context.Metadata[Metadata.RequestParameterSupported] = false;
context.Metadata[Metadata.RequestUriParameterSupported] = false; context.Metadata[Metadata.RequestUriParameterSupported] = false;
context.Metadata[Metadata.TlsClientCertificateBoundAccessTokens] = false;
// As of 3.2.0, OpenIddict automatically returns an "iss" parameter containing its identity as // As of 3.2.0, OpenIddict automatically returns an "iss" parameter containing its identity as
// part of authorization responses to help clients mitigate mix-up attacks. For more information, // part of authorization responses to help clients mitigate mix-up attacks. For more information,

13
src/OpenIddict.Server/OpenIddictServerOptions.cs

@ -364,7 +364,11 @@ public sealed class OpenIddictServerOptions
/// <summary> /// <summary>
/// Gets the OAuth 2.0 code challenge methods enabled for this application. /// Gets the OAuth 2.0 code challenge methods enabled for this application.
/// </summary> /// </summary>
public HashSet<string> CodeChallengeMethods { get; } = new(StringComparer.Ordinal); public HashSet<string> CodeChallengeMethods { get; } = new(StringComparer.Ordinal)
{
OpenIddictConstants.CodeChallengeMethods.Plain,
OpenIddictConstants.CodeChallengeMethods.Sha256
};
/// <summary> /// <summary>
/// Gets the OAuth 2.0/OpenID Connect flows enabled for this application. /// Gets the OAuth 2.0/OpenID Connect flows enabled for this application.
@ -389,7 +393,12 @@ public sealed class OpenIddictServerOptions
/// Gets the OAuth 2.0/OpenID Connect response modes enabled for this application. /// Gets the OAuth 2.0/OpenID Connect response modes enabled for this application.
/// </summary> /// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)] [EditorBrowsable(EditorBrowsableState.Advanced)]
public HashSet<string> ResponseModes { get; } = new(StringComparer.Ordinal); public HashSet<string> ResponseModes { get; } = new(StringComparer.Ordinal)
{
OpenIddictConstants.ResponseModes.FormPost,
OpenIddictConstants.ResponseModes.Fragment,
OpenIddictConstants.ResponseModes.Query
};
/// <summary> /// <summary>
/// Gets the OpenID Connect subject types enabled for this application. /// Gets the OpenID Connect subject types enabled for this application.

49
src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpBuilder.cs

@ -9,6 +9,7 @@ using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Net.Mail; using System.Net.Mail;
using System.Reflection; using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using OpenIddict.Validation.SystemNetHttp; using OpenIddict.Validation.SystemNetHttp;
using Polly; using Polly;
@ -227,6 +228,54 @@ public sealed class OpenIddictValidationSystemNetHttpBuilder
productVersion: assembly.GetName().Version!.ToString())); productVersion: assembly.GetName().Version!.ToString()));
} }
/// <summary>
/// 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.
/// </summary>
/// <param name="selector">The selector delegate.</param>
/// <remarks>
/// If no value is explicitly set, OpenIddict automatically tries to resolve the
/// X.509 certificate from the signing credentials attached to the validation options
/// (in this case, the X.509 certificate MUST include the digital signature and
/// client authentication key usages to be automatically selected by OpenIddict).
/// </remarks>
/// <returns>The <see cref="OpenIddictValidationSystemNetHttpBuilder"/> instance.</returns>
public OpenIddictValidationSystemNetHttpBuilder SetSelfSignedTlsClientAuthenticationCertificateSelector(
Func<X509Certificate2?> selector)
{
if (selector is null)
{
throw new ArgumentNullException(nameof(selector));
}
return Configure(options => options.SelfSignedTlsClientAuthenticationCertificateSelector = selector);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="selector">The selector delegate.</param>
/// <remarks>
/// If no value is explicitly set, OpenIddict automatically tries to resolve the
/// X.509 certificate from the signing credentials attached to the validation options
/// (in this case, the X.509 certificate MUST include the digital signature and
/// client authentication key usages to be automatically selected by OpenIddict).
/// </remarks>
/// <returns>The <see cref="OpenIddictValidationSystemNetHttpBuilder"/> instance.</returns>
public OpenIddictValidationSystemNetHttpBuilder SetTlsClientAuthenticationCertificateSelector(
Func<X509Certificate2?> selector)
{
if (selector is null)
{
throw new ArgumentNullException(nameof(selector));
}
return Configure(options => options.TlsClientAuthenticationCertificateSelector = selector);
}
/// <inheritdoc/> /// <inheritdoc/>
[EditorBrowsable(EditorBrowsableState.Never)] [EditorBrowsable(EditorBrowsableState.Never)]
public override bool Equals(object? obj) => base.Equals(obj); public override bool Equals(object? obj) => base.Equals(obj);

167
src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConfiguration.cs

@ -7,9 +7,12 @@
using System.ComponentModel; using System.ComponentModel;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http; using Microsoft.Extensions.Http;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Polly; using Polly;
#if SUPPORTS_HTTP_CLIENT_RESILIENCE #if SUPPORTS_HTTP_CLIENT_RESILIENCE
@ -24,7 +27,8 @@ namespace OpenIddict.Validation.SystemNetHttp;
[EditorBrowsable(EditorBrowsableState.Advanced)] [EditorBrowsable(EditorBrowsableState.Advanced)]
public sealed class OpenIddictValidationSystemNetHttpConfiguration : IConfigureOptions<OpenIddictValidationOptions>, public sealed class OpenIddictValidationSystemNetHttpConfiguration : IConfigureOptions<OpenIddictValidationOptions>,
IConfigureNamedOptions<HttpClientFactoryOptions>, IConfigureNamedOptions<HttpClientFactoryOptions>,
IPostConfigureOptions<HttpClientFactoryOptions> IPostConfigureOptions<HttpClientFactoryOptions>,
IPostConfigureOptions<OpenIddictValidationSystemNetHttpOptions>
{ {
private readonly IServiceProvider _provider; private readonly IServiceProvider _provider;
@ -45,6 +49,11 @@ public sealed class OpenIddictValidationSystemNetHttpConfiguration : IConfigureO
// Register the built-in event handlers used by the OpenIddict System.Net.Http validation components. // Register the built-in event handlers used by the OpenIddict System.Net.Http validation components.
options.Handlers.AddRange(OpenIddictValidationSystemNetHttpHandlers.DefaultHandlers); options.Handlers.AddRange(OpenIddictValidationSystemNetHttpHandlers.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);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -58,13 +67,28 @@ public sealed class OpenIddictValidationSystemNetHttpConfiguration : IConfigureO
throw new ArgumentNullException(nameof(options)); throw new ArgumentNullException(nameof(options));
} }
// Only amend the HTTP client factory options if the instance is managed by OpenIddict.
var assembly = typeof(OpenIddictValidationSystemNetHttpOptions).Assembly.GetName(); var assembly = typeof(OpenIddictValidationSystemNetHttpOptions).Assembly.GetName();
if (!string.Equals(name, assembly.Name, StringComparison.Ordinal))
// Only amend the HTTP client factory options if the instance is managed by OpenIddict.
if (string.IsNullOrEmpty(name) || !name.StartsWith(assembly.Name!, StringComparison.Ordinal))
{ {
return; 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]) : [];
var settings = _provider.GetRequiredService<IOptionsMonitor<OpenIddictValidationSystemNetHttpOptions>>().CurrentValue; var settings = _provider.GetRequiredService<IOptionsMonitor<OpenIddictValidationSystemNetHttpOptions>>().CurrentValue;
options.HttpClientActions.Add(static client => options.HttpClientActions.Add(static client =>
@ -106,6 +130,32 @@ public sealed class OpenIddictValidationSystemNetHttpConfiguration : IConfigureO
#pragma warning restore EXTEXP0001 #pragma warning restore EXTEXP0001
} }
#endif #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();
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();
if (certificate is not null)
{
handler.ClientCertificates.Add(certificate);
}
}
}); });
// Register the user-defined HTTP client handler actions. // Register the user-defined HTTP client handler actions.
@ -124,13 +174,28 @@ public sealed class OpenIddictValidationSystemNetHttpConfiguration : IConfigureO
throw new ArgumentNullException(nameof(options)); throw new ArgumentNullException(nameof(options));
} }
// Only amend the HTTP client factory options if the instance is managed by OpenIddict.
var assembly = typeof(OpenIddictValidationSystemNetHttpOptions).Assembly.GetName(); var assembly = typeof(OpenIddictValidationSystemNetHttpOptions).Assembly.GetName();
if (!string.Equals(name, assembly.Name, StringComparison.Ordinal))
// Only amend the HTTP client factory options if the instance is managed by OpenIddict.
if (string.IsNullOrEmpty(name) || !name.StartsWith(assembly.Name!, StringComparison.Ordinal))
{ {
return; 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]) : [];
options.HttpMessageHandlerBuilderActions.Insert(0, static builder => options.HttpMessageHandlerBuilderActions.Insert(0, static builder =>
{ {
// Note: Microsoft.Extensions.Http 9.0+ no longer uses HttpClientHandler as the default instance // Note: Microsoft.Extensions.Http 9.0+ no longer uses HttpClientHandler as the default instance
@ -186,4 +251,96 @@ public sealed class OpenIddictValidationSystemNetHttpConfiguration : IConfigureO
handler.UseCookies = false; handler.UseCookies = false;
}); });
} }
public void PostConfigure(string? name, OpenIddictValidationSystemNetHttpOptions options)
{
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 ??= () =>
{
foreach (var credentials in _provider.GetRequiredService<IOptionsMonitor<OpenIddictValidationOptions>>()
.CurrentValue.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 ??= () =>
{
foreach (var credentials in _provider.GetRequiredService<IOptionsMonitor<OpenIddictValidationOptions>>()
.CurrentValue.SigningCredentials)
{
if (credentials.Key is X509SecurityKey { Certificate: X509Certificate2 certificate } &&
certificate.Version is >= 3 && !IsSelfIssuedCertificate(certificate) &&
HasDigitalSignatureKeyUsage(certificate) &&
HasClientAuthenticationExtendedKeyUsage(certificate))
{
return certificate;
}
}
return null;
};
static bool HasClientAuthenticationExtendedKeyUsage(X509Certificate2 certificate)
{
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;
}
// 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);
}
} }

3
src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpExtensions.cs

@ -49,6 +49,9 @@ public static class OpenIddictValidationSystemNetHttpExtensions
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
IPostConfigureOptions<HttpClientFactoryOptions>, OpenIddictValidationSystemNetHttpConfiguration>()); IPostConfigureOptions<HttpClientFactoryOptions>, OpenIddictValidationSystemNetHttpConfiguration>());
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
IPostConfigureOptions<OpenIddictValidationSystemNetHttpOptions>, OpenIddictValidationSystemNetHttpConfiguration>());
return new OpenIddictValidationSystemNetHttpBuilder(builder.Services); return new OpenIddictValidationSystemNetHttpBuilder(builder.Services);
} }

95
src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Introspection.cs

@ -5,10 +5,6 @@
*/ */
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Diagnostics;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
namespace OpenIddict.Validation.SystemNetHttp; namespace OpenIddict.Validation.SystemNetHttp;
@ -26,7 +22,7 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers
AttachJsonAcceptHeaders<PrepareIntrospectionRequestContext>.Descriptor, AttachJsonAcceptHeaders<PrepareIntrospectionRequestContext>.Descriptor,
AttachUserAgentHeader<PrepareIntrospectionRequestContext>.Descriptor, AttachUserAgentHeader<PrepareIntrospectionRequestContext>.Descriptor,
AttachFromHeader<PrepareIntrospectionRequestContext>.Descriptor, AttachFromHeader<PrepareIntrospectionRequestContext>.Descriptor,
AttachBasicAuthenticationCredentials.Descriptor, AttachBasicAuthenticationCredentials<PrepareIntrospectionRequestContext>.Descriptor,
AttachHttpParameters<PrepareIntrospectionRequestContext>.Descriptor, AttachHttpParameters<PrepareIntrospectionRequestContext>.Descriptor,
SendHttpRequest<ApplyIntrospectionRequestContext>.Descriptor, SendHttpRequest<ApplyIntrospectionRequestContext>.Descriptor,
DisposeHttpRequest<ApplyIntrospectionRequestContext>.Descriptor, DisposeHttpRequest<ApplyIntrospectionRequestContext>.Descriptor,
@ -40,94 +36,5 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers
ValidateHttpResponse<ExtractIntrospectionResponseContext>.Descriptor, ValidateHttpResponse<ExtractIntrospectionResponseContext>.Descriptor,
DisposeHttpResponse<ExtractIntrospectionResponseContext>.Descriptor DisposeHttpResponse<ExtractIntrospectionResponseContext>.Descriptor
]); ]);
/// <summary>
/// Contains the logic responsible for attaching the client credentials to the HTTP Authorization header.
/// </summary>
public sealed class AttachBasicAuthenticationCredentials : IOpenIddictValidationHandler<PrepareIntrospectionRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<PrepareIntrospectionRequestContext>()
.AddFilter<RequireHttpUri>()
.UseSingletonHandler<AttachBasicAuthenticationCredentials>()
.SetOrder(AttachHttpParameters<PrepareIntrospectionRequestContext>.Descriptor.Order - 500)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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", "+");
}
}
} }
} }

221
src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs

@ -11,9 +11,11 @@ using System.IO.Compression;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Extensions; using OpenIddict.Extensions;
using static OpenIddict.Validation.SystemNetHttp.OpenIddictValidationSystemNetHttpConstants; using static OpenIddict.Validation.SystemNetHttp.OpenIddictValidationSystemNetHttpConstants;
@ -22,8 +24,131 @@ namespace OpenIddict.Validation.SystemNetHttp;
[EditorBrowsable(EditorBrowsableState.Never)] [EditorBrowsable(EditorBrowsableState.Never)]
public static partial class OpenIddictValidationSystemNetHttpHandlers public static partial class OpenIddictValidationSystemNetHttpHandlers
{ {
public static ImmutableArray<OpenIddictValidationHandlerDescriptor> DefaultHandlers { get; } public static ImmutableArray<OpenIddictValidationHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create([
= ImmutableArray.Create([.. Discovery.DefaultHandlers, .. Introspection.DefaultHandlers]); /*
* Authentication processing:
*/
AttachNonDefaultIntrospectionEndpointClientAuthenticationMethod.Descriptor,
.. Discovery.DefaultHandlers,
.. Introspection.DefaultHandlers
]);
/// <summary>
/// Contains the logic responsible for negotiating the best introspection endpoint client
/// authentication method supported by both the client and the authorization server.
/// </summary>
public sealed class AttachNonDefaultIntrospectionEndpointClientAuthenticationMethod : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
private readonly IOptionsMonitor<OpenIddictValidationSystemNetHttpOptions> _options;
public AttachNonDefaultIntrospectionEndpointClientAuthenticationMethod(
IOptionsMonitor<OpenIddictValidationSystemNetHttpOptions> options)
=> _options = options ?? throw new ArgumentNullException(nameof(options));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<AttachNonDefaultIntrospectionEndpointClientAuthenticationMethod>()
.SetOrder(AttachIntrospectionEndpointClientAuthenticationMethod.Descriptor.Order - 500)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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.IntrospectionEndpointClientAuthenticationMethod))
{
return default;
}
context.IntrospectionEndpointClientAuthenticationMethod = (
// Note: if client authentication methods are explicitly listed in the validation options, 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.Options.ClientAuthenticationMethods,
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() 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() is not null
=> ClientAuthenticationMethods.SelfSignedTlsClientAuth,
// If at least one asymmetric signing key was attached to the validation options
// 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.Options.SigningCredentials.Exists(static credentials => credentials.Key is AsymmetricSecurityKey)
=> ClientAuthenticationMethods.PrivateKeyJwt,
// If a client secret was attached to the validation options 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.Options.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.Options.ClientSecret) &&
client.Contains(ClientAuthenticationMethods.ClientSecretBasic) &&
server.Contains(ClientAuthenticationMethods.ClientSecretBasic)
=> ClientAuthenticationMethods.ClientSecretBasic,
({ Count: > 0 } client, { Count: 0 }) when !string.IsNullOrEmpty(context.Options.ClientSecret) &&
client.Contains(ClientAuthenticationMethods.ClientSecretBasic)
=> ClientAuthenticationMethods.ClientSecretBasic,
_ => null
};
return default;
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for creating and attaching a <see cref="HttpClient"/>. /// Contains the logic responsible for creating and attaching a <see cref="HttpClient"/>.
@ -54,10 +179,41 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
var assembly = typeof(OpenIddictValidationSystemNetHttpOptions).Assembly.GetName(); // 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(OpenIddictValidationSystemNetHttpOptions).Assembly.GetName().Name);
// Attach a flag indicating that a client certificate should be used in the TLS handshake.
if (context.ClientAuthenticationMethod is ClientAuthenticationMethods.TlsClientAuth)
{
builder.Append(':');
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)
{
builder.Append(':');
builder.Append("AttachSelfSignedTlsClientCertificate")
.Append('\u001e')
.Append(bool.TrueString);
}
// Create and store the HttpClient in the transaction properties. // Create and store the HttpClient in the transaction properties.
context.Transaction.SetProperty(typeof(HttpClient).FullName!, _factory.CreateClient(assembly.Name!) ?? context.Transaction.SetProperty(typeof(HttpClient).FullName!, _factory.CreateClient(builder.ToString()) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0174))); throw new InvalidOperationException(SR.GetResourceString(SR.ID0174)));
return default; return default;
@ -313,6 +469,63 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers
} }
} }
/// <summary>
/// Contains the logic responsible for attaching the client credentials to the HTTP Authorization header.
/// </summary>
public sealed class AttachBasicAuthenticationCredentials<TContext> : IOpenIddictValidationHandler<TContext> where TContext : BaseExternalContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpUri>()
.UseSingletonHandler<AttachBasicAuthenticationCredentials<TContext>>()
.SetOrder(AttachHttpParameters<TContext>.Descriptor.Order - 500)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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;
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for attaching the parameters to the HTTP request. /// Contains the logic responsible for attaching the parameters to the HTTP request.
/// </summary> /// </summary>

30
src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpOptions.cs

@ -4,10 +4,12 @@
* the license and the contributors participating to this project. * the license and the contributors participating to this project.
*/ */
using System.ComponentModel;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Net.Mail; using System.Net.Mail;
using System.Security.Cryptography.X509Certificates;
using Polly; using Polly;
using Polly.Extensions.Http; using Polly.Extensions.Http;
@ -80,4 +82,32 @@ public sealed class OpenIddictValidationSystemNetHttpOptions
/// instances created by the OpenIddict validation/System.Net.Http integration. /// instances created by the OpenIddict validation/System.Net.Http integration.
/// </summary> /// </summary>
public List<Action<HttpClientHandler>> HttpClientHandlerActions { get; } = []; public List<Action<HttpClientHandler>> HttpClientHandlerActions { get; } = [];
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// If no value is explicitly set, OpenIddict automatically tries to resolve the
/// X.509 certificate from the signing credentials attached to the validation options
/// (in this case, the X.509 certificate MUST include the digital signature and
/// client authentication key usages to be automatically selected by OpenIddict).
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public Func<X509Certificate2?> SelfSignedTlsClientAuthenticationCertificateSelector { get; set; } = default!;
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// If no value is explicitly set, OpenIddict automatically tries to resolve the
/// X.509 certificate from the signing credentials attached to the validation options
/// (in this case, the X.509 certificate MUST include the digital signature and
/// client authentication key usages to be automatically selected by OpenIddict).
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public Func<X509Certificate2?> TlsClientAuthenticationCertificateSelector { get; set; } = default!;
} }

12
src/OpenIddict.Validation/OpenIddictValidationEvents.cs

@ -148,6 +148,12 @@ public static partial class OpenIddictValidationEvents
/// Gets or sets the URI of the external endpoint to communicate with. /// Gets or sets the URI of the external endpoint to communicate with.
/// </summary> /// </summary>
public Uri? RemoteUri { get; set; } public Uri? RemoteUri { get; set; }
/// <summary>
/// Gets or sets the client authentication method used
/// when communicating with the external endpoint, if applicable.
/// </summary>
public string? ClientAuthenticationMethod { get; set; }
} }
/// <summary> /// <summary>
@ -293,6 +299,12 @@ public static partial class OpenIddictValidationEvents
/// </summary> /// </summary>
public Uri? IntrospectionEndpoint { get; set; } public Uri? IntrospectionEndpoint { get; set; }
/// <summary>
/// Gets or sets the client authentication method used when
/// communicating with the introspection endpoint, if applicable.
/// </summary>
public string? IntrospectionEndpointClientAuthenticationMethod { get; set; }
/// <summary> /// <summary>
/// Gets or sets a boolean indicating whether an introspection request should be sent. /// Gets or sets a boolean indicating whether an introspection request should be sent.
/// </summary> /// </summary>

45
src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs

@ -25,6 +25,7 @@ public static partial class OpenIddictValidationHandlers
ValidateIssuer.Descriptor, ValidateIssuer.Descriptor,
ExtractJsonWebKeySetEndpoint.Descriptor, ExtractJsonWebKeySetEndpoint.Descriptor,
ExtractIntrospectionEndpoint.Descriptor, ExtractIntrospectionEndpoint.Descriptor,
ExtractMtlsIntrospectionEndpoint.Descriptor,
ExtractIntrospectionEndpointClientAuthenticationMethods.Descriptor, ExtractIntrospectionEndpointClientAuthenticationMethods.Descriptor,
/* /*
@ -305,6 +306,48 @@ public static partial class OpenIddictValidationHandlers
} }
} }
/// <summary>
/// Contains the logic responsible for extracting the mTLS-enabled
/// introspection endpoint URI from the discovery document.
/// </summary>
public sealed class ExtractMtlsIntrospectionEndpoint : IOpenIddictValidationHandler<HandleConfigurationResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>()
.UseSingletonHandler<ExtractMtlsIntrospectionEndpoint>()
.SetOrder(ExtractIntrospectionEndpoint.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(HandleConfigurationResponseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var aliases = context.Response[Metadata.MtlsEndpointAliases]?.GetNamedParameters();
if (aliases is not { Count: > 0 })
{
return default;
}
// Note: as recommended by the specification, values present in the "mtls_endpoint_aliases" node
// that can't be recognized as OAuth 2.0 endpoints or are not valid URIs are simply ignored.
var endpoint = (string?) aliases[Metadata.IntrospectionEndpoint];
if (Uri.TryCreate(endpoint, UriKind.Absolute, out Uri? uri) && !OpenIddictHelpers.IsImplicitFileUri(uri))
{
context.Configuration.MtlsIntrospectionEndpoint = uri;
}
return default;
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for extracting the authentication methods /// Contains the logic responsible for extracting the authentication methods
/// supported by the introspection endpoint from the discovery document. /// supported by the introspection endpoint from the discovery document.
@ -317,7 +360,7 @@ public static partial class OpenIddictValidationHandlers
public static OpenIddictValidationHandlerDescriptor Descriptor { get; } public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>() = OpenIddictValidationHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>()
.UseSingletonHandler<ExtractIntrospectionEndpointClientAuthenticationMethods>() .UseSingletonHandler<ExtractIntrospectionEndpointClientAuthenticationMethods>()
.SetOrder(ExtractIntrospectionEndpoint.Descriptor.Order + 1_000) .SetOrder(ExtractMtlsIntrospectionEndpoint.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn) .SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build(); .Build();

119
src/OpenIddict.Validation/OpenIddictValidationHandlers.cs

@ -25,8 +25,9 @@ public static partial class OpenIddictValidationHandlers
EvaluateValidatedTokens.Descriptor, EvaluateValidatedTokens.Descriptor,
ValidateRequiredTokens.Descriptor, ValidateRequiredTokens.Descriptor,
ResolveServerConfiguration.Descriptor, ResolveServerConfiguration.Descriptor,
ResolveIntrospectionEndpoint.Descriptor,
EvaluateIntrospectionRequest.Descriptor, EvaluateIntrospectionRequest.Descriptor,
AttachIntrospectionEndpointClientAuthenticationMethod.Descriptor,
ResolveIntrospectionEndpoint.Descriptor,
AttachIntrospectionRequestParameters.Descriptor, AttachIntrospectionRequestParameters.Descriptor,
EvaluateGeneratedClientAssertion.Descriptor, EvaluateGeneratedClientAssertion.Descriptor,
PrepareClientAssertionPrincipal.Descriptor, PrepareClientAssertionPrincipal.Descriptor,
@ -189,16 +190,16 @@ public static partial class OpenIddictValidationHandlers
} }
/// <summary> /// <summary>
/// Contains the logic responsible for resolving the URI of the introspection endpoint. /// Contains the logic responsible for determining whether an introspection request should be sent.
/// </summary> /// </summary>
public sealed class ResolveIntrospectionEndpoint : IOpenIddictValidationHandler<ProcessAuthenticationContext> public sealed class EvaluateIntrospectionRequest : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{ {
/// <summary> /// <summary>
/// Gets the default descriptor definition assigned to this handler. /// Gets the default descriptor definition assigned to this handler.
/// </summary> /// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; } public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>() = OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<ResolveIntrospectionEndpoint>() .UseSingletonHandler<EvaluateIntrospectionRequest>()
.SetOrder(ResolveServerConfiguration.Descriptor.Order + 1_000) .SetOrder(ResolveServerConfiguration.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn) .SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build(); .Build();
@ -211,11 +212,63 @@ public static partial class OpenIddictValidationHandlers
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
// If the URI of the introspection endpoint wasn't explicitly set context.SendIntrospectionRequest = context.Options.ValidationType is OpenIddictValidationType.Introspection;
// at this stage, try to extract it from the server configuration.
context.IntrospectionEndpoint ??= context.Configuration.IntrospectionEndpoint switch return default;
}
}
/// <summary>
/// Contains the logic responsible for negotiating the best introspection endpoint client
/// authentication method supported by both the client and the authorization server.
/// </summary>
public sealed class AttachIntrospectionEndpointClientAuthenticationMethod : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireIntrospectionRequest>()
.UseSingletonHandler<AttachIntrospectionEndpointClientAuthenticationMethod>()
.SetOrder(EvaluateIntrospectionRequest.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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.IntrospectionEndpointClientAuthenticationMethod))
{ {
{ IsAbsoluteUri: true } uri when !OpenIddictHelpers.IsImplicitFileUri(uri) => uri, return default;
}
context.IntrospectionEndpointClientAuthenticationMethod = (
// Note: if client authentication methods are explicitly listed in the validation options, 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.Options.ClientAuthenticationMethods,
Server: context.Configuration.IntrospectionEndpointAuthMethodsSupported) switch
{
// If at least one signing key was attached to the validation options and both
// the client and the server explicitly support private_key_jwt, always prefer it.
({ Count: > 0 } client, { Count: > 0 } server) when context.Options.SigningCredentials.Count is not 0 &&
client.Contains(ClientAuthenticationMethods.PrivateKeyJwt) &&
server.Contains(ClientAuthenticationMethods.PrivateKeyJwt)
=> ClientAuthenticationMethods.PrivateKeyJwt,
// If a client secret was attached to the validation options 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.Options.ClientSecret) &&
client.Contains(ClientAuthenticationMethods.ClientSecretPost) &&
server.Contains(ClientAuthenticationMethods.ClientSecretPost)
=> ClientAuthenticationMethods.ClientSecretPost,
_ => null _ => null
}; };
@ -225,17 +278,18 @@ public static partial class OpenIddictValidationHandlers
} }
/// <summary> /// <summary>
/// Contains the logic responsible for determining whether an introspection request should be sent. /// Contains the logic responsible for resolving the URI of the introspection endpoint.
/// </summary> /// </summary>
public sealed class EvaluateIntrospectionRequest : IOpenIddictValidationHandler<ProcessAuthenticationContext> public sealed class ResolveIntrospectionEndpoint : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{ {
/// <summary> /// <summary>
/// Gets the default descriptor definition assigned to this handler. /// Gets the default descriptor definition assigned to this handler.
/// </summary> /// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; } public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>() = OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<EvaluateIntrospectionRequest>() .AddFilter<RequireIntrospectionRequest>()
.SetOrder(ResolveIntrospectionEndpoint.Descriptor.Order + 1_000) .UseSingletonHandler<ResolveIntrospectionEndpoint>()
.SetOrder(AttachIntrospectionEndpointClientAuthenticationMethod.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn) .SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build(); .Build();
@ -247,7 +301,22 @@ public static partial class OpenIddictValidationHandlers
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
context.SendIntrospectionRequest = context.Options.ValidationType is OpenIddictValidationType.Introspection; // If the URI of the introspection endpoint endpoint wasn't explicitly
// set at this stage, try to extract it from the server configuration.
context.IntrospectionEndpoint ??= context.IntrospectionEndpointClientAuthenticationMethod switch
{
// When TLS client certificate authentication was negotiated,
// always favor the mTLS-specific endpoint if available.
ClientAuthenticationMethods.SelfSignedTlsClientAuth or ClientAuthenticationMethods.TlsClientAuth
when context.Configuration.MtlsIntrospectionEndpoint is { IsAbsoluteUri: true } uri &&
!OpenIddictHelpers.IsImplicitFileUri(uri) => uri,
// Otherwise, use the non-mTLS-specific endpoint.
_ when context.Configuration.IntrospectionEndpoint is { IsAbsoluteUri: true } uri &&
!OpenIddictHelpers.IsImplicitFileUri(uri) => uri,
_ => null
};
return default; return default;
} }
@ -265,7 +334,7 @@ public static partial class OpenIddictValidationHandlers
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>() = OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireIntrospectionRequest>() .AddFilter<RequireIntrospectionRequest>()
.UseSingletonHandler<AttachIntrospectionRequestParameters>() .UseSingletonHandler<AttachIntrospectionRequestParameters>()
.SetOrder(EvaluateIntrospectionRequest.Descriptor.Order + 1_000) .SetOrder(ResolveIntrospectionEndpoint.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn) .SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build(); .Build();
@ -313,13 +382,11 @@ public static partial class OpenIddictValidationHandlers
} }
(context.GenerateClientAssertion, (context.GenerateClientAssertion,
context.IncludeClientAssertion) = context.Options.SigningCredentials.Count switch context.IncludeClientAssertion) = context.IntrospectionEndpointClientAuthenticationMethod switch
{ {
// If a introspection request is going to be sent and if at least one signing key // If the private_key_jwt client authentication method could be negotiated,
// was attached to the validation options, generate and include a client assertion // generate a client assertion that will be used to authenticate the client.
// token if the configuration indicates the server supports private_key_jwt. ClientAuthenticationMethods.PrivateKeyJwt => (true, true),
> 0 when context.Configuration.IntrospectionEndpointAuthMethodsSupported.Contains(
ClientAuthenticationMethods.PrivateKeyJwt) => (true, true),
_ => (false, false) _ => (false, false)
}; };
@ -490,7 +557,7 @@ public static partial class OpenIddictValidationHandlers
Debug.Assert(context.IntrospectionRequest is not null, SR.GetResourceString(SR.ID4008)); Debug.Assert(context.IntrospectionRequest is not null, SR.GetResourceString(SR.ID4008));
// Always attach the client_id to the request, even if an assertion is sent. // Always attach the client_id to the request, even if an assertion is sent or mTLS is used.
context.IntrospectionRequest.ClientId = context.Options.ClientId; context.IntrospectionRequest.ClientId = context.Options.ClientId;
// Note: client authentication methods are mutually exclusive so the client_assertion // Note: client authentication methods are mutually exclusive so the client_assertion
@ -502,9 +569,9 @@ public static partial class OpenIddictValidationHandlers
context.IntrospectionRequest.ClientAssertionType = context.ClientAssertionType; context.IntrospectionRequest.ClientAssertionType = context.ClientAssertionType;
} }
// Note: the client_secret may be null at this point (e.g for a public else if (context.IntrospectionEndpointClientAuthenticationMethod is
// client or if a custom authentication method is used by the application). ClientAuthenticationMethods.ClientSecretBasic or
else ClientAuthenticationMethods.ClientSecretPost)
{ {
context.IntrospectionRequest.ClientSecret = context.Options.ClientSecret; context.IntrospectionRequest.ClientSecret = context.Options.ClientSecret;
} }
@ -555,8 +622,8 @@ public static partial class OpenIddictValidationHandlers
{ {
(context.IntrospectionResponse, context.AccessTokenPrincipal) = (context.IntrospectionResponse, context.AccessTokenPrincipal) =
await _service.SendIntrospectionRequestAsync( await _service.SendIntrospectionRequestAsync(
context.Configuration, context.IntrospectionRequest, context.Configuration, context.IntrospectionRequest, context.IntrospectionEndpoint,
context.IntrospectionEndpoint, context.CancellationToken); context.IntrospectionEndpointClientAuthenticationMethod, context.CancellationToken);
} }
catch (ProtocolException exception) catch (ProtocolException exception)

16
src/OpenIddict.Validation/OpenIddictValidationOptions.cs

@ -131,6 +131,22 @@ public sealed class OpenIddictValidationOptions
/// </summary> /// </summary>
public HashSet<string> Audiences { get; } = new(StringComparer.Ordinal); public HashSet<string> Audiences { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets the OAuth 2.0 client authentication methods enabled for this application.
/// </summary>
public HashSet<string> ClientAuthenticationMethods { get; } = new(StringComparer.Ordinal)
{
// Note: client_secret_basic is deliberately not added here as it requires
// a dedicated event handler (typically provided by the HTTP integration)
// to attach the client credentials to the standard Authorization header.
//
// The System.Net.Http integration supports the client_secret_basic,
// self_signed_tls_client_auth and tls_client_auth authentication
// methods and automatically add them to this list at runtime.
OpenIddictConstants.ClientAuthenticationMethods.ClientSecretPost,
OpenIddictConstants.ClientAuthenticationMethods.PrivateKeyJwt
};
/// <summary> /// <summary>
/// Gets the token validation parameters used by the OpenIddict validation services. /// Gets the token validation parameters used by the OpenIddict validation services.
/// </summary> /// </summary>

4
src/OpenIddict.Validation/OpenIddictValidationService.cs

@ -381,11 +381,12 @@ public class OpenIddictValidationService
/// <param name="configuration">The server configuration.</param> /// <param name="configuration">The server configuration.</param>
/// <param name="request">The token request.</param> /// <param name="request">The token request.</param>
/// <param name="uri">The uri of the remote token endpoint.</param> /// <param name="uri">The uri of the remote token endpoint.</param>
/// <param name="method">The client authentication method, if applicable.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The response and the principal extracted from the introspection response.</returns> /// <returns>The response and the principal extracted from the introspection response.</returns>
internal async ValueTask<(OpenIddictResponse, ClaimsPrincipal)> SendIntrospectionRequestAsync( internal async ValueTask<(OpenIddictResponse, ClaimsPrincipal)> SendIntrospectionRequestAsync(
OpenIddictConfiguration configuration, OpenIddictRequest request, OpenIddictConfiguration configuration, OpenIddictRequest request,
Uri? uri = null, CancellationToken cancellationToken = default) Uri uri, string? method, CancellationToken cancellationToken = default)
{ {
if (configuration is null) if (configuration is null)
{ {
@ -434,6 +435,7 @@ public class OpenIddictValidationService
var context = new PrepareIntrospectionRequestContext(transaction) var context = new PrepareIntrospectionRequestContext(transaction)
{ {
CancellationToken = cancellationToken, CancellationToken = cancellationToken,
ClientAuthenticationMethod = method,
RemoteUri = uri, RemoteUri = uri,
Configuration = configuration, Configuration = configuration,
Request = request Request = request

14
test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs

@ -496,14 +496,8 @@ public class OpenIddictServerBuilderTests
var options = GetOptions(services); var options = GetOptions(services);
// Assert // Assert
Assert.Contains(CodeChallengeMethods.Sha256, options.CodeChallengeMethods);
Assert.Contains(GrantTypes.AuthorizationCode, options.GrantTypes); Assert.Contains(GrantTypes.AuthorizationCode, options.GrantTypes);
Assert.Contains(ResponseModes.FormPost, options.ResponseModes);
Assert.Contains(ResponseModes.Fragment, options.ResponseModes);
Assert.Contains(ResponseModes.Query, options.ResponseModes);
Assert.Contains(ResponseTypes.Code, options.ResponseTypes); Assert.Contains(ResponseTypes.Code, options.ResponseTypes);
} }
@ -584,14 +578,9 @@ public class OpenIddictServerBuilderTests
var options = GetOptions(services); var options = GetOptions(services);
// Assert // Assert
Assert.Contains(CodeChallengeMethods.Sha256, options.CodeChallengeMethods);
Assert.Contains(GrantTypes.AuthorizationCode, options.GrantTypes); Assert.Contains(GrantTypes.AuthorizationCode, options.GrantTypes);
Assert.Contains(GrantTypes.Implicit, options.GrantTypes); Assert.Contains(GrantTypes.Implicit, options.GrantTypes);
Assert.Contains(ResponseModes.FormPost, options.ResponseModes);
Assert.Contains(ResponseModes.Fragment, options.ResponseModes);
Assert.Contains(ResponseTypes.Code + ' ' + ResponseTypes.IdToken, options.ResponseTypes); Assert.Contains(ResponseTypes.Code + ' ' + ResponseTypes.IdToken, options.ResponseTypes);
Assert.Contains(ResponseTypes.Code + ' ' + ResponseTypes.IdToken + ' ' + ResponseTypes.Token, options.ResponseTypes); Assert.Contains(ResponseTypes.Code + ' ' + ResponseTypes.IdToken + ' ' + ResponseTypes.Token, options.ResponseTypes);
Assert.Contains(ResponseTypes.Code + ' ' + ResponseTypes.Token, options.ResponseTypes); Assert.Contains(ResponseTypes.Code + ' ' + ResponseTypes.Token, options.ResponseTypes);
@ -612,9 +601,6 @@ public class OpenIddictServerBuilderTests
// Assert // Assert
Assert.Contains(GrantTypes.Implicit, options.GrantTypes); Assert.Contains(GrantTypes.Implicit, options.GrantTypes);
Assert.Contains(ResponseModes.FormPost, options.ResponseModes);
Assert.Contains(ResponseModes.Fragment, options.ResponseModes);
Assert.Contains(ResponseTypes.IdToken, options.ResponseTypes); Assert.Contains(ResponseTypes.IdToken, options.ResponseTypes);
Assert.Contains(ResponseTypes.IdToken + ' ' + ResponseTypes.Token, options.ResponseTypes); Assert.Contains(ResponseTypes.IdToken + ' ' + ResponseTypes.Token, options.ResponseTypes);
Assert.Contains(ResponseTypes.Token, options.ResponseTypes); Assert.Contains(ResponseTypes.Token, options.ResponseTypes);

Loading…
Cancel
Save