|
| 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<OpenIdConnectOptions> |
| 22 | + /// → IConfigureOptions<OpenIdConnectOptions> (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