Browse Source

Implement HTTP decompression support in the System.Net.Http integration packages

pull/1524/head
Kévin Chalet 3 years ago
parent
commit
04a6698911
  1. 2
      Directory.Build.targets
  2. 3
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  3. 10
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs
  4. 13
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConstants.cs
  5. 8
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Discovery.cs
  6. 4
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Exchange.cs
  7. 4
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Userinfo.cs
  8. 210
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs
  9. 10
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConfiguration.cs
  10. 13
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConstants.cs
  11. 8
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Discovery.cs
  12. 4
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Introspection.cs
  13. 206
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs

2
Directory.Build.targets

@ -57,6 +57,7 @@
Condition=" ('$(TargetFrameworkIdentifier)' == '.NETCoreApp' And $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '2.1'))) Or
('$(TargetFrameworkIdentifier)' == '.NETStandard' And $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '2.1'))) ">
<DefineConstants>$(DefineConstants);SUPPORTS_BASE64_SPAN_CONVERSION</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_BROTLI_COMPRESSION</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_STATIC_RANDOM_NUMBER_GENERATOR_METHODS</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_TIME_CONSTANT_COMPARISONS</DefineConstants>
</PropertyGroup>
@ -90,6 +91,7 @@
Condition=" ('$(TargetFrameworkIdentifier)' == '.NETCoreApp' And $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '6.0'))) ">
<DefineConstants>$(DefineConstants);SUPPORTS_DIRECT_JSON_ELEMENT_SERIALIZATION</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_JSON_NODES</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_ZLIB_COMPRESSION</DefineConstants>
</PropertyGroup>
<!--

3
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -1772,6 +1772,9 @@ Alternatively, you can disable the token storage feature by calling 'services.Ad
<data name="ID2142" xml:space="preserve">
<value>The specified state token is not suitable for the requested operation.</value>
</data>
<data name="ID2143" xml:space="preserve">
<value>An unsupported content encoding was returned by the remote server.</value>
</data>
<data name="ID4000" xml:space="preserve">
<value>The '{0}' parameter shouldn't be null or empty at this point.</value>
</data>

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

@ -4,7 +4,6 @@
* the license and the contributors participating to this project.
*/
using System.Diagnostics;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Options;
@ -51,6 +50,15 @@ public class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptions<Open
return;
}
options.HttpClientActions.Add(options =>
{
// By default, HttpClient uses a default timeout of 100 seconds and allows payloads of up to 2GB.
// To help reduce the effects of malicious responses (e.g responses returned at a very slow pace
// or containing an infine amount of data), the default values are amended to use lower values.
options.MaxResponseContentBufferSize = 10 * 1024 * 1024;
options.Timeout = TimeSpan.FromMinutes(1);
});
options.HttpMessageHandlerBuilderActions.Add(builder =>
{
#if SUPPORTS_SERVICE_PROVIDER_IN_HTTP_MESSAGE_HANDLER_BUILDER

13
src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConstants.cs

@ -11,6 +11,19 @@ namespace OpenIddict.Client.SystemNetHttp;
/// </summary>
public static class OpenIddictClientSystemNetHttpConstants
{
public static class Charsets
{
public const string Utf8 = "utf-8";
}
public static class ContentEncodings
{
public const string Brotli = "br";
public const string Deflate = "deflate";
public const string Gzip = "gzip";
public const string Identity = "identity";
}
public static class MediaTypes
{
public const string Json = "application/json";

8
src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Discovery.cs

@ -17,7 +17,8 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
* Configuration request processing:
*/
PrepareGetHttpRequest<PrepareConfigurationRequestContext>.Descriptor,
AttachUserAgent<PrepareConfigurationRequestContext>.Descriptor,
AttachJsonAcceptHeaders<PrepareConfigurationRequestContext>.Descriptor,
AttachUserAgentHeader<PrepareConfigurationRequestContext>.Descriptor,
AttachQueryStringParameters<PrepareConfigurationRequestContext>.Descriptor,
SendHttpRequest<ApplyConfigurationRequestContext>.Descriptor,
DisposeHttpRequest<ApplyConfigurationRequestContext>.Descriptor,
@ -25,6 +26,7 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
/*
* Configuration response processing:
*/
DecompressResponseContent<ExtractConfigurationResponseContext>.Descriptor,
ExtractJsonHttpResponse<ExtractConfigurationResponseContext>.Descriptor,
ExtractWwwAuthenticateHeader<ExtractConfigurationResponseContext>.Descriptor,
ValidateHttpResponse<ExtractConfigurationResponseContext>.Descriptor,
@ -34,7 +36,8 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
* Cryptography request processing:
*/
PrepareGetHttpRequest<PrepareCryptographyRequestContext>.Descriptor,
AttachUserAgent<PrepareCryptographyRequestContext>.Descriptor,
AttachJsonAcceptHeaders<PrepareCryptographyRequestContext>.Descriptor,
AttachUserAgentHeader<PrepareCryptographyRequestContext>.Descriptor,
AttachQueryStringParameters<PrepareCryptographyRequestContext>.Descriptor,
SendHttpRequest<ApplyCryptographyRequestContext>.Descriptor,
DisposeHttpRequest<ApplyCryptographyRequestContext>.Descriptor,
@ -42,6 +45,7 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
/*
* Configuration response processing:
*/
DecompressResponseContent<ExtractCryptographyResponseContext>.Descriptor,
ExtractJsonHttpResponse<ExtractCryptographyResponseContext>.Descriptor,
ExtractWwwAuthenticateHeader<ExtractCryptographyResponseContext>.Descriptor,
ValidateHttpResponse<ExtractCryptographyResponseContext>.Descriptor,

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

@ -20,7 +20,8 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
* Token request processing:
*/
PreparePostHttpRequest<PrepareTokenRequestContext>.Descriptor,
AttachUserAgent<PrepareTokenRequestContext>.Descriptor,
AttachJsonAcceptHeaders<PrepareTokenRequestContext>.Descriptor,
AttachUserAgentHeader<PrepareTokenRequestContext>.Descriptor,
AttachBasicAuthenticationCredentials.Descriptor,
AttachFormParameters<PrepareTokenRequestContext>.Descriptor,
SendHttpRequest<ApplyTokenRequestContext>.Descriptor,
@ -29,6 +30,7 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
/*
* Token response processing:
*/
DecompressResponseContent<ExtractTokenResponseContext>.Descriptor,
ExtractJsonHttpResponse<ExtractTokenResponseContext>.Descriptor,
ExtractWwwAuthenticateHeader<ExtractTokenResponseContext>.Descriptor,
ValidateHttpResponse<ExtractTokenResponseContext>.Descriptor,

4
src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Userinfo.cs

@ -20,7 +20,8 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
* Userinfo request processing:
*/
PrepareGetHttpRequest<PrepareUserinfoRequestContext>.Descriptor,
AttachUserAgent<PrepareUserinfoRequestContext>.Descriptor,
AttachJsonAcceptHeaders<PrepareUserinfoRequestContext>.Descriptor,
AttachUserAgentHeader<PrepareUserinfoRequestContext>.Descriptor,
AttachBearerAccessToken.Descriptor,
AttachQueryStringParameters<PrepareUserinfoRequestContext>.Descriptor,
SendHttpRequest<ApplyUserinfoRequestContext>.Descriptor,
@ -29,6 +30,7 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
/*
* Userinfo response processing:
*/
DecompressResponseContent<ExtractUserinfoResponseContext>.Descriptor,
ExtractUserinfoTokenHttpResponse.Descriptor,
ExtractJsonHttpResponse<ExtractUserinfoResponseContext>.Descriptor,
ExtractWwwAuthenticateHeader<ExtractUserinfoResponseContext>.Descriptor,

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

@ -8,6 +8,7 @@ using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO.Compression;
using System.Net.Http.Headers;
using System.Text;
using Microsoft.Extensions.Logging;
@ -52,17 +53,9 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
throw new ArgumentNullException(nameof(context));
}
var request = new HttpRequestMessage(HttpMethod.Get, context.Address)
{
Headers =
{
Accept = { new MediaTypeWithQualityHeaderValue("application/json") },
AcceptCharset = { new StringWithQualityHeaderValue("utf-8") }
}
};
// Store the HttpRequestMessage in the transaction properties.
context.Transaction.SetProperty(typeof(HttpRequestMessage).FullName!, request);
context.Transaction.SetProperty(typeof(HttpRequestMessage).FullName!,
new HttpRequestMessage(HttpMethod.Get, context.Address));
return default;
}
@ -94,17 +87,50 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
throw new ArgumentNullException(nameof(context));
}
var request = new HttpRequestMessage(HttpMethod.Post, context.Address)
// Store the HttpRequestMessage in the transaction properties.
context.Transaction.SetProperty(typeof(HttpRequestMessage).FullName!,
new HttpRequestMessage(HttpMethod.Post, context.Address));
return default;
}
}
/// <summary>
/// Contains the logic responsible for attaching the appropriate HTTP
/// Accept-* headers to the HTTP request message to receive JSON responses.
/// </summary>
public class AttachJsonAcceptHeaders<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<RequireHttpMetadataAddress>()
.UseSingletonHandler<AttachJsonAcceptHeaders<TContext>>()
.SetOrder(PreparePostHttpRequest<TContext>.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(TContext context)
{
if (context is null)
{
Headers =
{
Accept = { new MediaTypeWithQualityHeaderValue("application/json") },
AcceptCharset = { new StringWithQualityHeaderValue("utf-8") }
}
};
throw new ArgumentNullException(nameof(context));
}
// Store the HttpRequestMessage in the transaction properties.
context.Transaction.SetProperty(typeof(HttpRequestMessage).FullName!, request);
// 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));
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypes.Json));
request.Headers.AcceptCharset.Add(new StringWithQualityHeaderValue(Charsets.Utf8));
// Note: for security reasons, HTTP compression is never opted-in by default. Providers
// that require using HTTP compression that register a custom event handler to send an
// Accept-Encoding header containing the supported algorithms (e.g GZip/Deflate/Brotli).
return default;
}
@ -113,11 +139,11 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
/// <summary>
/// Contains the logic responsible for attaching the user agent to the HTTP request.
/// </summary>
public class AttachUserAgent<TContext> : IOpenIddictClientHandler<TContext> where TContext : BaseExternalContext
public class AttachUserAgentHeader<TContext> : IOpenIddictClientHandler<TContext> where TContext : BaseExternalContext
{
private readonly IOptionsMonitor<OpenIddictClientSystemNetHttpOptions> _options;
public AttachUserAgent(IOptionsMonitor<OpenIddictClientSystemNetHttpOptions> options)
public AttachUserAgentHeader(IOptionsMonitor<OpenIddictClientSystemNetHttpOptions> options)
=> _options = options ?? throw new ArgumentNullException(nameof(options));
/// <summary>
@ -126,8 +152,8 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpMetadataAddress>()
.UseSingletonHandler<AttachUserAgent<TContext>>()
.SetOrder(AttachQueryStringParameters<TContext>.Descriptor.Order - 1_000)
.UseSingletonHandler<AttachUserAgentHeader<TContext>>()
.SetOrder(AttachJsonAcceptHeaders<TContext>.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@ -139,8 +165,6 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
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() ??
@ -289,7 +313,7 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
= OpenIddictClientHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpMetadataAddress>()
.UseSingletonHandler<SendHttpRequest<TContext>>()
.SetOrder(DisposeHttpRequest<TContext>.Descriptor.Order - 50_000)
.SetOrder(DecompressResponseContent<TContext>.Descriptor.Order - 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@ -384,6 +408,140 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
}
}
/// <summary>
/// Contains the logic responsible for decompressing the returned HTTP content.
/// </summary>
public class DecompressResponseContent<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<RequireHttpMetadataAddress>()
.UseSingletonHandler<DecompressResponseContent<TContext>>()
.SetOrder(ExtractJsonHttpResponse<TContext>.Descriptor.Order - 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(TContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// Note: automatic content decompression can be enabled by constructing an HttpClient wrapping
// a generic HttpClientHandler, a SocketsHttpHandler or a WinHttpHandler instance with the
// AutomaticDecompression property set to the desired algorithms (e.g GZip, Deflate or Brotli).
//
// Unfortunately, while convenient and efficient, relying on this property has two downsides:
//
// - By being specific to HttpClientHandler/SocketsHttpHandler/WinHttpHandler, the automatic
// decompression feature cannot be used with any other type of client handler, forcing users
// to use a specific instance configured with decompression support enforced and preventing
// them from chosing their own implementation (e.g via ConfigurePrimaryHttpMessageHandler()).
//
// - Setting AutomaticDecompression always overrides the Accept-Encoding header of all requests
// to include the selected algorithms without offering a way to make this behavior opt-in.
// Sadly, using HTTP content compression with transport security enabled has security implications
// that could potentially lead to compression side-channel attacks if the client is used with
// remote endpoints that reflect user-defined data and contain secret values (e.g BREACH attacks).
//
// Since OpenIddict itself cannot safely assume such scenarios will never happen (e.g a token request
// will typically be sent with an authorization code that can be defined by a malicious user and can
// potentially be reflected in the token response depending on the configuration of the remote server),
// it is safer to disable compression by default by not sending an Accept-Encoding header while
// still allowing encoded responses to be processed (e.g StackExchange forces content compression
// for all the supported HTTP APIs even if no Accept-Encoding header is explicitly sent by the client).
//
// For these reasons, OpenIddict doesn't rely on the automatic decompression feature and uses
// a custom event handler to deal with GZip/Deflate/Brotli-encoded responses, so that providers
// that require using HTTP compression can be supported without having to use it for all providers.
// 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() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0173));
// If no Content-Encoding header was returned, keep the response stream as-is.
if (response.Content is not { Headers.ContentEncoding.Count: > 0 })
{
return;
}
Stream? stream = null;
// Iterate the returned encodings and wrap the response stream using the specified algorithm.
// If one of the returned algorithms cannot be recognized, immediately return an error.
foreach (var encoding in response.Content.Headers.ContentEncoding.Reverse())
{
if (string.Equals(encoding, ContentEncodings.Identity, StringComparison.OrdinalIgnoreCase))
{
continue;
}
else if (string.Equals(encoding, ContentEncodings.Gzip, StringComparison.OrdinalIgnoreCase))
{
stream ??= await response.Content.ReadAsStreamAsync();
stream = new GZipStream(stream, CompressionMode.Decompress);
}
#if SUPPORTS_ZLIB_COMPRESSION
// Note: some server implementations are known to incorrectly implement the "Deflate" compression
// algorithm and don't wrap the compressed data in a ZLib frame as required by the specifications.
//
// Such implementations are deliberately not supported here. In this case, it is recommended to avoid
// including "deflate" in the Accept-Encoding header if the server is known to be non-compliant.
//
// For more information, read https://www.rfc-editor.org/rfc/rfc9110.html#name-deflate-coding.
else if (string.Equals(encoding, ContentEncodings.Deflate, StringComparison.OrdinalIgnoreCase))
{
stream ??= await response.Content.ReadAsStreamAsync();
stream = new ZLibStream(stream, CompressionMode.Decompress);
}
#endif
#if SUPPORTS_BROTLI_COMPRESSION
else if (string.Equals(encoding, ContentEncodings.Brotli, StringComparison.OrdinalIgnoreCase))
{
stream ??= await response.Content.ReadAsStreamAsync();
stream = new BrotliStream(stream, CompressionMode.Decompress);
}
#endif
else
{
context.Reject(
error: Errors.ServerError,
description: SR.GetResourceString(SR.ID2143),
uri: SR.FormatID8000(SR.ID2143));
return;
}
}
// At this point, if the stream was wrapped, replace the content attached
// to the HTTP response message to use the specified stream transformations.
if (stream is not null)
{
var content = new StreamContent(stream);
// Copy the headers from the original content to the new instance.
foreach (var header in response.Content.Headers)
{
content.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
// Reset the Content-Length and Content-Encoding headers to indicate
// the content was successfully decoded using the specified algorithms.
content.Headers.ContentLength = null;
content.Headers.ContentEncoding.Clear();
response.Content = content;
}
}
}
/// <summary>
/// Contains the logic responsible for extracting the response from the JSON-encoded HTTP body.
/// </summary>

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

@ -4,7 +4,6 @@
* the license and the contributors participating to this project.
*/
using System.Diagnostics;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Options;
@ -51,6 +50,15 @@ public class OpenIddictValidationSystemNetHttpConfiguration : IConfigureOptions<
return;
}
options.HttpClientActions.Add(options =>
{
// By default, HttpClient uses a default timeout of 100 seconds and allows payloads of up to 2GB.
// To help reduce the effects of malicious responses (e.g responses returned at a very slow pace
// or containing an infine amount of data), the default values are amended to use lower values.
options.MaxResponseContentBufferSize = 10 * 1024 * 1024;
options.Timeout = TimeSpan.FromMinutes(1);
});
options.HttpMessageHandlerBuilderActions.Add(builder =>
{
#if SUPPORTS_SERVICE_PROVIDER_IN_HTTP_MESSAGE_HANDLER_BUILDER

13
src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConstants.cs

@ -11,6 +11,19 @@ namespace OpenIddict.Validation.SystemNetHttp;
/// </summary>
public static class OpenIddictValidationSystemNetHttpConstants
{
public static class Charsets
{
public const string Utf8 = "utf-8";
}
public static class ContentEncodings
{
public const string Brotli = "br";
public const string Deflate = "deflate";
public const string Gzip = "gzip";
public const string Identity = "identity";
}
public static class MediaTypes
{
public const string Json = "application/json";

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

@ -17,7 +17,8 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers
* Configuration request processing:
*/
PrepareGetHttpRequest<PrepareConfigurationRequestContext>.Descriptor,
AttachUserAgent<PrepareConfigurationRequestContext>.Descriptor,
AttachJsonAcceptHeaders<PrepareConfigurationRequestContext>.Descriptor,
AttachUserAgentHeader<PrepareConfigurationRequestContext>.Descriptor,
AttachQueryStringParameters<PrepareConfigurationRequestContext>.Descriptor,
SendHttpRequest<ApplyConfigurationRequestContext>.Descriptor,
DisposeHttpRequest<ApplyConfigurationRequestContext>.Descriptor,
@ -25,6 +26,7 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers
/*
* Configuration response processing:
*/
DecompressResponseContent<ExtractConfigurationResponseContext>.Descriptor,
ExtractJsonHttpResponse<ExtractConfigurationResponseContext>.Descriptor,
ExtractWwwAuthenticateHeader<ExtractConfigurationResponseContext>.Descriptor,
ValidateHttpResponse<ExtractConfigurationResponseContext>.Descriptor,
@ -34,7 +36,8 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers
* Cryptography request processing:
*/
PrepareGetHttpRequest<PrepareCryptographyRequestContext>.Descriptor,
AttachUserAgent<PrepareCryptographyRequestContext>.Descriptor,
AttachJsonAcceptHeaders<PrepareCryptographyRequestContext>.Descriptor,
AttachUserAgentHeader<PrepareCryptographyRequestContext>.Descriptor,
AttachQueryStringParameters<PrepareCryptographyRequestContext>.Descriptor,
SendHttpRequest<ApplyCryptographyRequestContext>.Descriptor,
DisposeHttpRequest<ApplyCryptographyRequestContext>.Descriptor,
@ -42,6 +45,7 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers
/*
* Configuration response processing:
*/
DecompressResponseContent<ExtractCryptographyResponseContext>.Descriptor,
ExtractJsonHttpResponse<ExtractCryptographyResponseContext>.Descriptor,
ExtractWwwAuthenticateHeader<ExtractCryptographyResponseContext>.Descriptor,
ValidateHttpResponse<ExtractCryptographyResponseContext>.Descriptor,

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

@ -20,7 +20,8 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers
* Introspection request processing:
*/
PreparePostHttpRequest<PrepareIntrospectionRequestContext>.Descriptor,
AttachUserAgent<PrepareIntrospectionRequestContext>.Descriptor,
AttachJsonAcceptHeaders<PrepareIntrospectionRequestContext>.Descriptor,
AttachUserAgentHeader<PrepareIntrospectionRequestContext>.Descriptor,
AttachBasicAuthenticationCredentials.Descriptor,
AttachFormParameters<PrepareIntrospectionRequestContext>.Descriptor,
SendHttpRequest<ApplyIntrospectionRequestContext>.Descriptor,
@ -29,6 +30,7 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers
/*
* Introspection response processing:
*/
DecompressResponseContent<ExtractIntrospectionResponseContext>.Descriptor,
ExtractJsonHttpResponse<ExtractIntrospectionResponseContext>.Descriptor,
ExtractWwwAuthenticateHeader<ExtractIntrospectionResponseContext>.Descriptor,
ValidateHttpResponse<ExtractIntrospectionResponseContext>.Descriptor,

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

@ -8,6 +8,7 @@ using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO.Compression;
using System.Net.Http.Headers;
using System.Text;
using Microsoft.Extensions.Logging;
@ -51,17 +52,9 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers
throw new ArgumentNullException(nameof(context));
}
var request = new HttpRequestMessage(HttpMethod.Get, context.Address)
{
Headers =
{
Accept = { new MediaTypeWithQualityHeaderValue("application/json") },
AcceptCharset = { new StringWithQualityHeaderValue("utf-8") }
}
};
// Store the HttpRequestMessage in the transaction properties.
context.Transaction.SetProperty(typeof(HttpRequestMessage).FullName!, request);
context.Transaction.SetProperty(typeof(HttpRequestMessage).FullName!,
new HttpRequestMessage(HttpMethod.Get, context.Address));
return default;
}
@ -93,17 +86,50 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers
throw new ArgumentNullException(nameof(context));
}
var request = new HttpRequestMessage(HttpMethod.Post, context.Address)
// Store the HttpRequestMessage in the transaction properties.
context.Transaction.SetProperty(typeof(HttpRequestMessage).FullName!,
new HttpRequestMessage(HttpMethod.Post, context.Address));
return default;
}
}
/// <summary>
/// Contains the logic responsible for attaching the appropriate HTTP
/// Accept-* headers to the HTTP request message to receive JSON responses.
/// </summary>
public class AttachJsonAcceptHeaders<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<AttachJsonAcceptHeaders<TContext>>()
.SetOrder(PreparePostHttpRequest<TContext>.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(TContext context)
{
if (context is null)
{
Headers =
{
Accept = { new MediaTypeWithQualityHeaderValue("application/json") },
AcceptCharset = { new StringWithQualityHeaderValue("utf-8") }
}
};
throw new ArgumentNullException(nameof(context));
}
// Store the HttpRequestMessage in the transaction properties.
context.Transaction.SetProperty(typeof(HttpRequestMessage).FullName!, request);
// 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));
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypes.Json));
request.Headers.AcceptCharset.Add(new StringWithQualityHeaderValue(Charsets.Utf8));
// Note: for security reasons, HTTP compression is never opted-in by default. Providers
// that require using HTTP compression that register a custom event handler to send an
// Accept-Encoding header containing the supported algorithms (e.g GZip/Deflate/Brotli).
return default;
}
@ -112,11 +138,11 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers
/// <summary>
/// Contains the logic responsible for attaching the user agent to the HTTP request.
/// </summary>
public class AttachUserAgent<TContext> : IOpenIddictValidationHandler<TContext> where TContext : BaseExternalContext
public class AttachUserAgentHeader<TContext> : IOpenIddictValidationHandler<TContext> where TContext : BaseExternalContext
{
private readonly IOptionsMonitor<OpenIddictValidationSystemNetHttpOptions> _options;
public AttachUserAgent(IOptionsMonitor<OpenIddictValidationSystemNetHttpOptions> options)
public AttachUserAgentHeader(IOptionsMonitor<OpenIddictValidationSystemNetHttpOptions> options)
=> _options = options ?? throw new ArgumentNullException(nameof(options));
/// <summary>
@ -125,8 +151,8 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpMetadataAddress>()
.UseSingletonHandler<AttachUserAgent<TContext>>()
.SetOrder(AttachQueryStringParameters<TContext>.Descriptor.Order - 1_000)
.UseSingletonHandler<AttachUserAgentHeader<TContext>>()
.SetOrder(AttachJsonAcceptHeaders<TContext>.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
@ -383,6 +409,140 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers
}
}
/// <summary>
/// Contains the logic responsible for decompressing the returned HTTP content.
/// </summary>
public class DecompressResponseContent<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<DecompressResponseContent<TContext>>()
.SetOrder(ExtractJsonHttpResponse<TContext>.Descriptor.Order - 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(TContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// Note: automatic content decompression can be enabled by constructing an HttpClient wrapping
// a generic HttpClientHandler, a SocketsHttpHandler or a WinHttpHandler instance with the
// AutomaticDecompression property set to the desired algorithms (e.g GZip, Deflate or Brotli).
//
// Unfortunately, while convenient and efficient, relying on this property has two downsides:
//
// - By being specific to HttpClientHandler/SocketsHttpHandler/WinHttpHandler, the automatic
// decompression feature cannot be used with any other type of client handler, forcing users
// to use a specific instance configured with decompression support enforced and preventing
// them from chosing their own implementation (e.g via ConfigurePrimaryHttpMessageHandler()).
//
// - Setting AutomaticDecompression always overrides the Accept-Encoding header of all requests
// to include the selected algorithms without offering a way to make this behavior opt-in.
// Sadly, using HTTP content compression with transport security enabled has security implications
// that could potentially lead to compression side-channel attacks if the client is used with
// remote endpoints that reflect user-defined data and contain secret values (e.g BREACH attacks).
//
// Since OpenIddict itself cannot safely assume such scenarios will never happen (e.g a token request
// will typically be sent with an authorization code that can be defined by a malicious user and can
// potentially be reflected in the token response depending on the configuration of the remote server),
// it is safer to disable compression by default by not sending an Accept-Encoding header while
// still allowing encoded responses to be processed (e.g StackExchange forces content compression
// for all the supported HTTP APIs even if no Accept-Encoding header is explicitly sent by the client).
//
// For these reasons, OpenIddict doesn't rely on the automatic decompression feature and uses
// a custom event handler to deal with GZip/Deflate/Brotli-encoded responses, so that providers
// that require using HTTP compression can be supported without having to use it for all providers.
// 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() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0173));
// If no Content-Encoding header was returned, keep the response stream as-is.
if (response.Content is not { Headers.ContentEncoding.Count: > 0 })
{
return;
}
Stream? stream = null;
// Iterate the returned encodings and wrap the response stream using the specified algorithm.
// If one of the returned algorithms cannot be recognized, immediately return an error.
foreach (var encoding in response.Content.Headers.ContentEncoding.Reverse())
{
if (string.Equals(encoding, ContentEncodings.Identity, StringComparison.OrdinalIgnoreCase))
{
continue;
}
else if (string.Equals(encoding, ContentEncodings.Gzip, StringComparison.OrdinalIgnoreCase))
{
stream ??= await response.Content.ReadAsStreamAsync();
stream = new GZipStream(stream, CompressionMode.Decompress);
}
#if SUPPORTS_ZLIB_COMPRESSION
// Note: some server implementations are known to incorrectly implement the "Deflate" compression
// algorithm and don't wrap the compressed data in a ZLib frame as required by the specifications.
//
// Such implementations are deliberately not supported here. In this case, it is recommended to avoid
// including "deflate" in the Accept-Encoding header if the server is known to be non-compliant.
//
// For more information, read https://www.rfc-editor.org/rfc/rfc9110.html#name-deflate-coding.
else if (string.Equals(encoding, ContentEncodings.Deflate, StringComparison.OrdinalIgnoreCase))
{
stream ??= await response.Content.ReadAsStreamAsync();
stream = new ZLibStream(stream, CompressionMode.Decompress);
}
#endif
#if SUPPORTS_BROTLI_COMPRESSION
else if (string.Equals(encoding, ContentEncodings.Brotli, StringComparison.OrdinalIgnoreCase))
{
stream ??= await response.Content.ReadAsStreamAsync();
stream = new BrotliStream(stream, CompressionMode.Decompress);
}
#endif
else
{
context.Reject(
error: Errors.ServerError,
description: SR.GetResourceString(SR.ID2143),
uri: SR.FormatID8000(SR.ID2143));
return;
}
}
// At this point, if the stream was wrapped, replace the content attached
// to the HTTP response message to use the specified stream transformations.
if (stream is not null)
{
var content = new StreamContent(stream);
// Copy the headers from the original content to the new instance.
foreach (var header in response.Content.Headers)
{
content.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
// Reset the Content-Length and Content-Encoding headers to indicate
// the content was successfully decoded using the specified algorithms.
content.Headers.ContentLength = null;
content.Headers.ContentEncoding.Clear();
response.Content = content;
}
}
}
/// <summary>
/// Contains the logic responsible for extracting the response from the JSON-encoded HTTP body.
/// </summary>

Loading…
Cancel
Save