diff --git a/OpenIddict.slnx b/OpenIddict.slnx
index 69b2f560..ab5d6823 100644
--- a/OpenIddict.slnx
+++ b/OpenIddict.slnx
@@ -97,6 +97,7 @@
+
diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Cimd.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Cimd.cs
new file mode 100644
index 00000000..4a311089
--- /dev/null
+++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Cimd.cs
@@ -0,0 +1,250 @@
+/*
+ * 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;
+using Moq;
+using Xunit;
+using static OpenIddict.Server.OpenIddictServerEvents;
+using static OpenIddict.Server.OpenIddictServerHandlers;
+
+namespace OpenIddict.Server.IntegrationTests;
+
+public abstract partial class OpenIddictServerIntegrationTests
+{
+ [Fact]
+ public async Task HandleConfigurationRequest_AdvertisesCimdSupport_WhenEnabled()
+ {
+ // Arrange
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.EnableClientIdMetadataDocumentSupport();
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.GetAsync("/.well-known/openid-configuration");
+
+ // Assert
+ Assert.True((bool?) response[Metadata.ClientIdMetadataDocumentSupported]);
+ }
+
+ [Fact]
+ public async Task HandleConfigurationRequest_DoesNotAdvertiseCimdSupport_WhenDisabled()
+ {
+ // Arrange
+ await using var server = await CreateServerAsync();
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.GetAsync("/.well-known/openid-configuration");
+
+ // Assert
+ Assert.Null(response[Metadata.ClientIdMetadataDocumentSupported]);
+ }
+
+ [Fact]
+ public async Task ValidateAuthorizationRequest_RejectsUrlClientId_WhenCimdDisabled()
+ {
+ // Arrange
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.Services.AddSingleton(CreateApplicationManager(mock =>
+ {
+ mock.Setup(manager => manager.FindByClientIdAsync("https://example.com/client", It.IsAny()))
+ .ReturnsAsync((OpenIddictApplication?) null);
+ }));
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/authorize", new OpenIddictRequest
+ {
+ ClientId = "https://example.com/client",
+ RedirectUri = "http://www.fabrikam.com/path",
+ ResponseType = ResponseTypes.Code
+ });
+
+ // Assert — CIMD is disabled, so URL client_id is just treated as unknown client
+ Assert.Equal(Errors.InvalidRequest, response.Error);
+ Assert.Equal(SR.FormatID2052(Parameters.ClientId), response.ErrorDescription);
+ }
+
+ [Fact]
+ public async Task ValidateAuthorizationRequest_SetsTransactionFlag_WhenCimdEnabledAndClientIdIsHttpsUrl()
+ {
+ // Arrange
+ var flagWasSet = false;
+
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.EnableClientIdMetadataDocumentSupport();
+
+ options.Services.AddSingleton(CreateApplicationManager(mock =>
+ {
+ mock.Setup(manager => manager.FindByClientIdAsync("https://example.com/client", It.IsAny()))
+ .ReturnsAsync((OpenIddictApplication?) null);
+ }));
+
+ // Add an inline handler that runs after ValidateClientId to inspect the transaction flag.
+ options.AddEventHandler(builder =>
+ {
+ builder.UseInlineHandler(context =>
+ {
+ if (context.Transaction.Properties.TryGetValue(
+ ".ClientIdMetadataDocumentFetchRequired", out var value) &&
+ value is true)
+ {
+ flagWasSet = true;
+ }
+
+ // Reject to stop further processing (we don't have CIMD HTTP infrastructure here).
+ context.Reject(
+ error: Errors.InvalidClient,
+ description: "Test completed.");
+
+ return ValueTask.CompletedTask;
+ });
+
+ builder.SetOrder(ValidateClientId.Descriptor.Order + 1);
+ });
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ await client.PostAsync("/connect/authorize", new OpenIddictRequest
+ {
+ ClientId = "https://example.com/client",
+ RedirectUri = "http://www.fabrikam.com/path",
+ ResponseType = ResponseTypes.Code
+ });
+
+ // Assert
+ Assert.True(flagWasSet);
+ }
+
+ [Fact]
+ public async Task ValidateAuthorizationRequest_RejectsHttpUrl_WhenCimdEnabled()
+ {
+ // Arrange
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.EnableClientIdMetadataDocumentSupport();
+
+ options.Services.AddSingleton(CreateApplicationManager(mock =>
+ {
+ mock.Setup(manager => manager.FindByClientIdAsync("http://example.com/client", It.IsAny()))
+ .ReturnsAsync((OpenIddictApplication?) null);
+ }));
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/authorize", new OpenIddictRequest
+ {
+ ClientId = "http://example.com/client",
+ RedirectUri = "http://www.fabrikam.com/path",
+ ResponseType = ResponseTypes.Code
+ });
+
+ // Assert — HTTP URL should be rejected even with CIMD enabled (requires HTTPS)
+ Assert.Equal(Errors.InvalidRequest, response.Error);
+ Assert.Equal(SR.FormatID2052(Parameters.ClientId), response.ErrorDescription);
+ }
+
+ [Fact]
+ public async Task ValidateAuthorizationRequest_RejectsUrlWithFragment_WhenCimdEnabled()
+ {
+ // Arrange
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.EnableClientIdMetadataDocumentSupport();
+
+ options.Services.AddSingleton(CreateApplicationManager(mock =>
+ {
+ mock.Setup(manager => manager.FindByClientIdAsync("https://example.com/client#fragment", It.IsAny()))
+ .ReturnsAsync((OpenIddictApplication?) null);
+ }));
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/authorize", new OpenIddictRequest
+ {
+ ClientId = "https://example.com/client#fragment",
+ RedirectUri = "http://www.fabrikam.com/path",
+ ResponseType = ResponseTypes.Code
+ });
+
+ // Assert — URL with fragment should be rejected
+ Assert.Equal(Errors.InvalidRequest, response.Error);
+ Assert.Equal(SR.FormatID2052(Parameters.ClientId), response.ErrorDescription);
+ }
+
+ [Fact]
+ public async Task ValidateAuthorizationRequest_RejectsUrlWithUserInfo_WhenCimdEnabled()
+ {
+ // Arrange
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.EnableClientIdMetadataDocumentSupport();
+
+ options.Services.AddSingleton(CreateApplicationManager(mock =>
+ {
+ mock.Setup(manager => manager.FindByClientIdAsync("https://user:pass@example.com/client", It.IsAny()))
+ .ReturnsAsync((OpenIddictApplication?) null);
+ }));
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/authorize", new OpenIddictRequest
+ {
+ ClientId = "https://user:pass@example.com/client",
+ RedirectUri = "http://www.fabrikam.com/path",
+ ResponseType = ResponseTypes.Code
+ });
+
+ // Assert — URL with userinfo should be rejected
+ Assert.Equal(Errors.InvalidRequest, response.Error);
+ Assert.Equal(SR.FormatID2052(Parameters.ClientId), response.ErrorDescription);
+ }
+
+ [Fact]
+ public async Task ValidateAuthorizationRequest_RejectsRootPathUrl_WhenCimdEnabled()
+ {
+ // Arrange
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.EnableClientIdMetadataDocumentSupport();
+
+ options.Services.AddSingleton(CreateApplicationManager(mock =>
+ {
+ mock.Setup(manager => manager.FindByClientIdAsync("https://example.com/", It.IsAny()))
+ .ReturnsAsync((OpenIddictApplication?) null);
+ }));
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/authorize", new OpenIddictRequest
+ {
+ ClientId = "https://example.com/",
+ RedirectUri = "http://www.fabrikam.com/path",
+ ResponseType = ResponseTypes.Code
+ });
+
+ // Assert — Root path URL should be rejected
+ Assert.Equal(Errors.InvalidRequest, response.Error);
+ Assert.Equal(SR.FormatID2052(Parameters.ClientId), response.ErrorDescription);
+ }
+}
diff --git a/test/OpenIddict.Server.SystemNetHttp.Tests/OpenIddict.Server.SystemNetHttp.Tests.csproj b/test/OpenIddict.Server.SystemNetHttp.Tests/OpenIddict.Server.SystemNetHttp.Tests.csproj
new file mode 100644
index 00000000..d9adf296
--- /dev/null
+++ b/test/OpenIddict.Server.SystemNetHttp.Tests/OpenIddict.Server.SystemNetHttp.Tests.csproj
@@ -0,0 +1,28 @@
+
+
+
+ net472;net48;$(NetCoreTargetFrameworks)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/OpenIddict.Server.SystemNetHttp.Tests/OpenIddictServerSystemNetHttpApplicationManagerTests.cs b/test/OpenIddict.Server.SystemNetHttp.Tests/OpenIddictServerSystemNetHttpApplicationManagerTests.cs
new file mode 100644
index 00000000..f70a7271
--- /dev/null
+++ b/test/OpenIddict.Server.SystemNetHttp.Tests/OpenIddictServerSystemNetHttpApplicationManagerTests.cs
@@ -0,0 +1,468 @@
+/*
+ * 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.Text.Json;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using Moq;
+using OpenIddict.Core;
+using OpenIddict.Server.SystemNetHttp;
+using Xunit;
+
+namespace OpenIddict.Server.SystemNetHttp.Tests;
+
+public class OpenIddictServerSystemNetHttpApplicationManagerTests
+{
+ [Fact]
+ public async Task FindByClientIdAsync_ReturnsBaseResult_WhenPreRegisteredClientExists()
+ {
+ // Arrange
+ var application = new TestApplication();
+ var store = CreateStore();
+ store.Setup(s => s.FindByClientIdAsync("Fabrikam", It.IsAny()))
+ .ReturnsAsync(application);
+
+ var cimdContext = new OpenIddictServerSystemNetHttpCimdContext();
+ var manager = CreateManager(store, cimdContext);
+
+ // Act
+ var result = await manager.FindByClientIdAsync("Fabrikam");
+
+ // Assert
+ Assert.Same(application, result);
+ }
+
+ [Fact]
+ public async Task FindByClientIdAsync_ReturnsNull_WhenNoCimdContext()
+ {
+ // Arrange
+ var store = CreateStore();
+ store.Setup(s => s.FindByClientIdAsync("https://example.com/client", It.IsAny()))
+ .ReturnsAsync((TestApplication?) null);
+
+ var cimdContext = new OpenIddictServerSystemNetHttpCimdContext();
+ var manager = CreateManager(store, cimdContext);
+
+ // Act
+ var result = await manager.FindByClientIdAsync("https://example.com/client");
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task FindByClientIdAsync_ReturnsNull_WhenCimdClientIdDoesNotMatch()
+ {
+ // Arrange
+ var store = CreateStore();
+ store.Setup(s => s.FindByClientIdAsync("https://example.com/client-a", It.IsAny()))
+ .ReturnsAsync((TestApplication?) null);
+
+ var cimdContext = new OpenIddictServerSystemNetHttpCimdContext
+ {
+ ClientId = "https://example.com/client-b",
+ MetadataDocument = CreateMetadataDocument()
+ };
+
+ var manager = CreateManager(store, cimdContext);
+
+ // Act
+ var result = await manager.FindByClientIdAsync("https://example.com/client-a");
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task FindByClientIdAsync_SynthesizesVirtualApp_WhenCimdContextPopulated()
+ {
+ // Arrange
+ var store = CreateStore();
+ store.Setup(s => s.FindByClientIdAsync("https://example.com/client", It.IsAny()))
+ .ReturnsAsync((TestApplication?) null);
+
+ using var document = CreateMetadataDocument("""
+ {
+ "client_id": "https://example.com/client",
+ "client_name": "Test Client"
+ }
+ """);
+
+ var cimdContext = new OpenIddictServerSystemNetHttpCimdContext
+ {
+ ClientId = "https://example.com/client",
+ MetadataDocument = document
+ };
+
+ var manager = CreateManager(store, cimdContext);
+
+ // Act
+ var result = await manager.FindByClientIdAsync("https://example.com/client");
+
+ // Assert
+ Assert.NotNull(result);
+ store.Verify(s => s.SetClientIdAsync(result, "https://example.com/client", It.IsAny()), Times.Once());
+ }
+
+ [Fact]
+ public async Task FindByClientIdAsync_ReturnsCachedVirtualApp_OnSecondCall()
+ {
+ // Arrange
+ var store = CreateStore();
+ store.Setup(s => s.FindByClientIdAsync("https://example.com/client", It.IsAny()))
+ .ReturnsAsync((TestApplication?) null);
+
+ using var document = CreateMetadataDocument();
+
+ var cimdContext = new OpenIddictServerSystemNetHttpCimdContext
+ {
+ ClientId = "https://example.com/client",
+ MetadataDocument = document
+ };
+
+ var manager = CreateManager(store, cimdContext);
+
+ // Act
+ var first = await manager.FindByClientIdAsync("https://example.com/client");
+ var second = await manager.FindByClientIdAsync("https://example.com/client");
+
+ // Assert
+ Assert.NotNull(first);
+ Assert.Same(first, second);
+ store.Verify(s => s.InstantiateAsync(It.IsAny()), Times.Once());
+ }
+
+ [Fact]
+ public async Task FindByClientIdAsync_SynthesizedApp_HasCorrectClientType()
+ {
+ // Arrange
+ var (manager, store, _) = CreateManagerWithCimdContext();
+
+ // Act
+ var result = await manager.FindByClientIdAsync("https://example.com/client");
+
+ // Assert
+ Assert.NotNull(result);
+ store.Verify(s => s.SetClientTypeAsync(result, ClientTypes.Public, It.IsAny()), Times.Once());
+ }
+
+ [Fact]
+ public async Task FindByClientIdAsync_SynthesizedApp_HasCorrectApplicationType()
+ {
+ // Arrange
+ using var document = CreateMetadataDocument("""
+ {
+ "application_type": "native"
+ }
+ """);
+
+ var (manager, store, _) = CreateManagerWithCimdContext(document);
+
+ // Act
+ var result = await manager.FindByClientIdAsync("https://example.com/client");
+
+ // Assert
+ Assert.NotNull(result);
+ store.Verify(s => s.SetApplicationTypeAsync(result, "native", It.IsAny()), Times.Once());
+ }
+
+ [Fact]
+ public async Task FindByClientIdAsync_SynthesizedApp_DefaultsToWebApplicationType()
+ {
+ // Arrange
+ using var document = CreateMetadataDocument("{}");
+ var (manager, store, _) = CreateManagerWithCimdContext(document);
+
+ // Act
+ var result = await manager.FindByClientIdAsync("https://example.com/client");
+
+ // Assert
+ Assert.NotNull(result);
+ store.Verify(s => s.SetApplicationTypeAsync(result, ApplicationTypes.Web, It.IsAny()), Times.Once());
+ }
+
+ [Fact]
+ public async Task FindByClientIdAsync_SynthesizedApp_HasCorrectDisplayName()
+ {
+ // Arrange
+ using var document = CreateMetadataDocument("""
+ {
+ "client_name": "My Cool App"
+ }
+ """);
+
+ var (manager, store, _) = CreateManagerWithCimdContext(document);
+
+ // Act
+ var result = await manager.FindByClientIdAsync("https://example.com/client");
+
+ // Assert
+ Assert.NotNull(result);
+ store.Verify(s => s.SetDisplayNameAsync(result, "My Cool App", It.IsAny()), Times.Once());
+ }
+
+ [Fact]
+ public async Task FindByClientIdAsync_SynthesizedApp_HasCorrectRedirectUris()
+ {
+ // Arrange
+ using var document = CreateMetadataDocument("""
+ {
+ "redirect_uris": ["https://example.com/callback", "https://example.com/callback2"]
+ }
+ """);
+
+ var (manager, store, _) = CreateManagerWithCimdContext(document);
+
+ // Act
+ var result = await manager.FindByClientIdAsync("https://example.com/client");
+
+ // Assert
+ Assert.NotNull(result);
+ store.Verify(s => s.SetRedirectUrisAsync(
+ result,
+ It.Is>(uris =>
+ uris.Length == 2 &&
+ uris[0] == "https://example.com/callback" &&
+ uris[1] == "https://example.com/callback2"),
+ It.IsAny()), Times.Once());
+ }
+
+ [Fact]
+ public async Task FindByClientIdAsync_SynthesizedApp_HasCorrectGrantTypePermissions()
+ {
+ // Arrange
+ using var document = CreateMetadataDocument("""
+ {
+ "grant_types": ["authorization_code", "refresh_token"]
+ }
+ """);
+
+ var (manager, store, _) = CreateManagerWithCimdContext(document);
+
+ // Act
+ var result = await manager.FindByClientIdAsync("https://example.com/client");
+
+ // Assert
+ Assert.NotNull(result);
+ store.Verify(s => s.SetPermissionsAsync(
+ result,
+ It.Is>(perms =>
+ perms.Contains(Permissions.GrantTypes.AuthorizationCode) &&
+ perms.Contains(Permissions.GrantTypes.RefreshToken)),
+ It.IsAny()), Times.Once());
+ }
+
+ [Fact]
+ public async Task FindByClientIdAsync_SynthesizedApp_DefaultsToAuthorizationCodeGrant()
+ {
+ // Arrange — no grant_types in metadata
+ using var document = CreateMetadataDocument("{}");
+ var (manager, store, _) = CreateManagerWithCimdContext(document);
+
+ // Act
+ var result = await manager.FindByClientIdAsync("https://example.com/client");
+
+ // Assert
+ Assert.NotNull(result);
+ store.Verify(s => s.SetPermissionsAsync(
+ result,
+ It.Is>(perms =>
+ perms.Contains(Permissions.GrantTypes.AuthorizationCode)),
+ It.IsAny()), Times.Once());
+ }
+
+ [Fact]
+ public async Task FindByClientIdAsync_SynthesizedApp_HasCorrectResponseTypePermissions()
+ {
+ // Arrange
+ using var document = CreateMetadataDocument("""
+ {
+ "response_types": ["code", "code id_token"]
+ }
+ """);
+
+ var (manager, store, _) = CreateManagerWithCimdContext(document);
+
+ // Act
+ var result = await manager.FindByClientIdAsync("https://example.com/client");
+
+ // Assert
+ Assert.NotNull(result);
+ store.Verify(s => s.SetPermissionsAsync(
+ result,
+ It.Is>(perms =>
+ perms.Contains(Permissions.ResponseTypes.Code) &&
+ perms.Contains(Permissions.ResponseTypes.CodeIdToken)),
+ It.IsAny()), Times.Once());
+ }
+
+ [Fact]
+ public async Task FindByClientIdAsync_SynthesizedApp_DefaultsToCodeResponseType()
+ {
+ // Arrange — no response_types in metadata
+ using var document = CreateMetadataDocument("{}");
+ var (manager, store, _) = CreateManagerWithCimdContext(document);
+
+ // Act
+ var result = await manager.FindByClientIdAsync("https://example.com/client");
+
+ // Assert
+ Assert.NotNull(result);
+ store.Verify(s => s.SetPermissionsAsync(
+ result,
+ It.Is>(perms =>
+ perms.Contains(Permissions.ResponseTypes.Code)),
+ It.IsAny()), Times.Once());
+ }
+
+ [Fact]
+ public async Task FindByClientIdAsync_SynthesizedApp_RequiresPkce()
+ {
+ // Arrange
+ var (manager, store, _) = CreateManagerWithCimdContext();
+
+ // Act
+ var result = await manager.FindByClientIdAsync("https://example.com/client");
+
+ // Assert
+ Assert.NotNull(result);
+ store.Verify(s => s.SetRequirementsAsync(
+ result,
+ It.Is>(reqs =>
+ reqs.Contains(Requirements.Features.ProofKeyForCodeExchange)),
+ It.IsAny()), Times.Once());
+ }
+
+ [Fact]
+ public async Task GetIdAsync_ReturnsNull_ForVirtualApplication()
+ {
+ // Arrange
+ var (manager, _, cimdContext) = CreateManagerWithCimdContext();
+
+ var virtualApp = await manager.FindByClientIdAsync("https://example.com/client");
+ Assert.NotNull(virtualApp);
+
+ // Act
+ var id = await manager.GetIdAsync(virtualApp);
+
+ // Assert
+ Assert.Null(id);
+ }
+
+ [Fact]
+ public async Task GetIdAsync_DelegatesToBase_ForNonVirtualApplication()
+ {
+ // Arrange
+ var regularApp = new TestApplication();
+ var store = CreateStore();
+ store.Setup(s => s.GetIdAsync(regularApp, It.IsAny()))
+ .ReturnsAsync("some-id");
+
+ var cimdContext = new OpenIddictServerSystemNetHttpCimdContext();
+ var manager = CreateManager(store, cimdContext);
+
+ // Act
+ var id = await manager.GetIdAsync(regularApp);
+
+ // Assert
+ Assert.Equal("some-id", id);
+ }
+
+ [Fact]
+ public async Task FindByClientIdAsync_SynthesizedApp_HasEndpointPermissions()
+ {
+ // Arrange
+ var (manager, store, _) = CreateManagerWithCimdContext();
+
+ // Act
+ var result = await manager.FindByClientIdAsync("https://example.com/client");
+
+ // Assert
+ Assert.NotNull(result);
+ store.Verify(s => s.SetPermissionsAsync(
+ result,
+ It.Is>(perms =>
+ perms.Contains(Permissions.Endpoints.Authorization) &&
+ perms.Contains(Permissions.Endpoints.Token)),
+ It.IsAny()), Times.Once());
+ }
+
+ [Fact]
+ public async Task FindByClientIdAsync_SynthesizedApp_HasScopePermissions()
+ {
+ // Arrange
+ var (manager, store, _) = CreateManagerWithCimdContext();
+
+ // Act
+ var result = await manager.FindByClientIdAsync("https://example.com/client");
+
+ // Assert
+ Assert.NotNull(result);
+ store.Verify(s => s.SetPermissionsAsync(
+ result,
+ It.Is>(perms =>
+ perms.Contains(Permissions.Scopes.Email) &&
+ perms.Contains(Permissions.Scopes.Profile) &&
+ perms.Contains(Permissions.Scopes.Address) &&
+ perms.Contains(Permissions.Scopes.Phone) &&
+ perms.Contains(Permissions.Scopes.Roles)),
+ It.IsAny()), Times.Once());
+ }
+
+ private static Mock> CreateStore()
+ {
+ var store = new Mock>();
+ store.Setup(s => s.InstantiateAsync(It.IsAny()))
+ .ReturnsAsync(() => new TestApplication());
+ return store;
+ }
+
+ private static OpenIddictServerSystemNetHttpApplicationManager CreateManager(
+ Mock> store,
+ OpenIddictServerSystemNetHttpCimdContext cimdContext)
+ {
+ var cache = Mock.Of>();
+ var logger = NullLogger>.Instance;
+
+ var coreOptions = new OpenIddictCoreOptions
+ {
+ DisableEntityCaching = true,
+ DisableAdditionalFiltering = true
+ };
+
+ var optionsMonitor = Mock.Of>(
+ m => m.CurrentValue == coreOptions);
+
+ return new OpenIddictServerSystemNetHttpApplicationManager(
+ cache, logger, optionsMonitor, store.Object, cimdContext);
+ }
+
+ private static (OpenIddictServerSystemNetHttpApplicationManager Manager,
+ Mock> Store,
+ OpenIddictServerSystemNetHttpCimdContext CimdContext)
+ CreateManagerWithCimdContext(JsonDocument? document = null)
+ {
+ var store = CreateStore();
+ store.Setup(s => s.FindByClientIdAsync("https://example.com/client", It.IsAny()))
+ .ReturnsAsync((TestApplication?) null);
+
+ var cimdContext = new OpenIddictServerSystemNetHttpCimdContext
+ {
+ ClientId = "https://example.com/client",
+ MetadataDocument = document ?? CreateMetadataDocument()
+ };
+
+ var manager = CreateManager(store, cimdContext);
+ return (manager, store, cimdContext);
+ }
+
+ private static JsonDocument CreateMetadataDocument(string? json = null)
+ => JsonDocument.Parse(json ?? """{"client_id": "https://example.com/client"}""");
+
+ public class TestApplication { }
+}
diff --git a/test/OpenIddict.Server.SystemNetHttp.Tests/OpenIddictServerSystemNetHttpCimdContextTests.cs b/test/OpenIddict.Server.SystemNetHttp.Tests/OpenIddictServerSystemNetHttpCimdContextTests.cs
new file mode 100644
index 00000000..61e4a2bf
--- /dev/null
+++ b/test/OpenIddict.Server.SystemNetHttp.Tests/OpenIddictServerSystemNetHttpCimdContextTests.cs
@@ -0,0 +1,45 @@
+/*
+ * 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.Text.Json;
+using OpenIddict.Server.SystemNetHttp;
+using Xunit;
+
+namespace OpenIddict.Server.SystemNetHttp.Tests;
+
+public class OpenIddictServerSystemNetHttpCimdContextTests
+{
+ [Fact]
+ public void Properties_DefaultToNull()
+ {
+ // Arrange
+ var context = new OpenIddictServerSystemNetHttpCimdContext();
+
+ // Assert
+ Assert.Null(context.ClientId);
+ Assert.Null(context.MetadataDocument);
+ Assert.Null(context.VirtualApplication);
+ }
+
+ [Fact]
+ public void Properties_CanBeSetAndRead()
+ {
+ // Arrange
+ var context = new OpenIddictServerSystemNetHttpCimdContext();
+ using var document = JsonDocument.Parse("""{"client_name": "Test"}""");
+ var virtualApp = new object();
+
+ // Act
+ context.ClientId = "https://example.com/client";
+ context.MetadataDocument = document;
+ context.VirtualApplication = virtualApp;
+
+ // Assert
+ Assert.Equal("https://example.com/client", context.ClientId);
+ Assert.Same(document, context.MetadataDocument);
+ Assert.Same(virtualApp, context.VirtualApplication);
+ }
+}
diff --git a/test/OpenIddict.Server.SystemNetHttp.Tests/OpenIddictServerSystemNetHttpHandlerFilterTests.cs b/test/OpenIddict.Server.SystemNetHttp.Tests/OpenIddictServerSystemNetHttpHandlerFilterTests.cs
new file mode 100644
index 00000000..322c7f69
--- /dev/null
+++ b/test/OpenIddict.Server.SystemNetHttp.Tests/OpenIddictServerSystemNetHttpHandlerFilterTests.cs
@@ -0,0 +1,64 @@
+/*
+ * 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.Logging;
+using Moq;
+using OpenIddict.Server.SystemNetHttp;
+using Xunit;
+using static OpenIddict.Server.OpenIddictServerEvents;
+using static OpenIddict.Server.SystemNetHttp.OpenIddictServerSystemNetHttpHandlerFilters;
+
+namespace OpenIddict.Server.SystemNetHttp.Tests;
+
+public class OpenIddictServerSystemNetHttpHandlerFilterTests
+{
+ [Fact]
+ public async Task IsActiveAsync_ReturnsTrue_WhenCimdEnabled()
+ {
+ // Arrange
+ var filter = new RequireClientIdMetadataDocumentSupportEnabled();
+ var context = CreateBaseContext(enableCimd: true);
+
+ // Act
+ var result = await filter.IsActiveAsync(context);
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public async Task IsActiveAsync_ReturnsFalse_WhenCimdDisabled()
+ {
+ // Arrange
+ var filter = new RequireClientIdMetadataDocumentSupportEnabled();
+ var context = CreateBaseContext(enableCimd: false);
+
+ // Act
+ var result = await filter.IsActiveAsync(context);
+
+ // Assert
+ Assert.False(result);
+ }
+
+ private static HandleConfigurationRequestContext CreateBaseContext(bool enableCimd)
+ {
+ var options = new OpenIddictServerOptions
+ {
+ EnableClientIdMetadataDocumentSupport = enableCimd
+ };
+
+ var transaction = new OpenIddictServerTransaction
+ {
+ Options = options,
+ Logger = Mock.Of()
+ };
+
+ return new HandleConfigurationRequestContext(transaction)
+ {
+ Issuer = new Uri("https://localhost/")
+ };
+ }
+}