Browse Source

Make various improvements to the OpenIddict.Validation/System.Net.Http integration package

pull/1188/head
Kévin Chalet 6 years ago
parent
commit
037c2ed532
  1. 6
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Discovery.cs
  2. 35
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Introspection.cs
  3. 162
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs
  4. 2
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpOptions.cs

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

@ -20,11 +20,13 @@ namespace OpenIddict.Validation.SystemNetHttp
PrepareGetHttpRequest<PrepareConfigurationRequestContext>.Descriptor, PrepareGetHttpRequest<PrepareConfigurationRequestContext>.Descriptor,
AttachQueryStringParameters<PrepareConfigurationRequestContext>.Descriptor, AttachQueryStringParameters<PrepareConfigurationRequestContext>.Descriptor,
SendHttpRequest<ApplyConfigurationRequestContext>.Descriptor, SendHttpRequest<ApplyConfigurationRequestContext>.Descriptor,
DisposeHttpRequest<ApplyConfigurationRequestContext>.Descriptor,
/* /*
* Configuration response processing: * Configuration response processing:
*/ */
ExtractJsonHttpResponse<ExtractConfigurationResponseContext>.Descriptor, ExtractJsonHttpResponse<ExtractConfigurationResponseContext>.Descriptor,
DisposeHttpResponse<ExtractConfigurationResponseContext>.Descriptor,
/* /*
* Cryptography request processing: * Cryptography request processing:
@ -32,11 +34,13 @@ namespace OpenIddict.Validation.SystemNetHttp
PrepareGetHttpRequest<PrepareCryptographyRequestContext>.Descriptor, PrepareGetHttpRequest<PrepareCryptographyRequestContext>.Descriptor,
AttachQueryStringParameters<PrepareCryptographyRequestContext>.Descriptor, AttachQueryStringParameters<PrepareCryptographyRequestContext>.Descriptor,
SendHttpRequest<ApplyCryptographyRequestContext>.Descriptor, SendHttpRequest<ApplyCryptographyRequestContext>.Descriptor,
DisposeHttpRequest<ApplyCryptographyRequestContext>.Descriptor,
/* /*
* Configuration response processing: * Configuration response processing:
*/ */
ExtractJsonHttpResponse<ExtractCryptographyResponseContext>.Descriptor); ExtractJsonHttpResponse<ExtractCryptographyResponseContext>.Descriptor,
DisposeHttpResponse<ExtractCryptographyResponseContext>.Descriptor);
} }
} }
} }

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

@ -30,11 +30,13 @@ namespace OpenIddict.Validation.SystemNetHttp
AttachBasicAuthenticationCredentials.Descriptor, AttachBasicAuthenticationCredentials.Descriptor,
AttachFormParameters<PrepareIntrospectionRequestContext>.Descriptor, AttachFormParameters<PrepareIntrospectionRequestContext>.Descriptor,
SendHttpRequest<ApplyIntrospectionRequestContext>.Descriptor, SendHttpRequest<ApplyIntrospectionRequestContext>.Descriptor,
DisposeHttpRequest<ApplyIntrospectionRequestContext>.Descriptor,
/* /*
* Introspection response processing: * Introspection response processing:
*/ */
ExtractJsonHttpResponse<ExtractIntrospectionResponseContext>.Descriptor); ExtractJsonHttpResponse<ExtractIntrospectionResponseContext>.Descriptor,
DisposeHttpResponse<ExtractIntrospectionResponseContext>.Descriptor);
/// <summary> /// <summary>
/// Contains the logic responsible of attaching the client credentials to the HTTP Authorization header. /// Contains the logic responsible of attaching the client credentials to the HTTP Authorization header.
@ -70,45 +72,44 @@ namespace OpenIddict.Validation.SystemNetHttp
throw new InvalidOperationException(SR.GetResourceString(SR.ID0173)); throw new InvalidOperationException(SR.GetResourceString(SR.ID0173));
} }
// If no client identifier was attached to the request, skip the following logic.
if (string.IsNullOrEmpty(context.Request.ClientId))
{
return;
}
var configuration = await context.Options.ConfigurationManager.GetConfigurationAsync(default) ?? var configuration = await context.Options.ConfigurationManager.GetConfigurationAsync(default) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); throw new InvalidOperationException(SR.GetResourceString(SR.ID0140));
// The OAuth 2.0 specification recommends sending the client credentials using basic authentication. // 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 // However, this authentication method is known to have compatibility issues with the way the
// client credentials are encoded, that MUST be formURL-encoded before being base64-encoded. // client credentials are encoded (they MUST be formURL-encoded before being base64-encoded).
// To guarantee that the OpenIddict validation handler can be used with servers implementing // 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 // 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. // 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, // 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. // 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 // See https://tools.ietf.org/html/rfc8414#section-2
// and https://tools.ietf.org/html/rfc6749#section-2.3.1 for more information. // and https://tools.ietf.org/html/rfc6749#section-2.3.1 for more information.
if (!configuration.IntrospectionEndpointAuthMethodsSupported.Contains(ClientAuthenticationMethods.ClientSecretPost)) if (!configuration.IntrospectionEndpointAuthMethodsSupported.Contains(ClientAuthenticationMethods.ClientSecretPost))
{ {
var builder = new StringBuilder() // 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(EscapeDataString(context.Request.ClientId))
.Append(':') .Append(':')
.Append(EscapeDataString(context.Request.ClientSecret)); .Append(EscapeDataString(context.Request.ClientSecret))
.ToString()));
var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes(builder.ToString()));
// Attach the authorization header containing the client credentials to the HTTP request. // Attach the authorization header containing the client credentials to the HTTP request.
request.Headers.Authorization = new AuthenticationHeaderValue(Schemes.Basic, credentials); request.Headers.Authorization = new AuthenticationHeaderValue(Schemes.Basic, credentials);
// Remove the client credentials from the request. // Remove the client credentials from the request payload to ensure they are not sent twice.
context.Request.ClientId = context.Request.ClientSecret = null; context.Request.ClientId = context.Request.ClientSecret = null;
} }
static string? EscapeDataString(string? value) static string? EscapeDataString(string? value)
{ => value is not null ? Uri.EscapeDataString(value).Replace("%20", "+") : null;
if (string.IsNullOrEmpty(value))
{
return null;
}
return Uri.EscapeDataString(value).Replace("%20", "+");
}
} }
} }
} }

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

@ -9,10 +9,12 @@ using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using static OpenIddict.Validation.OpenIddictValidationEvents; using static OpenIddict.Validation.OpenIddictValidationEvents;
@ -46,6 +48,8 @@ namespace OpenIddict.Validation.SystemNetHttp
.Build(); .Build();
/// <inheritdoc/> /// <inheritdoc/>
[SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope",
Justification = "The HTTP request message is disposed later by a dedicated handler.")]
public ValueTask HandleAsync(TContext context) public ValueTask HandleAsync(TContext context)
{ {
if (context is null) if (context is null)
@ -53,12 +57,17 @@ namespace OpenIddict.Validation.SystemNetHttp
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
var request = new HttpRequestMessage(HttpMethod.Get, context.Address); var request = new HttpRequestMessage(HttpMethod.Get, context.Address)
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); {
request.Headers.AcceptCharset.Add(new StringWithQualityHeaderValue("utf-8")); Headers =
{
Accept = { new MediaTypeWithQualityHeaderValue("application/json") },
AcceptCharset = { new StringWithQualityHeaderValue("utf-8") }
}
};
// Store the HttpRequestMessage in the transaction properties. // Store the HttpRequestMessage in the transaction properties.
context.Transaction.Properties[typeof(HttpRequestMessage).FullName!] = request; context.Transaction.SetProperty(typeof(HttpRequestMessage).FullName!, request);
return default; return default;
} }
@ -81,6 +90,8 @@ namespace OpenIddict.Validation.SystemNetHttp
.Build(); .Build();
/// <inheritdoc/> /// <inheritdoc/>
[SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope",
Justification = "The HTTP request message is disposed later by a dedicated handler.")]
public ValueTask HandleAsync(TContext context) public ValueTask HandleAsync(TContext context)
{ {
if (context is null) if (context is null)
@ -88,12 +99,17 @@ namespace OpenIddict.Validation.SystemNetHttp
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
var request = new HttpRequestMessage(HttpMethod.Post, context.Address); var request = new HttpRequestMessage(HttpMethod.Post, context.Address)
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); {
request.Headers.AcceptCharset.Add(new StringWithQualityHeaderValue("utf-8")); Headers =
{
Accept = { new MediaTypeWithQualityHeaderValue("application/json") },
AcceptCharset = { new StringWithQualityHeaderValue("utf-8") }
}
};
// Store the HttpRequestMessage in the transaction properties. // Store the HttpRequestMessage in the transaction properties.
context.Transaction.Properties[typeof(HttpRequestMessage).FullName!] = request; context.Transaction.SetProperty(typeof(HttpRequestMessage).FullName!, request);
return default; return default;
} }
@ -116,7 +132,7 @@ namespace OpenIddict.Validation.SystemNetHttp
.Build(); .Build();
/// <inheritdoc/> /// <inheritdoc/>
public async ValueTask HandleAsync(TContext context) public ValueTask HandleAsync(TContext context)
{ {
if (context is null) if (context is null)
{ {
@ -134,25 +150,35 @@ namespace OpenIddict.Validation.SystemNetHttp
throw new InvalidOperationException(SR.GetResourceString(SR.ID0173)); throw new InvalidOperationException(SR.GetResourceString(SR.ID0173));
} }
if (request.RequestUri is not null) if (request.RequestUri is null || context.Transaction.Request.Count == 0)
{
return default;
}
var builder = new StringBuilder();
foreach (var (key, value) in
from parameter in context.Transaction.Request.GetParameters()
let values = (string?[]?) parameter.Value
where values is not null
from value in values
where !string.IsNullOrEmpty(value)
select (parameter.Key, Value: value))
{ {
// Note: System.Net.Http doesn't expose convenient methods allowing to create if (builder.Length > 0)
// query strings from existing key/value pairs. To work around this limitation,
// a FormUrlEncodedContent is instantiated and used to manually create the URL.
using var content = new FormUrlEncodedContent(
from parameter in context.Transaction.Request.GetParameters()
let values = (string[]?) parameter.Value
where values is not null
from value in values
select new KeyValuePair<string, string>(parameter.Key, value));
var builder = new UriBuilder(request.RequestUri)
{ {
Query = await content.ReadAsStringAsync() builder.Append('&');
}; }
request.RequestUri = builder.Uri; builder.Append(Uri.EscapeDataString(key));
builder.Append('=');
builder.Append(Uri.EscapeDataString(value));
} }
// Compute the final request URI using the base address and the query string.
request.RequestUri = new UriBuilder(request.RequestUri) { Query = builder.ToString() }.Uri;
return default;
} }
} }
@ -218,7 +244,7 @@ namespace OpenIddict.Validation.SystemNetHttp
= OpenIddictValidationHandlerDescriptor.CreateBuilder<TContext>() = OpenIddictValidationHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpMetadataAddress>() .AddFilter<RequireHttpMetadataAddress>()
.UseSingletonHandler<SendHttpRequest<TContext>>() .UseSingletonHandler<SendHttpRequest<TContext>>()
.SetOrder(int.MaxValue - 100_000) .SetOrder(DisposeHttpRequest<TContext>.Descriptor.Order - 50_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn) .SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build(); .Build();
@ -252,7 +278,48 @@ namespace OpenIddict.Validation.SystemNetHttp
} }
// Store the HttpResponseMessage in the transaction properties. // Store the HttpResponseMessage in the transaction properties.
context.Transaction.Properties[typeof(HttpResponseMessage).FullName!] = response; context.Transaction.SetProperty(typeof(HttpResponseMessage).FullName!, response);
}
}
/// <summary>
/// Contains the logic responsible of disposing of the HTTP request message.
/// </summary>
public class DisposeHttpRequest<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<DisposeHttpRequest<TContext>>()
.SetOrder(int.MaxValue - 100_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(TContext context)
{
if (context is 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 is null)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0173));
}
request.Dispose();
// Remove the request from the transaction properties.
context.Transaction.SetProperty<HttpRequestMessage>(typeof(HttpRequestMessage).FullName!, null);
return default;
} }
} }
@ -268,7 +335,7 @@ namespace OpenIddict.Validation.SystemNetHttp
= OpenIddictValidationHandlerDescriptor.CreateBuilder<TContext>() = OpenIddictValidationHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpMetadataAddress>() .AddFilter<RequireHttpMetadataAddress>()
.UseSingletonHandler<ExtractJsonHttpResponse<TContext>>() .UseSingletonHandler<ExtractJsonHttpResponse<TContext>>()
.SetOrder(int.MaxValue - 100_000) .SetOrder(DisposeHttpResponse<TContext>.Descriptor.Order - 50_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn) .SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build(); .Build();
@ -296,5 +363,46 @@ namespace OpenIddict.Validation.SystemNetHttp
context.Transaction.Response = await response.Content.ReadFromJsonAsync<OpenIddictResponse>(); context.Transaction.Response = await response.Content.ReadFromJsonAsync<OpenIddictResponse>();
} }
} }
/// <summary>
/// Contains the logic responsible of disposing of the HTTP response message.
/// </summary>
public class DisposeHttpResponse<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<DisposeHttpResponse<TContext>>()
.SetOrder(int.MaxValue - 100_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(TContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// This handler only applies to System.Net.Http requests. If the HTTP response cannot be resolved,
// this may indicate that the request was incorrectly processed by another client stack.
var response = context.Transaction.GetHttpResponseMessage();
if (response is null)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0173));
}
response.Dispose();
// Remove the response from the transaction properties.
context.Transaction.SetProperty<HttpResponseMessage>(typeof(HttpResponseMessage).FullName!, null);
return default;
}
}
} }
} }

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

@ -23,6 +23,6 @@ namespace OpenIddict.Validation.SystemNetHttp
public IAsyncPolicy<HttpResponseMessage>? HttpErrorPolicy { get; set; } public IAsyncPolicy<HttpResponseMessage>? HttpErrorPolicy { get; set; }
= HttpPolicyExtensions.HandleTransientHttpError() = HttpPolicyExtensions.HandleTransientHttpError()
.OrResult(response => response.StatusCode == HttpStatusCode.NotFound) .OrResult(response => response.StatusCode == HttpStatusCode.NotFound)
.WaitAndRetryAsync(4, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt))); .WaitAndRetryAsync(3, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)));
} }
} }

Loading…
Cancel
Save