Skip to content

Commit b15ff68

Browse files
Merge pull request #347 from DuendeSoftware/ev/atm/solve-cirular-dep-in-IClientAssertionService
fix circular dependency
2 parents 06ed637 + 7a2c178 commit b15ff68

File tree

2 files changed

+89
-3
lines changed

2 files changed

+89
-3
lines changed

access-token-management/src/AccessTokenManagement.OpenIdConnect/Internal/ConfigureOpenIdConnectOptions.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@ internal class ConfigureOpenIdConnectOptions(
2222
IHttpContextAccessor httpContextAccessor,
2323
IOptions<UserTokenManagementOptions> userAccessTokenManagementOptions,
2424
IAuthenticationSchemeProvider schemeProvider,
25-
IClientAssertionService clientAssertionService,
25+
IServiceProvider serviceProvider,
2626
ILoggerFactory loggerFactory) : IConfigureNamedOptions<OpenIdConnectOptions>
2727
{
2828
private readonly Scheme _configScheme = GetConfigScheme(userAccessTokenManagementOptions.Value, schemeProvider);
29+
private IClientAssertionService ClientAssertionService => serviceProvider.GetRequiredService<IClientAssertionService>();
2930

3031
private ClientCredentialsClientName ClientName => _configScheme.ToClientName();
3132

@@ -119,7 +120,7 @@ async Task Callback(AuthorizationCodeReceivedContext context)
119120
}
120121

121122
// Automatically send client assertion during code exchange if a service is registered
122-
var assertion = await clientAssertionService
123+
var assertion = await ClientAssertionService
123124
.GetClientAssertionAsync(ClientName, ct: context.HttpContext.RequestAborted)
124125
.ConfigureAwait(false);
125126

@@ -162,7 +163,7 @@ async Task Callback(PushedAuthorizationContext context)
162163
await inner.Invoke(context);
163164

164165
// --- Client assertion ---
165-
var assertion = await clientAssertionService
166+
var assertion = await ClientAssertionService
166167
.GetClientAssertionAsync(ClientName, ct: context.HttpContext.RequestAborted)
167168
.ConfigureAwait(false);
168169

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
3+
4+
using Duende.AccessTokenManagement.OpenIdConnect;
5+
using Duende.IdentityModel.Client;
6+
using Microsoft.Extensions.DependencyInjection;
7+
8+
namespace Duende.AccessTokenManagement;
9+
10+
/// <summary>
11+
/// Tests that verify the DI registration does not create circular dependencies.
12+
/// See https://github.com/DuendeSoftware/foss/pull/347 for context.
13+
/// </summary>
14+
public class CircularDependencyTests
15+
{
16+
/// <summary>
17+
/// Reproduces the circular dependency described in PR #347:
18+
///
19+
/// IClientAssertionService (user impl)
20+
/// → IOpenIdConnectConfigurationService
21+
/// → IOptionsMonitor&lt;OpenIdConnectOptions&gt;
22+
/// → IConfigureOptions&lt;OpenIdConnectOptions&gt; (ConfigureOpenIdConnectOptions)
23+
/// → IClientAssertionService ← CYCLE
24+
///
25+
/// The fix in ConfigureOpenIdConnectOptions resolves IClientAssertionService
26+
/// lazily via IServiceProvider instead of constructor injection, breaking the cycle.
27+
/// </summary>
28+
[Fact]
29+
public void IClientAssertionService_depending_on_IOpenIdConnectConfigurationService_should_not_cause_circular_dependency()
30+
{
31+
var services = new ServiceCollection();
32+
33+
// Register authentication with an OpenIdConnect scheme (minimal setup).
34+
services.AddAuthentication(options =>
35+
{
36+
options.DefaultChallengeScheme = "oidc";
37+
options.DefaultSignInScheme = "cookie";
38+
})
39+
.AddCookie("cookie")
40+
.AddOpenIdConnect("oidc", options =>
41+
{
42+
options.Authority = "https://demo.duendesoftware.com";
43+
options.ClientId = "test-client";
44+
options.ClientSecret = "secret";
45+
});
46+
47+
// Register ATM's OpenIdConnect services (includes ConfigureOpenIdConnectOptions).
48+
services.AddOpenIdConnectAccessTokenManagement();
49+
50+
// Register a custom IClientAssertionService that depends on
51+
// IOpenIdConnectConfigurationService — the exact pattern from the
52+
// WebJarJwt sample that triggered the circular dependency before the fix.
53+
services.AddTransient<IClientAssertionService, ClientAssertionServiceWithOidcDependency>();
54+
55+
// ValidateOnBuild detects circular dependencies at container build time.
56+
// Before the fix, this would throw:
57+
// "A circular dependency was detected for the service of type
58+
// 'Duende.AccessTokenManagement.IClientAssertionService'."
59+
var act = () => services.BuildServiceProvider(new ServiceProviderOptions
60+
{
61+
ValidateOnBuild = true,
62+
ValidateScopes = true,
63+
});
64+
65+
act.ShouldNotThrow();
66+
}
67+
68+
/// <summary>
69+
/// A test implementation of IClientAssertionService that depends on
70+
/// IOpenIdConnectConfigurationService, reproducing the dependency chain
71+
/// from the WebJarJwt sample that caused the circular dependency.
72+
/// </summary>
73+
private sealed class ClientAssertionServiceWithOidcDependency(
74+
IOpenIdConnectConfigurationService configurationService) : IClientAssertionService
75+
{
76+
// Keep a reference to prove DI resolved the dependency successfully.
77+
private readonly IOpenIdConnectConfigurationService _configurationService = configurationService;
78+
79+
public Task<ClientAssertion?> GetClientAssertionAsync(
80+
ClientCredentialsClientName? clientName = null,
81+
TokenRequestParameters? parameters = null,
82+
CancellationToken ct = default) =>
83+
Task.FromResult<ClientAssertion?>(null);
84+
}
85+
}

0 commit comments

Comments
 (0)