diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Discovery.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Discovery.cs index e3ff9149..ab7cfee6 100644 --- a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Discovery.cs +++ b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Discovery.cs @@ -18,6 +18,7 @@ namespace OpenIddict.Validation.SystemNetHttp * Configuration request processing: */ PrepareGetHttpRequest.Descriptor, + AttachQueryStringParameters.Descriptor, SendHttpRequest.Descriptor, /* @@ -29,6 +30,7 @@ namespace OpenIddict.Validation.SystemNetHttp * Cryptography request processing: */ PrepareGetHttpRequest.Descriptor, + AttachQueryStringParameters.Descriptor, SendHttpRequest.Descriptor, /* diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Introspection.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Introspection.cs index 1448227c..30fcf278 100644 --- a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Introspection.cs +++ b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Introspection.cs @@ -4,8 +4,16 @@ * the license and the contributors participating to this project. */ +using System; using System.Collections.Immutable; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using JetBrains.Annotations; +using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Validation.OpenIddictValidationEvents; +using static OpenIddict.Validation.SystemNetHttp.OpenIddictValidationSystemNetHttpHandlerFilters; namespace OpenIddict.Validation.SystemNetHttp { @@ -18,12 +26,86 @@ namespace OpenIddict.Validation.SystemNetHttp * Introspection request processing: */ PreparePostHttpRequest.Descriptor, + AttachBasicAuthenticationCredentials.Descriptor, + AttachFormParameters.Descriptor, SendHttpRequest.Descriptor, /* * Introspection response processing: */ ExtractJsonHttpResponse.Descriptor); + + /// + /// Contains the logic responsible of attaching the client credentials to the HTTP Authorization header. + /// + public class AttachBasicAuthenticationCredentials : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachFormParameters.Descriptor.Order - 1000) + .Build(); + + public async ValueTask HandleAsync([NotNull] PrepareIntrospectionRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to System.Net.Http requests. If the HTTP request cannot be resolved, + // this may indicate that the request was incorrectly processed by another client stack. + var request = context.Transaction.GetHttpRequestMessage(); + if (request == null) + { + throw new InvalidOperationException("The System.Net.Http request cannot be resolved."); + } + + var configuration = await context.Options.ConfigurationManager.GetConfigurationAsync(default) ?? + throw new InvalidOperationException("An unknown error occurred while retrieving the server configuration."); + + // The OAuth 2.0 specification recommends sending the client credentials using basic authentication. + // However, this authentication method is known to have compatibility issues with how the + // client credentials are encoded, that MUST be formURL-encoded before being base64-encoded. + // To guarantee that the OpenIddict validation handler can be used with servers implementing + // non-standard encoding, the client_secret_post is always preferred when it's explicitly + // listed as a supported client authentication method for the introspection endpoint. + // 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 supported 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 (!configuration.IntrospectionEndpointAuthMethodsSupported.Contains(ClientAuthenticationMethods.ClientSecretPost)) + { + var builder = new StringBuilder() + .Append(EscapeDataString(context.Request.ClientId)) + .Append(':') + .Append(EscapeDataString(context.Request.ClientSecret)); + + var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes(builder.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. + context.Request.ClientId = context.Request.ClientSecret = null; + } + + static string EscapeDataString(string value) + { + if (string.IsNullOrEmpty(value)) + { + return null; + } + + return Uri.EscapeDataString(value).Replace("%20", "+"); + } + } + } } } } diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs index a9b59330..32c2188e 100644 --- a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs +++ b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs @@ -40,7 +40,73 @@ namespace OpenIddict.Validation.SystemNetHttp = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler>() - .SetOrder(int.MaxValue - 100_000) + .SetOrder(int.MinValue + 100_000) + .Build(); + + public ValueTask HandleAsync([NotNull] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var request = new HttpRequestMessage(HttpMethod.Get, context.Address); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + request.Headers.AcceptCharset.Add(new StringWithQualityHeaderValue("utf-8")); + + // Store the HttpRequestMessage in the transaction properties. + context.Transaction.Properties[typeof(HttpRequestMessage).FullName] = request; + + return default; + } + } + + /// + /// Contains the logic responsible of preparing an HTTP POST request message. + /// + public class PreparePostHttpRequest : IOpenIddictValidationHandler where TContext : BaseExternalContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(PrepareGetHttpRequest.Descriptor.Order + 1_000) + .Build(); + + public ValueTask HandleAsync([NotNull] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var request = new HttpRequestMessage(HttpMethod.Post, context.Address); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + request.Headers.AcceptCharset.Add(new StringWithQualityHeaderValue("utf-8")); + + // Store the HttpRequestMessage in the transaction properties. + context.Transaction.Properties[typeof(HttpRequestMessage).FullName] = request; + + return default; + } + } + + /// + /// Contains the logic responsible of attaching the query string parameters to the HTTP request. + /// + public class AttachQueryStringParameters : IOpenIddictValidationHandler where TContext : BaseExternalContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(AttachFormParameters.Descriptor.Order - 100_000) .Build(); public async ValueTask HandleAsync([NotNull] TContext context) @@ -50,6 +116,14 @@ namespace OpenIddict.Validation.SystemNetHttp throw new ArgumentNullException(nameof(context)); } + // This handler only applies to System.Net.Http requests. If the HTTP request cannot be resolved, + // this may indicate that the request was incorrectly processed by another client stack. + var request = context.Transaction.GetHttpRequestMessage(); + if (request == null) + { + throw new InvalidOperationException("The System.Net.Http request cannot be resolved."); + } + // Note: System.Net.Http doesn't expose convenient methods allowing to create // query strings from existing key/value pairs. To work around this limitation, // a FormUrlEncodedContent is instantiated and used to manually create the URL. @@ -60,24 +134,19 @@ namespace OpenIddict.Validation.SystemNetHttp from value in values select new KeyValuePair(parameter.Key, value)); - var builder = new UriBuilder(context.Address) + var builder = new UriBuilder(request.RequestUri) { Query = await content.ReadAsStringAsync() }; - var request = new HttpRequestMessage(HttpMethod.Get, builder.Uri); - request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - request.Headers.AcceptCharset.Add(new StringWithQualityHeaderValue("utf-8")); - - // Store the HttpRequestMessage in the transaction properties. - context.Transaction.Properties[typeof(HttpRequestMessage).FullName] = request; + request.RequestUri = builder.Uri; } } /// - /// Contains the logic responsible of preparing an HTTP POST request message. + /// Contains the logic responsible of attaching the form parameters to the HTTP request. /// - public class PreparePostHttpRequest : IOpenIddictValidationHandler where TContext : BaseExternalContext + public class AttachFormParameters : IOpenIddictValidationHandler where TContext : BaseExternalContext { /// /// Gets the default descriptor definition assigned to this handler. @@ -85,8 +154,8 @@ namespace OpenIddict.Validation.SystemNetHttp public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() - .UseSingletonHandler>() - .SetOrder(PrepareGetHttpRequest.Descriptor.Order - 1_000) + .UseSingletonHandler>() + .SetOrder(int.MaxValue - 100_000) .Build(); public ValueTask HandleAsync([NotNull] TContext context) @@ -96,9 +165,13 @@ namespace OpenIddict.Validation.SystemNetHttp throw new ArgumentNullException(nameof(context)); } - var request = new HttpRequestMessage(HttpMethod.Post, context.Address); - request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - request.Headers.AcceptCharset.Add(new StringWithQualityHeaderValue("utf-8")); + // 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(); + if (request == null) + { + throw new InvalidOperationException("The System.Net.Http request cannot be resolved."); + } request.Content = new FormUrlEncodedContent( from parameter in context.Request.GetParameters() @@ -107,9 +180,6 @@ namespace OpenIddict.Validation.SystemNetHttp from value in values select new KeyValuePair(parameter.Key, value)); - // Store the HttpRequestMessage in the transaction properties. - context.Transaction.Properties[typeof(HttpRequestMessage).FullName] = request; - return default; } } diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs index c6b51820..2d553057 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs @@ -24,8 +24,8 @@ namespace OpenIddict.Validation */ HandleErrorResponse.Descriptor, ValidateIssuer.Descriptor, - ExtractCryptographyEndpointUri.Descriptor, - ExtractIntrospectionEndpointUri.Descriptor, + ExtractCryptographyEndpoint.Descriptor, + ExtractIntrospectionEndpoint.Descriptor, /* * Cryptography response handling: @@ -100,14 +100,14 @@ namespace OpenIddict.Validation /// /// Contains the logic responsible of extracting the JWKS endpoint address from the discovery document. /// - public class ExtractCryptographyEndpointUri : IOpenIddictValidationHandler + public class ExtractCryptographyEndpoint : IOpenIddictValidationHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() + .UseSingletonHandler() .SetOrder(ValidateIssuer.Descriptor.Order + 1_000) .Build(); @@ -155,15 +155,15 @@ namespace OpenIddict.Validation /// /// Contains the logic responsible of extracting the introspection endpoint address from the discovery document. /// - public class ExtractIntrospectionEndpointUri : IOpenIddictValidationHandler + public class ExtractIntrospectionEndpoint : IOpenIddictValidationHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(ExtractCryptographyEndpointUri.Descriptor.Order + 1_000) + .UseSingletonHandler() + .SetOrder(ExtractCryptographyEndpoint.Descriptor.Order + 1_000) .Build(); /// @@ -192,6 +192,21 @@ namespace OpenIddict.Validation context.Configuration.IntrospectionEndpoint = address; + // Resolve the client authentication methods supported by the introspection endpoint, if available. + if (context.Response.TryGetParameter(Metadata.IntrospectionEndpointAuthMethodsSupported, out var methods)) + { + foreach (var method in methods.GetUnnamedParameters()) + { + var value = (string) method; + if (string.IsNullOrEmpty(value)) + { + continue; + } + + context.Configuration.IntrospectionEndpointAuthMethodsSupported.Add(value); + } + } + return default; } }