Browse Source
Add a new OpenIddict.Server.SystemNetHttp.Tests project with 23 unit tests covering the CIMD application manager, context, and handler filter. Add 8 integration tests in the base server integration tests for ValidateClientId CIMD branching and discovery metadata advertisement.pull/2416/head
6 changed files with 856 additions and 0 deletions
@ -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<CancellationToken>())) |
||||
|
.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<CancellationToken>())) |
||||
|
.ReturnsAsync((OpenIddictApplication?) null); |
||||
|
})); |
||||
|
|
||||
|
// Add an inline handler that runs after ValidateClientId to inspect the transaction flag.
|
||||
|
options.AddEventHandler<ProcessAuthenticationContext>(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<CancellationToken>())) |
||||
|
.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<CancellationToken>())) |
||||
|
.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<CancellationToken>())) |
||||
|
.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<CancellationToken>())) |
||||
|
.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); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFrameworks>net472;net48;$(NetCoreTargetFrameworks)</TargetFrameworks> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="MartinCostello.Logging.XUnit" /> |
||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" /> |
||||
|
<PackageReference Include="Moq" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\..\src\OpenIddict.Core\OpenIddict.Core.csproj" /> |
||||
|
<ProjectReference Include="..\..\src\OpenIddict.Server.SystemNetHttp\OpenIddict.Server.SystemNetHttp.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' "> |
||||
|
<Reference Include="System.Net.Http" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<Using Include="OpenIddict.Abstractions" /> |
||||
|
<Using Include="OpenIddict.Abstractions.OpenIddictConstants" Static="true" /> |
||||
|
<Using Include="OpenIddict.Abstractions.OpenIddictResources" Alias="SR" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -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<CancellationToken>())) |
||||
|
.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<CancellationToken>())) |
||||
|
.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<CancellationToken>())) |
||||
|
.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<CancellationToken>())) |
||||
|
.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<CancellationToken>()), Times.Once()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task FindByClientIdAsync_ReturnsCachedVirtualApp_OnSecondCall() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var store = CreateStore(); |
||||
|
store.Setup(s => s.FindByClientIdAsync("https://example.com/client", It.IsAny<CancellationToken>())) |
||||
|
.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<CancellationToken>()), 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<CancellationToken>()), 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<CancellationToken>()), 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<CancellationToken>()), 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<CancellationToken>()), 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<ImmutableArray<string>>(uris => |
||||
|
uris.Length == 2 && |
||||
|
uris[0] == "https://example.com/callback" && |
||||
|
uris[1] == "https://example.com/callback2"), |
||||
|
It.IsAny<CancellationToken>()), 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<ImmutableArray<string>>(perms => |
||||
|
perms.Contains(Permissions.GrantTypes.AuthorizationCode) && |
||||
|
perms.Contains(Permissions.GrantTypes.RefreshToken)), |
||||
|
It.IsAny<CancellationToken>()), 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<ImmutableArray<string>>(perms => |
||||
|
perms.Contains(Permissions.GrantTypes.AuthorizationCode)), |
||||
|
It.IsAny<CancellationToken>()), 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<ImmutableArray<string>>(perms => |
||||
|
perms.Contains(Permissions.ResponseTypes.Code) && |
||||
|
perms.Contains(Permissions.ResponseTypes.CodeIdToken)), |
||||
|
It.IsAny<CancellationToken>()), 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<ImmutableArray<string>>(perms => |
||||
|
perms.Contains(Permissions.ResponseTypes.Code)), |
||||
|
It.IsAny<CancellationToken>()), 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<ImmutableArray<string>>(reqs => |
||||
|
reqs.Contains(Requirements.Features.ProofKeyForCodeExchange)), |
||||
|
It.IsAny<CancellationToken>()), 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<CancellationToken>())) |
||||
|
.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<ImmutableArray<string>>(perms => |
||||
|
perms.Contains(Permissions.Endpoints.Authorization) && |
||||
|
perms.Contains(Permissions.Endpoints.Token)), |
||||
|
It.IsAny<CancellationToken>()), 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<ImmutableArray<string>>(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<CancellationToken>()), Times.Once()); |
||||
|
} |
||||
|
|
||||
|
private static Mock<IOpenIddictApplicationStore<TestApplication>> CreateStore() |
||||
|
{ |
||||
|
var store = new Mock<IOpenIddictApplicationStore<TestApplication>>(); |
||||
|
store.Setup(s => s.InstantiateAsync(It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(() => new TestApplication()); |
||||
|
return store; |
||||
|
} |
||||
|
|
||||
|
private static OpenIddictServerSystemNetHttpApplicationManager<TestApplication> CreateManager( |
||||
|
Mock<IOpenIddictApplicationStore<TestApplication>> store, |
||||
|
OpenIddictServerSystemNetHttpCimdContext cimdContext) |
||||
|
{ |
||||
|
var cache = Mock.Of<IOpenIddictApplicationCache<TestApplication>>(); |
||||
|
var logger = NullLogger<OpenIddictApplicationManager<TestApplication>>.Instance; |
||||
|
|
||||
|
var coreOptions = new OpenIddictCoreOptions |
||||
|
{ |
||||
|
DisableEntityCaching = true, |
||||
|
DisableAdditionalFiltering = true |
||||
|
}; |
||||
|
|
||||
|
var optionsMonitor = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>( |
||||
|
m => m.CurrentValue == coreOptions); |
||||
|
|
||||
|
return new OpenIddictServerSystemNetHttpApplicationManager<TestApplication>( |
||||
|
cache, logger, optionsMonitor, store.Object, cimdContext); |
||||
|
} |
||||
|
|
||||
|
private static (OpenIddictServerSystemNetHttpApplicationManager<TestApplication> Manager, |
||||
|
Mock<IOpenIddictApplicationStore<TestApplication>> Store, |
||||
|
OpenIddictServerSystemNetHttpCimdContext CimdContext) |
||||
|
CreateManagerWithCimdContext(JsonDocument? document = null) |
||||
|
{ |
||||
|
var store = CreateStore(); |
||||
|
store.Setup(s => s.FindByClientIdAsync("https://example.com/client", It.IsAny<CancellationToken>())) |
||||
|
.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 { } |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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<ILogger>() |
||||
|
}; |
||||
|
|
||||
|
return new HandleConfigurationRequestContext(transaction) |
||||
|
{ |
||||
|
Issuer = new Uri("https://localhost/") |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue