Browse Source
Implement draft-ietf-oauth-client-id-metadata-document-00 support, allowing OAuth clients to use an HTTPS URL as their client_id with the server fetching a JSON metadata document from that URL. - Add EnableClientIdMetadataDocumentSupport option and related config - Create OpenIddict.Server.SystemNetHttp project for HTTP outbound metadata document fetching (following Client.SystemNetHttp patterns) - Modify handler pipeline: ValidateClientId sets CIMD flag when FindByClientIdAsync returns null and client_id is a valid HTTPS URL - Add CIMD bypasses to authentication, sign-in, and token generation handlers that look up pre-registered applications - Validate redirect_uri against fetched metadata document - Advertise client_id_metadata_document_supported in discovery - Update sandbox demonstrator with CIMD support and test endpointpull/2416/head
22 changed files with 1212 additions and 20 deletions
@ -0,0 +1,10 @@ |
|||
{ |
|||
"Logging": { |
|||
"LogLevel": { |
|||
"Default": "Information", |
|||
"Microsoft": "Warning", |
|||
"Microsoft.Hosting.Lifetime": "Information", |
|||
"OpenIddict": "Debug" |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFrameworks> |
|||
$(NetFrameworkTargetFrameworks); |
|||
$(NetCoreTargetFrameworks); |
|||
$(NetStandardTargetFrameworks) |
|||
</TargetFrameworks> |
|||
</PropertyGroup> |
|||
|
|||
<PropertyGroup> |
|||
<Description>System.Net.Http integration package for the OpenIddict server services.</Description> |
|||
<PackageTags>$(PackageTags);http;httpclient</PackageTags> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\OpenIddict.Server\OpenIddict.Server.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Microsoft.Extensions.Http.Polly" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup |
|||
Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' Or '$(TargetFrameworkIdentifier)' == '.NETStandard' "> |
|||
<PackageReference Include="System.Net.Http.Json" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETCoreApp' "> |
|||
<PackageReference Include="Microsoft.Extensions.Http.Resilience" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<Using Include="System.Net.Http.Json" /> |
|||
<Using Include="OpenIddict.Abstractions" /> |
|||
<Using Include="OpenIddict.Abstractions.OpenIddictConstants" Static="true" /> |
|||
<Using Include="OpenIddict.Abstractions.OpenIddictResources" Alias="SR" /> |
|||
<Using Include="OpenIddict.Server.OpenIddictServerEvents" Static="true" /> |
|||
<Using Include="OpenIddict.Server.OpenIddictServerHandlers" Static="true" /> |
|||
<Using Include="OpenIddict.Server.OpenIddictServerHandlerFilters" Static="true" /> |
|||
<Using Include="OpenIddict.Server.SystemNetHttp.OpenIddictServerSystemNetHttpHandlers" Static="true" /> |
|||
<Using Include="OpenIddict.Server.SystemNetHttp.OpenIddictServerSystemNetHttpHandlerFilters" Static="true" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,208 @@ |
|||
/* |
|||
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
|||
* See https://github.com/openiddict/openiddict-core for more information concerning
|
|||
* the license and the contributors participating to this project. |
|||
*/ |
|||
|
|||
using System.ComponentModel; |
|||
using System.Net.Http; |
|||
using System.Net.Http.Headers; |
|||
using System.Net.Mail; |
|||
using System.Reflection; |
|||
using OpenIddict.Server.SystemNetHttp; |
|||
using Polly; |
|||
|
|||
namespace Microsoft.Extensions.DependencyInjection; |
|||
|
|||
/// <summary>
|
|||
/// Exposes the necessary methods required to configure the OpenIddict server/System.Net.Http integration.
|
|||
/// </summary>
|
|||
public sealed class OpenIddictServerSystemNetHttpBuilder |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of <see cref="OpenIddictServerSystemNetHttpBuilder"/>.
|
|||
/// </summary>
|
|||
/// <param name="services">The services collection.</param>
|
|||
public OpenIddictServerSystemNetHttpBuilder(IServiceCollection services) |
|||
=> Services = services ?? throw new ArgumentNullException(nameof(services)); |
|||
|
|||
/// <summary>
|
|||
/// Gets the services collection.
|
|||
/// </summary>
|
|||
[EditorBrowsable(EditorBrowsableState.Never)] |
|||
public IServiceCollection Services { get; } |
|||
|
|||
/// <summary>
|
|||
/// Amends the default OpenIddict server/System.Net.Http configuration.
|
|||
/// </summary>
|
|||
/// <param name="configuration">The delegate used to configure the OpenIddict options.</param>
|
|||
/// <remarks>This extension can be safely called multiple times.</remarks>
|
|||
/// <returns>The <see cref="OpenIddictServerSystemNetHttpBuilder"/> instance.</returns>
|
|||
public OpenIddictServerSystemNetHttpBuilder Configure(Action<OpenIddictServerSystemNetHttpOptions> configuration) |
|||
{ |
|||
ArgumentNullException.ThrowIfNull(configuration); |
|||
|
|||
Services.Configure(configuration); |
|||
|
|||
return this; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Configures the <see cref="HttpClient"/> used by the OpenIddict server/System.Net.Http integration.
|
|||
/// </summary>
|
|||
/// <param name="configuration">The delegate used to configure the <see cref="HttpClient"/>.</param>
|
|||
/// <returns>The <see cref="OpenIddictServerSystemNetHttpBuilder"/> instance.</returns>
|
|||
[EditorBrowsable(EditorBrowsableState.Advanced)] |
|||
public OpenIddictServerSystemNetHttpBuilder ConfigureHttpClient(Action<HttpClient> configuration) |
|||
{ |
|||
ArgumentNullException.ThrowIfNull(configuration); |
|||
|
|||
return Configure(options => options.HttpClientActions.Add(configuration)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Configures the <see cref="HttpClientHandler"/> used by the OpenIddict server/System.Net.Http integration.
|
|||
/// </summary>
|
|||
/// <param name="configuration">The delegate used to configure the <see cref="HttpClientHandler"/>.</param>
|
|||
/// <returns>The <see cref="OpenIddictServerSystemNetHttpBuilder"/> instance.</returns>
|
|||
[EditorBrowsable(EditorBrowsableState.Advanced)] |
|||
public OpenIddictServerSystemNetHttpBuilder ConfigureHttpClientHandler(Action<HttpClientHandler> configuration) |
|||
{ |
|||
ArgumentNullException.ThrowIfNull(configuration); |
|||
|
|||
return Configure(options => options.HttpClientHandlerActions.Add(configuration)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sets the contact address used in the "From" header that is attached
|
|||
/// to the HTTP requests sent by the OpenIddict server services.
|
|||
/// </summary>
|
|||
/// <param name="address">The mail address.</param>
|
|||
/// <returns>The <see cref="OpenIddictServerSystemNetHttpBuilder"/> instance.</returns>
|
|||
public OpenIddictServerSystemNetHttpBuilder SetContactAddress(MailAddress address) |
|||
{ |
|||
ArgumentNullException.ThrowIfNull(address); |
|||
|
|||
return Configure(options => options.ContactAddress = address); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sets the contact address used in the "From" header that is attached
|
|||
/// to the HTTP requests sent by the OpenIddict server services.
|
|||
/// </summary>
|
|||
/// <param name="address">The mail address.</param>
|
|||
/// <returns>The <see cref="OpenIddictServerSystemNetHttpBuilder"/> instance.</returns>
|
|||
public OpenIddictServerSystemNetHttpBuilder SetContactAddress(string address) |
|||
{ |
|||
ArgumentException.ThrowIfNullOrEmpty(address); |
|||
|
|||
return SetContactAddress(new MailAddress(address)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Replaces the default HTTP error policy used by the OpenIddict server services.
|
|||
/// </summary>
|
|||
/// <param name="policy">The HTTP Polly error policy.</param>
|
|||
/// <returns>The <see cref="OpenIddictServerSystemNetHttpBuilder"/> instance.</returns>
|
|||
public OpenIddictServerSystemNetHttpBuilder SetHttpErrorPolicy(IAsyncPolicy<HttpResponseMessage> policy) |
|||
{ |
|||
ArgumentNullException.ThrowIfNull(policy); |
|||
|
|||
return Configure(options => options.HttpErrorPolicy = policy); |
|||
} |
|||
|
|||
#if SUPPORTS_HTTP_CLIENT_RESILIENCE
|
|||
/// <summary>
|
|||
/// Replaces the default HTTP resilience pipeline used by the OpenIddict server services.
|
|||
/// </summary>
|
|||
/// <param name="configuration">
|
|||
/// The delegate used to configure the <see cref="ResiliencePipeline{HttpResponseMessage}"/>.
|
|||
/// </param>
|
|||
/// <remarks>
|
|||
/// Note: this option has no effect when an HTTP error policy was explicitly configured
|
|||
/// using <see cref="SetHttpErrorPolicy(IAsyncPolicy{HttpResponseMessage})"/>.
|
|||
/// </remarks>
|
|||
/// <returns>The <see cref="OpenIddictServerSystemNetHttpBuilder"/> instance.</returns>
|
|||
public OpenIddictServerSystemNetHttpBuilder SetHttpResiliencePipeline( |
|||
Action<ResiliencePipelineBuilder<HttpResponseMessage>> configuration) |
|||
{ |
|||
ArgumentNullException.ThrowIfNull(configuration); |
|||
|
|||
var builder = new ResiliencePipelineBuilder<HttpResponseMessage>(); |
|||
configuration(builder); |
|||
|
|||
return SetHttpResiliencePipeline(builder.Build()); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Replaces the default HTTP resilience pipeline used by the OpenIddict server services.
|
|||
/// </summary>
|
|||
/// <param name="pipeline">The HTTP resilience pipeline.</param>
|
|||
/// <remarks>
|
|||
/// Note: this option has no effect when an HTTP error policy was explicitly configured
|
|||
/// using <see cref="SetHttpErrorPolicy(IAsyncPolicy{HttpResponseMessage})"/>.
|
|||
/// </remarks>
|
|||
/// <returns>The <see cref="OpenIddictServerSystemNetHttpBuilder"/> instance.</returns>
|
|||
public OpenIddictServerSystemNetHttpBuilder SetHttpResiliencePipeline(ResiliencePipeline<HttpResponseMessage> pipeline) |
|||
{ |
|||
ArgumentNullException.ThrowIfNull(pipeline); |
|||
|
|||
return Configure(options => options.HttpResiliencePipeline = pipeline); |
|||
} |
|||
#endif
|
|||
|
|||
/// <summary>
|
|||
/// Sets the product information used in the "User-Agent" header that is attached
|
|||
/// to the HTTP requests sent by the OpenIddict server services.
|
|||
/// </summary>
|
|||
/// <param name="information">The product information.</param>
|
|||
/// <returns>The <see cref="OpenIddictServerSystemNetHttpBuilder"/> instance.</returns>
|
|||
public OpenIddictServerSystemNetHttpBuilder SetProductInformation(ProductInfoHeaderValue information) |
|||
{ |
|||
ArgumentNullException.ThrowIfNull(information); |
|||
|
|||
return Configure(options => options.ProductInformation = information); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sets the product information used in the "User-Agent" header that is attached
|
|||
/// to the HTTP requests sent by the OpenIddict server services.
|
|||
/// </summary>
|
|||
/// <param name="name">The product name.</param>
|
|||
/// <param name="version">The product version.</param>
|
|||
/// <returns>The <see cref="OpenIddictServerSystemNetHttpBuilder"/> instance.</returns>
|
|||
public OpenIddictServerSystemNetHttpBuilder SetProductInformation(string name, string? version) |
|||
{ |
|||
ArgumentException.ThrowIfNullOrEmpty(name); |
|||
|
|||
return SetProductInformation(new ProductInfoHeaderValue(name, version)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sets the product information used in the user agent header that is attached
|
|||
/// to the HTTP requests sent by the OpenIddict server services based
|
|||
/// on the identity of the specified .NET assembly (name and version).
|
|||
/// </summary>
|
|||
/// <param name="assembly">The assembly from which the product information is created.</param>
|
|||
/// <returns>The <see cref="OpenIddictServerSystemNetHttpBuilder"/> instance.</returns>
|
|||
public OpenIddictServerSystemNetHttpBuilder SetProductInformation(Assembly assembly) |
|||
{ |
|||
ArgumentNullException.ThrowIfNull(assembly); |
|||
|
|||
return SetProductInformation(new ProductInfoHeaderValue( |
|||
productName: assembly.GetName().Name!, |
|||
productVersion: assembly.GetName().Version!.ToString())); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
[EditorBrowsable(EditorBrowsableState.Never)] |
|||
public override bool Equals(object? obj) => base.Equals(obj); |
|||
|
|||
/// <inheritdoc/>
|
|||
[EditorBrowsable(EditorBrowsableState.Never)] |
|||
public override int GetHashCode() => base.GetHashCode(); |
|||
|
|||
/// <inheritdoc/>
|
|||
[EditorBrowsable(EditorBrowsableState.Never)] |
|||
public override string? ToString() => base.ToString(); |
|||
} |
|||
@ -0,0 +1,157 @@ |
|||
/* |
|||
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
|||
* See https://github.com/openiddict/openiddict-core for more information concerning
|
|||
* the license and the contributors participating to this project. |
|||
*/ |
|||
|
|||
using System.ComponentModel; |
|||
using System.Net; |
|||
using System.Net.Http; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Http; |
|||
using Microsoft.Extensions.Options; |
|||
using Polly; |
|||
|
|||
#if SUPPORTS_HTTP_CLIENT_RESILIENCE
|
|||
using Microsoft.Extensions.Http.Resilience; |
|||
#endif
|
|||
|
|||
namespace OpenIddict.Server.SystemNetHttp; |
|||
|
|||
/// <summary>
|
|||
/// Contains the methods required to ensure that the OpenIddict server/System.Net.Http integration configuration is valid.
|
|||
/// </summary>
|
|||
[EditorBrowsable(EditorBrowsableState.Advanced)] |
|||
public sealed class OpenIddictServerSystemNetHttpConfiguration : IConfigureOptions<OpenIddictServerOptions>, |
|||
IConfigureNamedOptions<HttpClientFactoryOptions>, |
|||
IPostConfigureOptions<HttpClientFactoryOptions> |
|||
{ |
|||
private readonly IServiceProvider _provider; |
|||
|
|||
/// <summary>
|
|||
/// Creates a new instance of the <see cref="OpenIddictServerSystemNetHttpConfiguration"/> class.
|
|||
/// </summary>
|
|||
/// <param name="provider">The service provider.</param>
|
|||
public OpenIddictServerSystemNetHttpConfiguration(IServiceProvider provider) |
|||
=> _provider = provider ?? throw new ArgumentNullException(nameof(provider)); |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Configure(OpenIddictServerOptions options) |
|||
{ |
|||
ArgumentNullException.ThrowIfNull(options); |
|||
|
|||
// Register the built-in event handlers used by the OpenIddict System.Net.Http server components.
|
|||
options.Handlers.AddRange(OpenIddictServerSystemNetHttpHandlers.DefaultHandlers); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Configure(HttpClientFactoryOptions options) => Configure(Options.DefaultName, options); |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Configure(string? name, HttpClientFactoryOptions options) |
|||
{ |
|||
ArgumentNullException.ThrowIfNull(options); |
|||
|
|||
var assembly = typeof(OpenIddictServerSystemNetHttpOptions).Assembly.GetName(); |
|||
|
|||
// 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; |
|||
} |
|||
|
|||
var settings = _provider.GetRequiredService<IOptionsMonitor<OpenIddictServerSystemNetHttpOptions>>().CurrentValue; |
|||
|
|||
options.HttpClientActions.Add(static client => |
|||
{ |
|||
// 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 infinite amount of data), the default values are amended to use lower values.
|
|||
client.MaxResponseContentBufferSize = 10 * 1024 * 1024; |
|||
client.Timeout = TimeSpan.FromMinutes(1); |
|||
}); |
|||
|
|||
// Register the user-defined HTTP client actions.
|
|||
foreach (var action in settings.HttpClientActions) |
|||
{ |
|||
options.HttpClientActions.Add(action); |
|||
} |
|||
|
|||
options.HttpMessageHandlerBuilderActions.Add(builder => |
|||
{ |
|||
var options = builder.Services.GetRequiredService<IOptionsMonitor<OpenIddictServerSystemNetHttpOptions>>(); |
|||
|
|||
// If applicable, add the handler responsible for replaying failed HTTP requests.
|
|||
//
|
|||
// Note: on .NET 8.0 and higher, the HTTP error policy is always set
|
|||
// to null by default and an HTTP resilience pipeline is used instead.
|
|||
if (options.CurrentValue.HttpErrorPolicy is IAsyncPolicy<HttpResponseMessage> policy) |
|||
{ |
|||
builder.AdditionalHandlers.Add(new PolicyHttpMessageHandler(policy)); |
|||
} |
|||
|
|||
#if SUPPORTS_HTTP_CLIENT_RESILIENCE
|
|||
else if (options.CurrentValue.HttpResiliencePipeline is ResiliencePipeline<HttpResponseMessage> pipeline) |
|||
{ |
|||
#pragma warning disable EXTEXP0001
|
|||
builder.AdditionalHandlers.Add(new ResilienceHandler(pipeline)); |
|||
#pragma warning restore EXTEXP0001
|
|||
} |
|||
#endif
|
|||
if (builder.PrimaryHandler is not HttpClientHandler handler) |
|||
{ |
|||
throw new InvalidOperationException(SR.FormatID0373(typeof(HttpClientHandler).FullName)); |
|||
} |
|||
}); |
|||
|
|||
// Register the user-defined HTTP client handler actions.
|
|||
foreach (var action in settings.HttpClientHandlerActions) |
|||
{ |
|||
options.HttpMessageHandlerBuilderActions.Add(builder => action( |
|||
builder.PrimaryHandler as HttpClientHandler ?? |
|||
throw new InvalidOperationException(SR.FormatID0373(typeof(HttpClientHandler).FullName)))); |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void PostConfigure(string? name, HttpClientFactoryOptions options) |
|||
{ |
|||
ArgumentNullException.ThrowIfNull(options); |
|||
|
|||
var assembly = typeof(OpenIddictServerSystemNetHttpOptions).Assembly.GetName(); |
|||
|
|||
// 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; |
|||
} |
|||
|
|||
options.HttpMessageHandlerBuilderActions.Insert(0, static builder => |
|||
{ |
|||
// Note: Microsoft.Extensions.Http 9.0+ no longer uses HttpClientHandler as the default instance
|
|||
// for PrimaryHandler on platforms that support SocketsHttpHandler. Since OpenIddict requires an
|
|||
// HttpClientHandler instance, it is manually reassigned here if it's not an HttpClientHandler.
|
|||
if (builder.PrimaryHandler is not HttpClientHandler) |
|||
{ |
|||
builder.PrimaryHandler = new HttpClientHandler(); |
|||
} |
|||
}); |
|||
|
|||
options.HttpMessageHandlerBuilderActions.Add(static builder => |
|||
{ |
|||
if (builder.PrimaryHandler is not HttpClientHandler handler) |
|||
{ |
|||
throw new InvalidOperationException(SR.FormatID0373(typeof(HttpClientHandler).FullName)); |
|||
} |
|||
|
|||
// Disable automatic content decompression for security reasons (BREACH attacks).
|
|||
if (handler.SupportsAutomaticDecompression) |
|||
{ |
|||
handler.AutomaticDecompression = DecompressionMethods.None; |
|||
} |
|||
|
|||
// Disable cookies support for security reasons.
|
|||
handler.UseCookies = false; |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
/* |
|||
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
|||
* See https://github.com/openiddict/openiddict-core for more information concerning
|
|||
* the license and the contributors participating to this project. |
|||
*/ |
|||
|
|||
namespace OpenIddict.Server.SystemNetHttp; |
|||
|
|||
/// <summary>
|
|||
/// Exposes common constants used by the OpenIddict server/System.Net.Http integration.
|
|||
/// </summary>
|
|||
public static class OpenIddictServerSystemNetHttpConstants |
|||
{ |
|||
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"; |
|||
} |
|||
|
|||
public static class Properties |
|||
{ |
|||
public const string ClientIdMetadataDocument = ".ClientIdMetadataDocument"; |
|||
public const string ClientIdMetadataDocumentFetchRequired = ".ClientIdMetadataDocumentFetchRequired"; |
|||
} |
|||
} |
|||
@ -0,0 +1,69 @@ |
|||
/* |
|||
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
|||
* See https://github.com/openiddict/openiddict-core for more information concerning
|
|||
* the license and the contributors participating to this project. |
|||
*/ |
|||
|
|||
using Microsoft.Extensions.DependencyInjection.Extensions; |
|||
using Microsoft.Extensions.Http; |
|||
using Microsoft.Extensions.Options; |
|||
using OpenIddict.Server; |
|||
using OpenIddict.Server.SystemNetHttp; |
|||
|
|||
namespace Microsoft.Extensions.DependencyInjection; |
|||
|
|||
/// <summary>
|
|||
/// Exposes extensions allowing to register the OpenIddict server/System.Net.Http integration services.
|
|||
/// </summary>
|
|||
public static class OpenIddictServerSystemNetHttpExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Registers the OpenIddict server/System.Net.Http integration services in the DI container.
|
|||
/// </summary>
|
|||
/// <param name="builder">The services builder used by OpenIddict to register new services.</param>
|
|||
/// <remarks>This extension can be safely called multiple times.</remarks>
|
|||
/// <returns>The <see cref="OpenIddictServerSystemNetHttpBuilder"/> instance.</returns>
|
|||
public static OpenIddictServerSystemNetHttpBuilder UseSystemNetHttp(this OpenIddictServerBuilder builder) |
|||
{ |
|||
ArgumentNullException.ThrowIfNull(builder); |
|||
|
|||
builder.Services.AddHttpClient(); |
|||
|
|||
// Register the built-in event handlers used by the OpenIddict System.Net.Http components.
|
|||
// Note: the order used here is not important, as the actual order is set in the options.
|
|||
builder.Services.TryAdd(OpenIddictServerSystemNetHttpHandlers.DefaultHandlers.Select(descriptor => descriptor.ServiceDescriptor)); |
|||
|
|||
// Register the built-in filters used by the default OpenIddict System.Net.Http event handlers.
|
|||
builder.Services.TryAddSingleton<RequireClientIdMetadataDocumentSupportEnabled>(); |
|||
|
|||
// Note: TryAddEnumerable() is used here to ensure the initializers are registered only once.
|
|||
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< |
|||
IConfigureOptions<OpenIddictServerOptions>, OpenIddictServerSystemNetHttpConfiguration>()); |
|||
|
|||
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< |
|||
IConfigureOptions<HttpClientFactoryOptions>, OpenIddictServerSystemNetHttpConfiguration>()); |
|||
|
|||
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< |
|||
IPostConfigureOptions<HttpClientFactoryOptions>, OpenIddictServerSystemNetHttpConfiguration>()); |
|||
|
|||
return new OpenIddictServerSystemNetHttpBuilder(builder.Services); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Registers the OpenIddict server/System.Net.Http integration services in the DI container.
|
|||
/// </summary>
|
|||
/// <param name="builder">The services builder used by OpenIddict to register new services.</param>
|
|||
/// <param name="configuration">The configuration delegate used to configure the server services.</param>
|
|||
/// <remarks>This extension can be safely called multiple times.</remarks>
|
|||
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
|
|||
public static OpenIddictServerBuilder UseSystemNetHttp( |
|||
this OpenIddictServerBuilder builder, Action<OpenIddictServerSystemNetHttpBuilder> configuration) |
|||
{ |
|||
ArgumentNullException.ThrowIfNull(builder); |
|||
ArgumentNullException.ThrowIfNull(configuration); |
|||
|
|||
configuration(builder.UseSystemNetHttp()); |
|||
|
|||
return builder; |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
/* |
|||
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
|||
* See https://github.com/openiddict/openiddict-core for more information concerning
|
|||
* the license and the contributors participating to this project. |
|||
*/ |
|||
|
|||
using System.ComponentModel; |
|||
|
|||
namespace OpenIddict.Server.SystemNetHttp; |
|||
|
|||
[EditorBrowsable(EditorBrowsableState.Advanced)] |
|||
public static class OpenIddictServerSystemNetHttpHandlerFilters |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a filter that excludes the associated handlers
|
|||
/// if CIMD support was not enabled in the server options.
|
|||
/// </summary>
|
|||
public sealed class RequireClientIdMetadataDocumentSupportEnabled : IOpenIddictServerHandlerFilter<BaseContext> |
|||
{ |
|||
/// <inheritdoc/>
|
|||
public ValueTask<bool> IsActiveAsync(BaseContext context) |
|||
{ |
|||
ArgumentNullException.ThrowIfNull(context); |
|||
|
|||
return new(context.Options.EnableClientIdMetadataDocumentSupport); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,228 @@ |
|||
/* |
|||
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
|||
* See https://github.com/openiddict/openiddict-core for more information concerning
|
|||
* the license and the contributors participating to this project. |
|||
*/ |
|||
|
|||
using System.Collections.Immutable; |
|||
using System.Net.Http; |
|||
using System.Net.Http.Headers; |
|||
using System.Text.Json; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Options; |
|||
|
|||
namespace OpenIddict.Server.SystemNetHttp; |
|||
|
|||
public static partial class OpenIddictServerSystemNetHttpHandlers |
|||
{ |
|||
public static class Authentication |
|||
{ |
|||
public static ImmutableArray<OpenIddictServerHandlerDescriptor> DefaultHandlers { get; } = |
|||
[ |
|||
FetchClientIdMetadataDocument.Descriptor |
|||
]; |
|||
|
|||
/// <summary>
|
|||
/// Contains the logic responsible for fetching the CIMD metadata document
|
|||
/// when the client_id is an HTTPS URL and no pre-registered client was found.
|
|||
/// </summary>
|
|||
public sealed class FetchClientIdMetadataDocument : IOpenIddictServerHandler<ValidateAuthorizationRequestContext> |
|||
{ |
|||
private readonly IHttpClientFactory _factory; |
|||
private readonly IOptionsMonitor<OpenIddictServerOptions> _serverOptions; |
|||
private readonly IOptionsMonitor<OpenIddictServerSystemNetHttpOptions> _httpOptions; |
|||
|
|||
public FetchClientIdMetadataDocument( |
|||
IHttpClientFactory factory, |
|||
IOptionsMonitor<OpenIddictServerOptions> serverOptions, |
|||
IOptionsMonitor<OpenIddictServerSystemNetHttpOptions> httpOptions) |
|||
{ |
|||
_factory = factory ?? throw new ArgumentNullException(nameof(factory)); |
|||
_serverOptions = serverOptions ?? throw new ArgumentNullException(nameof(serverOptions)); |
|||
_httpOptions = httpOptions ?? throw new ArgumentNullException(nameof(httpOptions)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the default descriptor definition assigned to this handler.
|
|||
/// </summary>
|
|||
public static OpenIddictServerHandlerDescriptor Descriptor { get; } |
|||
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateAuthorizationRequestContext>() |
|||
.AddFilter<RequireClientIdMetadataDocumentSupportEnabled>() |
|||
.UseScopedHandler<FetchClientIdMetadataDocument>() |
|||
// Run after ValidateAuthentication and before RestorePushedAuthorizationRequestParameters.
|
|||
.SetOrder(OpenIddictServerHandlers.Authentication.ValidateAuthentication.Descriptor.Order + 500) |
|||
.SetType(OpenIddictServerHandlerType.BuiltIn) |
|||
.Build(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public async ValueTask HandleAsync(ValidateAuthorizationRequestContext context) |
|||
{ |
|||
ArgumentNullException.ThrowIfNull(context); |
|||
|
|||
// Only proceed if the transaction was flagged as requiring CIMD fetch.
|
|||
// This flag is set by the modified ValidateClientId handler (Phase 3) when
|
|||
// FindByClientIdAsync() returns null and the client_id is a valid CIMD URL.
|
|||
if (!context.Transaction.Properties.TryGetValue( |
|||
OpenIddictServerSystemNetHttpConstants.Properties.ClientIdMetadataDocumentFetchRequired, out var value) || |
|||
value is not true) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var clientId = context.Transaction.Request?.ClientId; |
|||
if (string.IsNullOrEmpty(clientId)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (!Uri.TryCreate(clientId, UriKind.Absolute, out var clientUri) || |
|||
!string.Equals(clientUri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
context.Reject( |
|||
error: Errors.InvalidClient, |
|||
description: "The specified client_id is not a valid HTTPS URL.", |
|||
uri: null); |
|||
|
|||
return; |
|||
} |
|||
|
|||
var options = _serverOptions.CurrentValue; |
|||
|
|||
var assembly = typeof(OpenIddictServerSystemNetHttpOptions).Assembly.GetName(); |
|||
var client = _factory.CreateClient(assembly.Name!); |
|||
|
|||
// Apply size limit and timeout from server options.
|
|||
client.Timeout = options.ClientIdMetadataDocumentFetchTimeout; |
|||
|
|||
using var request = new HttpRequestMessage(HttpMethod.Get, clientUri); |
|||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); |
|||
|
|||
// Attach User-Agent header if configured.
|
|||
var httpOptions = _httpOptions.CurrentValue; |
|||
if (httpOptions.ProductInformation is not null) |
|||
{ |
|||
request.Headers.UserAgent.Add(httpOptions.ProductInformation); |
|||
} |
|||
|
|||
using var response = await client.SendAsync(request, context.CancellationToken); |
|||
|
|||
if (!response.IsSuccessStatusCode) |
|||
{ |
|||
context.Logger.LogWarning( |
|||
"The CIMD metadata document fetch for client_id '{ClientId}' failed with status code {StatusCode}.", |
|||
clientId, response.StatusCode); |
|||
|
|||
context.Reject( |
|||
error: Errors.InvalidClient, |
|||
description: "The client_id metadata document could not be retrieved.", |
|||
uri: null); |
|||
|
|||
return; |
|||
} |
|||
|
|||
// Enforce size limit: read the response body with a size check.
|
|||
var sizeLimit = options.ClientIdMetadataDocumentSizeLimit; |
|||
var content = response.Content; |
|||
|
|||
#if SUPPORTS_STREAM_MEMORY_METHODS
|
|||
using var stream = await content.ReadAsStreamAsync(context.CancellationToken); |
|||
#else
|
|||
using var stream = await content.ReadAsStreamAsync(); |
|||
#endif
|
|||
var buffer = new byte[sizeLimit + 1]; |
|||
var totalRead = 0; |
|||
int bytesRead; |
|||
|
|||
while (totalRead < buffer.Length && |
|||
(bytesRead = await stream.ReadAsync(buffer.AsMemory(totalRead, buffer.Length - totalRead), context.CancellationToken)) > 0) |
|||
{ |
|||
totalRead += bytesRead; |
|||
} |
|||
|
|||
if (totalRead > sizeLimit) |
|||
{ |
|||
context.Logger.LogWarning( |
|||
"The CIMD metadata document for client_id '{ClientId}' exceeds the maximum allowed size of {SizeLimit} bytes.", |
|||
clientId, sizeLimit); |
|||
|
|||
context.Reject( |
|||
error: Errors.InvalidClient, |
|||
description: "The client_id metadata document exceeds the maximum allowed size.", |
|||
uri: null); |
|||
|
|||
return; |
|||
} |
|||
|
|||
// Parse the JSON metadata document.
|
|||
JsonDocument document; |
|||
try |
|||
{ |
|||
document = JsonDocument.Parse(buffer.AsMemory(0, totalRead)); |
|||
} |
|||
catch (JsonException) |
|||
{ |
|||
context.Logger.LogWarning( |
|||
"The CIMD metadata document for client_id '{ClientId}' is not valid JSON.", |
|||
clientId); |
|||
|
|||
context.Reject( |
|||
error: Errors.InvalidClient, |
|||
description: "The client_id metadata document is not valid JSON.", |
|||
uri: null); |
|||
|
|||
return; |
|||
} |
|||
|
|||
// Validate that the document contains a client_id field matching the URL (exact string comparison).
|
|||
if (!document.RootElement.TryGetProperty("client_id", out var clientIdElement) || |
|||
clientIdElement.ValueKind != JsonValueKind.String || |
|||
!string.Equals(clientIdElement.GetString(), clientId, StringComparison.Ordinal)) |
|||
{ |
|||
context.Logger.LogWarning( |
|||
"The CIMD metadata document's client_id field does not match the expected value '{ClientId}'.", |
|||
clientId); |
|||
|
|||
context.Reject( |
|||
error: Errors.InvalidClient, |
|||
description: "The client_id in the metadata document does not match the requested client_id.", |
|||
uri: null); |
|||
|
|||
document.Dispose(); |
|||
return; |
|||
} |
|||
|
|||
// Validate that the client does not use forbidden authentication methods.
|
|||
// CIMD clients MUST NOT use client_secret_post, client_secret_basic, or client_secret_jwt.
|
|||
if (document.RootElement.TryGetProperty("token_endpoint_auth_method", out var authMethodElement) && |
|||
authMethodElement.ValueKind == JsonValueKind.String) |
|||
{ |
|||
var authMethod = authMethodElement.GetString(); |
|||
if (string.Equals(authMethod, "client_secret_post", StringComparison.Ordinal) || |
|||
string.Equals(authMethod, "client_secret_basic", StringComparison.Ordinal) || |
|||
string.Equals(authMethod, "client_secret_jwt", StringComparison.Ordinal)) |
|||
{ |
|||
context.Logger.LogWarning( |
|||
"The CIMD metadata document for client_id '{ClientId}' specifies a forbidden authentication method '{AuthMethod}'.", |
|||
clientId, authMethod); |
|||
|
|||
context.Reject( |
|||
error: Errors.InvalidClient, |
|||
description: "The client_id metadata document specifies a forbidden authentication method.", |
|||
uri: null); |
|||
|
|||
document.Dispose(); |
|||
return; |
|||
} |
|||
} |
|||
|
|||
// Store the parsed metadata document on the transaction properties for use by subsequent handlers.
|
|||
context.Transaction.Properties[OpenIddictServerSystemNetHttpConstants.Properties.ClientIdMetadataDocument] = document; |
|||
|
|||
context.Logger.LogInformation( |
|||
"The CIMD metadata document for client_id '{ClientId}' was successfully fetched and validated.", |
|||
clientId); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
/* |
|||
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
|||
* See https://github.com/openiddict/openiddict-core for more information concerning
|
|||
* the license and the contributors participating to this project. |
|||
*/ |
|||
|
|||
using System.Collections.Immutable; |
|||
using System.ComponentModel; |
|||
using System.Diagnostics; |
|||
using System.IO.Compression; |
|||
using System.Net.Http; |
|||
using System.Net.Http.Headers; |
|||
using System.Text; |
|||
using System.Text.Json; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Options; |
|||
using static OpenIddict.Server.SystemNetHttp.OpenIddictServerSystemNetHttpConstants; |
|||
|
|||
namespace OpenIddict.Server.SystemNetHttp; |
|||
|
|||
[EditorBrowsable(EditorBrowsableState.Never)] |
|||
public static partial class OpenIddictServerSystemNetHttpHandlers |
|||
{ |
|||
public static ImmutableArray<OpenIddictServerHandlerDescriptor> DefaultHandlers { get; } = |
|||
[ |
|||
.. Authentication.DefaultHandlers |
|||
]; |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
/* |
|||
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
|||
* See https://github.com/openiddict/openiddict-core for more information concerning
|
|||
* the license and the contributors participating to this project. |
|||
*/ |
|||
|
|||
using OpenIddict.Server; |
|||
|
|||
namespace System.Net.Http; |
|||
|
|||
/// <summary>
|
|||
/// Exposes companion extensions for the OpenIddict server/System.Net.Http integration.
|
|||
/// </summary>
|
|||
public static class OpenIddictServerSystemNetHttpHelpers |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the <see cref="HttpClient"/> associated with the current context.
|
|||
/// </summary>
|
|||
/// <param name="transaction">The transaction instance.</param>
|
|||
/// <returns>The <see cref="HttpClient"/> instance or <see langword="null"/> if it couldn't be found.</returns>
|
|||
public static HttpClient? GetHttpClient(this OpenIddictServerTransaction transaction) |
|||
=> transaction.GetProperty<HttpClient>(typeof(HttpClient).FullName!); |
|||
|
|||
/// <summary>
|
|||
/// Gets the <see cref="HttpRequestMessage"/> associated with the current context.
|
|||
/// </summary>
|
|||
/// <param name="transaction">The transaction instance.</param>
|
|||
/// <returns>The <see cref="HttpRequestMessage"/> instance or <see langword="null"/> if it couldn't be found.</returns>
|
|||
public static HttpRequestMessage? GetHttpRequestMessage(this OpenIddictServerTransaction transaction) |
|||
=> transaction.GetProperty<HttpRequestMessage>(typeof(HttpRequestMessage).FullName!); |
|||
|
|||
/// <summary>
|
|||
/// Gets the <see cref="HttpResponseMessage"/> associated with the current context.
|
|||
/// </summary>
|
|||
/// <param name="transaction">The transaction instance.</param>
|
|||
/// <returns>The <see cref="HttpResponseMessage"/> instance or <see langword="null"/> if it couldn't be found.</returns>
|
|||
public static HttpResponseMessage? GetHttpResponseMessage(this OpenIddictServerTransaction transaction) |
|||
=> transaction.GetProperty<HttpResponseMessage>(typeof(HttpResponseMessage).FullName!); |
|||
} |
|||
@ -0,0 +1,83 @@ |
|||
/* |
|||
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
|||
* See https://github.com/openiddict/openiddict-core for more information concerning
|
|||
* the license and the contributors participating to this project. |
|||
*/ |
|||
|
|||
using System.Net; |
|||
using System.Net.Http; |
|||
using System.Net.Http.Headers; |
|||
using System.Net.Mail; |
|||
using Polly; |
|||
using Polly.Extensions.Http; |
|||
|
|||
#if SUPPORTS_HTTP_CLIENT_RESILIENCE
|
|||
using Microsoft.Extensions.Http.Resilience; |
|||
#endif
|
|||
|
|||
namespace OpenIddict.Server.SystemNetHttp; |
|||
|
|||
/// <summary>
|
|||
/// Provides various settings needed to configure the OpenIddict server/System.Net.Http integration.
|
|||
/// </summary>
|
|||
public sealed class OpenIddictServerSystemNetHttpOptions |
|||
{ |
|||
/// <summary>
|
|||
/// Gets or sets the HTTP Polly error policy used by the internal OpenIddict HTTP clients.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Note: on .NET 8.0 and higher, this property is set to <see langword="null"/> by default.
|
|||
/// </remarks>
|
|||
public IAsyncPolicy<HttpResponseMessage>? HttpErrorPolicy { get; set; } |
|||
#if !SUPPORTS_HTTP_CLIENT_RESILIENCE
|
|||
= HttpPolicyExtensions.HandleTransientHttpError() |
|||
.OrResult(static response => response.StatusCode is HttpStatusCode.NotFound) |
|||
.WaitAndRetryAsync(4, static attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt))); |
|||
#endif
|
|||
|
|||
#if SUPPORTS_HTTP_CLIENT_RESILIENCE
|
|||
/// <summary>
|
|||
/// Gets or sets the HTTP resilience pipeline used by the internal OpenIddict HTTP clients.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Note: this property is not used when <see cref="HttpErrorPolicy"/>
|
|||
/// is explicitly set to a non-<see langword="null"/> value.
|
|||
/// </remarks>
|
|||
public ResiliencePipeline<HttpResponseMessage>? HttpResiliencePipeline { get; set; } |
|||
= new ResiliencePipelineBuilder<HttpResponseMessage>() |
|||
.AddRetry(new HttpRetryStrategyOptions |
|||
{ |
|||
DelayGenerator = static arguments => new( |
|||
TimeSpan.FromSeconds(Math.Pow(2, arguments.AttemptNumber))), |
|||
MaxRetryAttempts = 4, |
|||
ShouldHandle = static arguments => new( |
|||
HttpClientResiliencePredicates.IsTransient(arguments.Outcome) || |
|||
arguments.Outcome.Result?.StatusCode is HttpStatusCode.NotFound) |
|||
}) |
|||
.Build(); |
|||
#endif
|
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the contact mail address used in the "From" header that is
|
|||
/// attached to the HTTP requests sent by the OpenIddict server services.
|
|||
/// </summary>
|
|||
public MailAddress? ContactAddress { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the product information used in the "User-Agent" header that is
|
|||
/// attached to the HTTP requests sent by the OpenIddict server services.
|
|||
/// </summary>
|
|||
public ProductInfoHeaderValue? ProductInformation { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the user-defined actions used to amend the <see cref="HttpClient"/>
|
|||
/// instances created by the OpenIddict server/System.Net.Http integration.
|
|||
/// </summary>
|
|||
public List<Action<HttpClient>> HttpClientActions { get; } = []; |
|||
|
|||
/// <summary>
|
|||
/// Gets the user-defined actions used to amend the <see cref="HttpClientHandler"/>
|
|||
/// instances created by the OpenIddict server/System.Net.Http integration.
|
|||
/// </summary>
|
|||
public List<Action<HttpClientHandler>> HttpClientHandlerActions { get; } = []; |
|||
} |
|||
Loading…
Reference in new issue