Browse Source

Implement client_secret_basic support for introspection

pull/961/head
Kévin Chalet 6 years ago
parent
commit
861a2376ca
  1. 2
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Discovery.cs
  2. 82
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Introspection.cs
  3. 106
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs
  4. 29
      src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs

2
src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Discovery.cs

@ -18,6 +18,7 @@ namespace OpenIddict.Validation.SystemNetHttp
* Configuration request processing:
*/
PrepareGetHttpRequest<PrepareConfigurationRequestContext>.Descriptor,
AttachQueryStringParameters<PrepareConfigurationRequestContext>.Descriptor,
SendHttpRequest<ApplyConfigurationRequestContext>.Descriptor,
/*
@ -29,6 +30,7 @@ namespace OpenIddict.Validation.SystemNetHttp
* Cryptography request processing:
*/
PrepareGetHttpRequest<PrepareCryptographyRequestContext>.Descriptor,
AttachQueryStringParameters<PrepareCryptographyRequestContext>.Descriptor,
SendHttpRequest<ApplyCryptographyRequestContext>.Descriptor,
/*

82
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<PrepareIntrospectionRequestContext>.Descriptor,
AttachBasicAuthenticationCredentials.Descriptor,
AttachFormParameters<PrepareIntrospectionRequestContext>.Descriptor,
SendHttpRequest<ApplyIntrospectionRequestContext>.Descriptor,
/*
* Introspection response processing:
*/
ExtractJsonHttpResponse<ExtractIntrospectionResponseContext>.Descriptor);
/// <summary>
/// Contains the logic responsible of attaching the client credentials to the HTTP Authorization header.
/// </summary>
public class AttachBasicAuthenticationCredentials : IOpenIddictValidationHandler<PrepareIntrospectionRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<PrepareIntrospectionRequestContext>()
.AddFilter<RequireHttpMetadataAddress>()
.UseSingletonHandler<AttachBasicAuthenticationCredentials>()
.SetOrder(AttachFormParameters<PrepareIntrospectionRequestContext>.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", "+");
}
}
}
}
}
}

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

@ -40,7 +40,73 @@ namespace OpenIddict.Validation.SystemNetHttp
= OpenIddictValidationHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpMetadataAddress>()
.UseSingletonHandler<PrepareGetHttpRequest<TContext>>()
.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;
}
}
/// <summary>
/// Contains the logic responsible of preparing an HTTP POST request message.
/// </summary>
public class PreparePostHttpRequest<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<RequireHttpMetadataAddress>()
.UseSingletonHandler<PreparePostHttpRequest<TContext>>()
.SetOrder(PrepareGetHttpRequest<TContext>.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;
}
}
/// <summary>
/// Contains the logic responsible of attaching the query string parameters to the HTTP request.
/// </summary>
public class AttachQueryStringParameters<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<RequireHttpMetadataAddress>()
.UseSingletonHandler<AttachQueryStringParameters<TContext>>()
.SetOrder(AttachFormParameters<TContext>.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<string, string>(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;
}
}
/// <summary>
/// Contains the logic responsible of preparing an HTTP POST request message.
/// Contains the logic responsible of attaching the form parameters to the HTTP request.
/// </summary>
public class PreparePostHttpRequest<TContext> : IOpenIddictValidationHandler<TContext> where TContext : BaseExternalContext
public class AttachFormParameters<TContext> : IOpenIddictValidationHandler<TContext> where TContext : BaseExternalContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
@ -85,8 +154,8 @@ namespace OpenIddict.Validation.SystemNetHttp
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpMetadataAddress>()
.UseSingletonHandler<PreparePostHttpRequest<TContext>>()
.SetOrder(PrepareGetHttpRequest<TContext>.Descriptor.Order - 1_000)
.UseSingletonHandler<AttachFormParameters<TContext>>()
.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<string, string>(parameter.Key, value));
// Store the HttpRequestMessage in the transaction properties.
context.Transaction.Properties[typeof(HttpRequestMessage).FullName] = request;
return default;
}
}

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

@ -24,8 +24,8 @@ namespace OpenIddict.Validation
*/
HandleErrorResponse<HandleConfigurationResponseContext>.Descriptor,
ValidateIssuer.Descriptor,
ExtractCryptographyEndpointUri.Descriptor,
ExtractIntrospectionEndpointUri.Descriptor,
ExtractCryptographyEndpoint.Descriptor,
ExtractIntrospectionEndpoint.Descriptor,
/*
* Cryptography response handling:
@ -100,14 +100,14 @@ namespace OpenIddict.Validation
/// <summary>
/// Contains the logic responsible of extracting the JWKS endpoint address from the discovery document.
/// </summary>
public class ExtractCryptographyEndpointUri : IOpenIddictValidationHandler<HandleConfigurationResponseContext>
public class ExtractCryptographyEndpoint : IOpenIddictValidationHandler<HandleConfigurationResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>()
.UseSingletonHandler<ExtractCryptographyEndpointUri>()
.UseSingletonHandler<ExtractCryptographyEndpoint>()
.SetOrder(ValidateIssuer.Descriptor.Order + 1_000)
.Build();
@ -155,15 +155,15 @@ namespace OpenIddict.Validation
/// <summary>
/// Contains the logic responsible of extracting the introspection endpoint address from the discovery document.
/// </summary>
public class ExtractIntrospectionEndpointUri : IOpenIddictValidationHandler<HandleConfigurationResponseContext>
public class ExtractIntrospectionEndpoint : IOpenIddictValidationHandler<HandleConfigurationResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>()
.UseSingletonHandler<ExtractIntrospectionEndpointUri>()
.SetOrder(ExtractCryptographyEndpointUri.Descriptor.Order + 1_000)
.UseSingletonHandler<ExtractIntrospectionEndpoint>()
.SetOrder(ExtractCryptographyEndpoint.Descriptor.Order + 1_000)
.Build();
/// <summary>
@ -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;
}
}

Loading…
Cancel
Save