Browse Source

Add CIMD (Client ID Metadata Document) support (Phases 1-4)

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 endpoint
pull/2416/head
Thor Arne Johansen 7 days ago
parent
commit
1f6d309833
  1. 1
      OpenIddict.slnx
  2. 24
      sandbox/OpenIddict.Sandbox.AspNetCore.CimdServer/AuthorizationController.cs
  3. 1
      sandbox/OpenIddict.Sandbox.AspNetCore.CimdServer/OpenIddict.Sandbox.AspNetCore.CimdServer.csproj
  4. 18
      sandbox/OpenIddict.Sandbox.AspNetCore.CimdServer/Program.cs
  5. 10
      sandbox/OpenIddict.Sandbox.AspNetCore.CimdServer/appsettings.json
  6. 1
      src/OpenIddict.Abstractions/OpenIddictConstants.cs
  7. 45
      src/OpenIddict.Server.SystemNetHttp/OpenIddict.Server.SystemNetHttp.csproj
  8. 208
      src/OpenIddict.Server.SystemNetHttp/OpenIddictServerSystemNetHttpBuilder.cs
  9. 157
      src/OpenIddict.Server.SystemNetHttp/OpenIddictServerSystemNetHttpConfiguration.cs
  10. 37
      src/OpenIddict.Server.SystemNetHttp/OpenIddictServerSystemNetHttpConstants.cs
  11. 69
      src/OpenIddict.Server.SystemNetHttp/OpenIddictServerSystemNetHttpExtensions.cs
  12. 28
      src/OpenIddict.Server.SystemNetHttp/OpenIddictServerSystemNetHttpHandlerFilters.cs
  13. 228
      src/OpenIddict.Server.SystemNetHttp/OpenIddictServerSystemNetHttpHandlers.Authentication.cs
  14. 29
      src/OpenIddict.Server.SystemNetHttp/OpenIddictServerSystemNetHttpHandlers.cs
  15. 39
      src/OpenIddict.Server.SystemNetHttp/OpenIddictServerSystemNetHttpHelpers.cs
  16. 83
      src/OpenIddict.Server.SystemNetHttp/OpenIddictServerSystemNetHttpOptions.cs
  17. 9
      src/OpenIddict.Server/OpenIddictServerBuilder.cs
  18. 124
      src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs
  19. 6
      src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs
  20. 5
      src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs
  21. 84
      src/OpenIddict.Server/OpenIddictServerHandlers.cs
  22. 26
      src/OpenIddict.Server/OpenIddictServerOptions.cs

1
OpenIddict.slnx

@ -73,6 +73,7 @@
<Project Path="src/OpenIddict.Server.AspNetCore/OpenIddict.Server.AspNetCore.csproj" />
<Project Path="src/OpenIddict.Server.DataProtection/OpenIddict.Server.DataProtection.csproj" />
<Project Path="src/OpenIddict.Server.Owin/OpenIddict.Server.Owin.csproj" />
<Project Path="src/OpenIddict.Server.SystemNetHttp/OpenIddict.Server.SystemNetHttp.csproj" />
<Project Path="src/OpenIddict.Server/OpenIddict.Server.csproj" />
<Project Path="src/OpenIddict.Validation/OpenIddict.Validation.csproj" />
<Project Path="src/OpenIddict.Validation.AspNetCore/OpenIddict.Validation.AspNetCore.csproj" />

24
sandbox/OpenIddict.Sandbox.AspNetCore.CimdServer/AuthorizationController.cs

@ -66,9 +66,6 @@ public class AuthorizationController : Controller
var userEntity = await _userManager.GetUserAsync(result.Principal) ??
throw new InvalidOperationException("The user details cannot be retrieved.");
var application = await _applicationManager.FindByClientIdAsync(request.ClientId!) ??
throw new InvalidOperationException("Details concerning the calling client application cannot be found.");
// Auto-approve consent for this demonstrator.
var identity = new ClaimsIdentity(
authenticationType: TokenValidationParameters.DefaultAuthenticationType,
@ -82,14 +79,21 @@ public class AuthorizationController : Controller
identity.SetScopes(request.GetScopes());
identity.SetResources(await _scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync());
var authorization = await _authorizationManager.CreateAsync(
identity: identity,
subject: await _userManager.GetUserIdAsync(userEntity),
client: (await _applicationManager.GetIdAsync(application))!,
type: AuthorizationTypes.Permanent,
scopes: identity.GetScopes());
// For CIMD clients (URL-based client_id with no pre-registration), skip
// the application lookup and authorization entry creation.
var application = await _applicationManager.FindByClientIdAsync(request.ClientId!);
if (application is not null)
{
var authorization = await _authorizationManager.CreateAsync(
identity: identity,
subject: await _userManager.GetUserIdAsync(userEntity),
client: (await _applicationManager.GetIdAsync(application))!,
type: AuthorizationTypes.Permanent,
scopes: identity.GetScopes());
identity.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization));
}
identity.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization));
identity.SetDestinations(GetDestinations);
return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);

1
sandbox/OpenIddict.Sandbox.AspNetCore.CimdServer/OpenIddict.Sandbox.AspNetCore.CimdServer.csproj

@ -9,6 +9,7 @@
<ProjectReference Include="..\..\src\OpenIddict.Core\OpenIddict.Core.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Server.AspNetCore\OpenIddict.Server.AspNetCore.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Server.DataProtection\OpenIddict.Server.DataProtection.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Server.SystemNetHttp\OpenIddict.Server.SystemNetHttp.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Validation.AspNetCore\OpenIddict.Validation.AspNetCore.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Validation.ServerIntegration\OpenIddict.Validation.ServerIntegration.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.EntityFrameworkCore\OpenIddict.EntityFrameworkCore.csproj" />

18
sandbox/OpenIddict.Sandbox.AspNetCore.CimdServer/Program.cs

@ -38,12 +38,17 @@ builder.Services.AddOpenIddict()
options.RegisterScopes("openid", "profile", "email");
// Enable Client ID Metadata Document (CIMD) support.
options.EnableClientIdMetadataDocumentSupport();
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
options.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough()
.EnableTokenEndpointPassthrough();
options.UseSystemNetHttp();
})
.AddValidation(options =>
{
@ -61,4 +66,17 @@ app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
// Serve a CIMD metadata document for testing.
// The client_id URL is: https://localhost:7295/clients/cimd-test
app.MapGet("/clients/cimd-test", () => Results.Json(new
{
client_id = "https://localhost:7295/clients/cimd-test",
client_name = "CIMD Test Client",
redirect_uris = new[] { "http://localhost/callback" },
grant_types = new[] { "authorization_code" },
response_types = new[] { "code" },
token_endpoint_auth_method = "none"
}));
Console.WriteLine("CIMD Server starting on https://localhost:7295");
app.Run();

10
sandbox/OpenIddict.Sandbox.AspNetCore.CimdServer/appsettings.json

@ -0,0 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"OpenIddict": "Debug"
}
}
}

1
src/OpenIddict.Abstractions/OpenIddictConstants.cs

@ -286,6 +286,7 @@ public static class OpenIddictConstants
public const string AcrValuesSupported = "acr_values_supported";
public const string AuthorizationEndpoint = "authorization_endpoint";
public const string AuthorizationResponseIssParameterSupported = "authorization_response_iss_parameter_supported";
public const string ClientIdMetadataDocumentSupported = "client_id_metadata_document_supported";
public const string ClaimsLocalesSupported = "claims_locales_supported";
public const string ClaimsParameterSupported = "claims_parameter_supported";
public const string ClaimsSupported = "claims_supported";

45
src/OpenIddict.Server.SystemNetHttp/OpenIddict.Server.SystemNetHttp.csproj

@ -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>

208
src/OpenIddict.Server.SystemNetHttp/OpenIddictServerSystemNetHttpBuilder.cs

@ -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();
}

157
src/OpenIddict.Server.SystemNetHttp/OpenIddictServerSystemNetHttpConfiguration.cs

@ -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;
});
}
}

37
src/OpenIddict.Server.SystemNetHttp/OpenIddictServerSystemNetHttpConstants.cs

@ -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";
}
}

69
src/OpenIddict.Server.SystemNetHttp/OpenIddictServerSystemNetHttpExtensions.cs

@ -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;
}
}

28
src/OpenIddict.Server.SystemNetHttp/OpenIddictServerSystemNetHttpHandlerFilters.cs

@ -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);
}
}
}

228
src/OpenIddict.Server.SystemNetHttp/OpenIddictServerSystemNetHttpHandlers.Authentication.cs

@ -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);
}
}
}
}

29
src/OpenIddict.Server.SystemNetHttp/OpenIddictServerSystemNetHttpHandlers.cs

@ -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
];
}

39
src/OpenIddict.Server.SystemNetHttp/OpenIddictServerSystemNetHttpHelpers.cs

@ -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!);
}

83
src/OpenIddict.Server.SystemNetHttp/OpenIddictServerSystemNetHttpOptions.cs

@ -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; } = [];
}

9
src/OpenIddict.Server/OpenIddictServerBuilder.cs

@ -1943,6 +1943,15 @@ public sealed class OpenIddictServerBuilder
public OpenIddictServerBuilder UseReferenceRefreshTokens()
=> Configure(options => options.UseReferenceRefreshTokens = true);
/// <summary>
/// Enables support for client ID metadata documents (CIMD), allowing clients
/// to use an HTTPS URL as their client identifier. The server will fetch the
/// metadata document from that URL when the client is not pre-registered.
/// </summary>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
public OpenIddictServerBuilder EnableClientIdMetadataDocumentSupport()
=> Configure(options => options.EnableClientIdMetadataDocumentSupport = true);
/// <summary>
/// Enables authorization request storage, so that authorization requests
/// are automatically stored in the token store, which allows flowing

124
src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs

@ -1391,6 +1391,13 @@ public static partial class OpenIddictServerHandlers
if (!context.Options.EnableDegradedMode)
{
// Skip for CIMD clients (they are public, so the confidential-client downgrade check doesn't apply).
if (context.Transaction.Properties.TryGetValue(
".ClientIdMetadataDocumentFetchRequired", out var cimdFlag) && cimdFlag is true)
{
return;
}
if (_applicationManager is null)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
@ -1449,6 +1456,74 @@ public static partial class OpenIddictServerHandlers
Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId));
// For CIMD clients, validate the redirect_uri against the metadata document.
if (context.Transaction.Properties.TryGetValue(
".ClientIdMetadataDocumentFetchRequired", out var cimdFlag) && cimdFlag is true)
{
// A redirect_uri is always required for CIMD clients.
if (string.IsNullOrEmpty(context.RedirectUri))
{
context.Logger.LogInformation(6033, SR.GetResourceString(SR.ID6033), Parameters.RedirectUri);
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2029(Parameters.RedirectUri),
uri: SR.FormatID8000(SR.ID2029));
return;
}
// Retrieve the CIMD metadata document from the transaction properties.
if (!context.Transaction.Properties.TryGetValue(
".ClientIdMetadataDocument", out var documentObj) ||
documentObj is not System.Text.Json.JsonDocument document)
{
context.Reject(
error: Errors.InvalidClient,
description: "The client_id metadata document is not available.",
uri: null);
return;
}
// Validate that the redirect_uri matches one from the metadata document.
if (!document.RootElement.TryGetProperty("redirect_uris", out var redirectUrisElement) ||
redirectUrisElement.ValueKind != System.Text.Json.JsonValueKind.Array)
{
context.Reject(
error: Errors.InvalidClient,
description: "The client_id metadata document does not contain redirect_uris.",
uri: null);
return;
}
var redirectUriFound = false;
foreach (var element in redirectUrisElement.EnumerateArray())
{
if (element.ValueKind == System.Text.Json.JsonValueKind.String &&
string.Equals(element.GetString(), context.RedirectUri, StringComparison.Ordinal))
{
redirectUriFound = true;
break;
}
}
if (!redirectUriFound)
{
context.Logger.LogInformation(6046, SR.GetResourceString(SR.ID6046), context.RedirectUri);
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2043(Parameters.RedirectUri),
uri: SR.FormatID8000(SR.ID2043));
return;
}
return;
}
var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0032));
@ -1638,6 +1713,13 @@ public static partial class OpenIddictServerHandlers
Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId));
// Skip for CIMD clients (no pre-registered application to look up).
if (context.Transaction.Properties.TryGetValue(
".ClientIdMetadataDocumentFetchRequired", out var cimdFlag) && cimdFlag is true)
{
return;
}
var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0032));
@ -1688,6 +1770,13 @@ public static partial class OpenIddictServerHandlers
Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId));
// Skip for CIMD clients (no pre-registered application to look up).
if (context.Transaction.Properties.TryGetValue(
".ClientIdMetadataDocumentFetchRequired", out var cimdFlag) && cimdFlag is true)
{
return;
}
var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0032));
@ -1783,6 +1872,13 @@ public static partial class OpenIddictServerHandlers
Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId));
// Skip for CIMD clients (no pre-registered application to look up).
if (context.Transaction.Properties.TryGetValue(
".ClientIdMetadataDocumentFetchRequired", out var cimdFlag) && cimdFlag is true)
{
return;
}
var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0032));
@ -1858,6 +1954,13 @@ public static partial class OpenIddictServerHandlers
Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId));
// Skip for CIMD clients (no pre-registered application to look up).
if (context.Transaction.Properties.TryGetValue(
".ClientIdMetadataDocumentFetchRequired", out var cimdFlag) && cimdFlag is true)
{
return;
}
var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0032));
@ -1919,6 +2022,13 @@ public static partial class OpenIddictServerHandlers
Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId));
// Skip for CIMD clients (no pre-registered application to look up).
if (context.Transaction.Properties.TryGetValue(
".ClientIdMetadataDocumentFetchRequired", out var cimdFlag) && cimdFlag is true)
{
return;
}
var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0032));
@ -1972,6 +2082,13 @@ public static partial class OpenIddictServerHandlers
Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId));
// Skip for CIMD clients (no pre-registered application to look up).
if (context.Transaction.Properties.TryGetValue(
".ClientIdMetadataDocumentFetchRequired", out var cimdFlag) && cimdFlag is true)
{
return;
}
// If a request token principal with the correct type could be extracted, the request is always
// considered valid, whether the pushed authorization requests requirement is enforced or not.
var type = context.RequestTokenPrincipal?.GetClaim(Claims.Private.RequestTokenType);
@ -2039,6 +2156,13 @@ public static partial class OpenIddictServerHandlers
Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId));
// Skip for CIMD clients (no pre-registered application to look up).
if (context.Transaction.Properties.TryGetValue(
".ClientIdMetadataDocumentFetchRequired", out var cimdFlag) && cimdFlag is true)
{
return;
}
// If a code_challenge was provided or if no authorization code is requested, the request is always
// considered valid, whether the proof key for code exchange requirement is enforced or not.
if (!string.IsNullOrEmpty(context.Request.CodeChallenge) || !context.Request.HasResponseType(ResponseTypes.Code))

6
src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs

@ -795,6 +795,12 @@ public static partial class OpenIddictServerHandlers
context.Metadata[Metadata.RequestUriParameterSupported] = false;
context.Metadata[Metadata.TlsClientCertificateBoundAccessTokens] = false;
// If CIMD (Client ID Metadata Document) support is enabled, advertise it in the discovery document.
if (context.Options.EnableClientIdMetadataDocumentSupport)
{
context.Metadata[Metadata.ClientIdMetadataDocumentSupported] = true;
}
// As of 3.2.0, OpenIddict automatically returns an "iss" parameter containing its identity as
// part of authorization responses to help clients mitigate mix-up attacks. For more information,
// see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-iss-auth-resp-05.

5
src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs

@ -1438,7 +1438,10 @@ public static partial class OpenIddictServerHandlers
};
// If the client application is known, associate it with the token.
if (!string.IsNullOrEmpty(context.ClientId))
// For CIMD clients, there is no pre-registered application entity.
if (!string.IsNullOrEmpty(context.ClientId) &&
!(context.Transaction.Properties.TryGetValue(
".ClientIdMetadataDocumentFetchRequired", out var cimdFlag) && cimdFlag is true))
{
var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0017));

84
src/OpenIddict.Server/OpenIddictServerHandlers.cs

@ -1033,6 +1033,26 @@ public static partial class OpenIddictServerHandlers
var application = await _applicationManager.FindByClientIdAsync(context.ClientId);
if (application is null)
{
// If CIMD support is enabled and the client_id is a valid HTTPS URL,
// flag the transaction for metadata document fetching instead of rejecting.
if (context.Options.EnableClientIdMetadataDocumentSupport &&
Uri.TryCreate(context.ClientId, UriKind.Absolute, out var clientUri) &&
string.Equals(clientUri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) &&
!string.IsNullOrEmpty(clientUri.AbsolutePath) &&
clientUri.AbsolutePath is not "/" &&
string.IsNullOrEmpty(clientUri.Fragment) &&
string.IsNullOrEmpty(clientUri.UserInfo))
{
context.Logger.LogInformation(
"The client_id '{ClientId}' was not found in the store but matches CIMD URL format. " +
"A metadata document fetch will be attempted.", context.ClientId);
// Flag the transaction so the CIMD fetch handler knows to process this request.
context.Transaction.Properties[".ClientIdMetadataDocumentFetchRequired"] = true;
return;
}
context.Logger.LogInformation(6221, SR.GetResourceString(SR.ID6221), context.ClientId);
context.Reject(
@ -1097,6 +1117,25 @@ public static partial class OpenIddictServerHandlers
return;
}
// Skip client type validation for CIMD clients (they are treated as public clients).
if (context.Transaction.Properties.TryGetValue(
".ClientIdMetadataDocumentFetchRequired", out var cimdFlag) && cimdFlag is true)
{
// CIMD clients MUST NOT use client_secret_post, client_secret_basic, or client_secret_jwt.
// Reject requests containing a client_secret when the client is a CIMD client.
if (!string.IsNullOrEmpty(context.ClientSecret))
{
context.Reject(
error: Errors.InvalidClient,
description: "CIMD clients cannot use client secret authentication.",
uri: null);
return;
}
return;
}
var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0032));
@ -3206,7 +3245,10 @@ public static partial class OpenIddictServerHandlers
descriptor.Scopes.UnionWith(context.Principal.GetScopes());
// If the client application is known, associate it to the authorization.
if (!string.IsNullOrEmpty(context.Request.ClientId))
// For CIMD clients, there is no pre-registered application entity.
if (!string.IsNullOrEmpty(context.Request.ClientId) &&
!(context.Transaction.Properties.TryGetValue(
".ClientIdMetadataDocumentFetchRequired", out var cimdFlag) && cimdFlag is true))
{
var application = await _applicationManager.FindByClientIdAsync(context.Request.ClientId) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0017));
@ -3339,7 +3381,10 @@ public static partial class OpenIddictServerHandlers
var lifetime = context.Principal.GetAccessTokenLifetime();
// If the client to which the token is returned is known, use the attached setting if available.
if (lifetime is null && !context.Options.EnableDegradedMode && !string.IsNullOrEmpty(context.ClientId))
// Skip for CIMD clients (no pre-registered application to look up).
if (lifetime is null && !context.Options.EnableDegradedMode && !string.IsNullOrEmpty(context.ClientId) &&
!(context.Transaction.Properties.TryGetValue(
".ClientIdMetadataDocumentFetchRequired", out var cimdFlag) && cimdFlag is true))
{
if (_applicationManager is null)
{
@ -3466,7 +3511,10 @@ public static partial class OpenIddictServerHandlers
var lifetime = context.Principal.GetAuthorizationCodeLifetime();
// If the client to which the token is returned is known, use the attached setting if available.
if (lifetime is null && !context.Options.EnableDegradedMode && !string.IsNullOrEmpty(context.ClientId))
// Skip for CIMD clients (no pre-registered application to look up).
if (lifetime is null && !context.Options.EnableDegradedMode && !string.IsNullOrEmpty(context.ClientId) &&
!(context.Transaction.Properties.TryGetValue(
".ClientIdMetadataDocumentFetchRequired", out var cimdFlag) && cimdFlag is true))
{
if (_applicationManager is null)
{
@ -3595,7 +3643,10 @@ public static partial class OpenIddictServerHandlers
var lifetime = context.Principal.GetDeviceCodeLifetime();
// If the client to which the token is returned is known, use the attached setting if available.
if (lifetime is null && !context.Options.EnableDegradedMode && !string.IsNullOrEmpty(context.ClientId))
// Skip for CIMD clients (no pre-registered application to look up).
if (lifetime is null && !context.Options.EnableDegradedMode && !string.IsNullOrEmpty(context.ClientId) &&
!(context.Transaction.Properties.TryGetValue(
".ClientIdMetadataDocumentFetchRequired", out var cimdFlag) && cimdFlag is true))
{
if (_applicationManager is null)
{
@ -3839,7 +3890,10 @@ public static partial class OpenIddictServerHandlers
};
// If the client to which the token is returned is known, use the attached setting if available.
if (lifetime is null && !context.Options.EnableDegradedMode && !string.IsNullOrEmpty(context.ClientId))
// Skip for CIMD clients (no pre-registered application to look up).
if (lifetime is null && !context.Options.EnableDegradedMode && !string.IsNullOrEmpty(context.ClientId) &&
!(context.Transaction.Properties.TryGetValue(
".ClientIdMetadataDocumentFetchRequired", out var cimdFlag) && cimdFlag is true))
{
if (_applicationManager is null)
{
@ -3980,7 +4034,10 @@ public static partial class OpenIddictServerHandlers
var lifetime = context.Principal.GetRequestTokenLifetime();
// If the client to which the token is returned is known, use the attached setting if available.
if (lifetime is null && !context.Options.EnableDegradedMode && !string.IsNullOrEmpty(context.ClientId))
// Skip for CIMD clients (no pre-registered application to look up).
if (lifetime is null && !context.Options.EnableDegradedMode && !string.IsNullOrEmpty(context.ClientId) &&
!(context.Transaction.Properties.TryGetValue(
".ClientIdMetadataDocumentFetchRequired", out var cimdFlag) && cimdFlag is true))
{
if (_applicationManager is null)
{
@ -4131,7 +4188,10 @@ public static partial class OpenIddictServerHandlers
var lifetime = context.Principal.GetRefreshTokenLifetime();
// If the client to which the token is returned is known, use the attached setting if available.
if (lifetime is null && !context.Options.EnableDegradedMode && !string.IsNullOrEmpty(context.ClientId))
// Skip for CIMD clients (no pre-registered application to look up).
if (lifetime is null && !context.Options.EnableDegradedMode && !string.IsNullOrEmpty(context.ClientId) &&
!(context.Transaction.Properties.TryGetValue(
".ClientIdMetadataDocumentFetchRequired", out var cimdFlag) && cimdFlag is true))
{
if (_applicationManager is null)
{
@ -4268,7 +4328,10 @@ public static partial class OpenIddictServerHandlers
var lifetime = context.Principal.GetIdentityTokenLifetime();
// If the client to which the token is returned is known, use the attached setting if available.
if (lifetime is null && !context.Options.EnableDegradedMode && !string.IsNullOrEmpty(context.ClientId))
// Skip for CIMD clients (no pre-registered application to look up).
if (lifetime is null && !context.Options.EnableDegradedMode && !string.IsNullOrEmpty(context.ClientId) &&
!(context.Transaction.Properties.TryGetValue(
".ClientIdMetadataDocumentFetchRequired", out var cimdFlag) && cimdFlag is true))
{
if (_applicationManager is null)
{
@ -4398,7 +4461,10 @@ public static partial class OpenIddictServerHandlers
var lifetime = context.Principal.GetUserCodeLifetime();
// If the client to which the token is returned is known, use the attached setting if available.
if (lifetime is null && !context.Options.EnableDegradedMode && !string.IsNullOrEmpty(context.ClientId))
// Skip for CIMD clients (no pre-registered application to look up).
if (lifetime is null && !context.Options.EnableDegradedMode && !string.IsNullOrEmpty(context.ClientId) &&
!(context.Transaction.Properties.TryGetValue(
".ClientIdMetadataDocumentFetchRequired", out var cimdFlag) && cimdFlag is true))
{
if (_applicationManager is null)
{

26
src/OpenIddict.Server/OpenIddictServerOptions.cs

@ -370,6 +370,32 @@ public sealed class OpenIddictServerOptions
/// </summary>
public bool DisableScopeValidation { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether support for client ID metadata documents
/// (CIMD) is enabled. When enabled, the server accepts HTTPS URLs as client identifiers
/// and fetches the client metadata from the specified URL when the client is not
/// pre-registered. For more information, see draft-ietf-oauth-client-id-metadata-document.
/// </summary>
public bool EnableClientIdMetadataDocumentSupport { get; set; }
/// <summary>
/// Gets or sets the maximum size, in bytes, of a client ID metadata document
/// that the server will accept. The default value is 5120 (5 KB).
/// </summary>
public int ClientIdMetadataDocumentSizeLimit { get; set; } = 5_120;
/// <summary>
/// Gets or sets the timeout for fetching a client ID metadata document.
/// The default value is 10 seconds.
/// </summary>
public TimeSpan ClientIdMetadataDocumentFetchTimeout { get; set; } = TimeSpan.FromSeconds(10);
/// <summary>
/// Gets or sets the default cache duration for client ID metadata documents
/// when no HTTP cache headers are present. The default value is 1 hour.
/// </summary>
public TimeSpan ClientIdMetadataDocumentDefaultCacheDuration { get; set; } = TimeSpan.FromHours(1);
/// <summary>
/// Gets or sets a boolean indicating whether requests received by the authorization
/// endpoint should be stored in the token store, which allows flowing

Loading…
Cancel
Save