diff --git a/bff/src/Bff/DynamicFrontends/BffConfigureCookieOptions.cs b/bff/src/Bff/DynamicFrontends/BffConfigureCookieOptions.cs index df4f66bc2..47e23bb24 100644 --- a/bff/src/Bff/DynamicFrontends/BffConfigureCookieOptions.cs +++ b/bff/src/Bff/DynamicFrontends/BffConfigureCookieOptions.cs @@ -1,7 +1,9 @@ // Copyright (c) Duende Software. All rights reserved. // See LICENSE in the project root for license information. +using Duende.Bff.AccessTokenManagement; using Duende.Bff.Configuration; +using Duende.Bff.DynamicFrontends.Internal; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; @@ -12,7 +14,7 @@ internal class BffConfigureCookieOptions( TimeProvider timeProvider, IOptions bffConfiguration, IOptions bffOptions, - CurrentFrontendAccessor currentFrontendAccessor + FrontendSelector frontendSelector ) : IConfigureNamedOptions { private readonly BffOptions _bffOptions = bffOptions.Value; @@ -23,8 +25,10 @@ internal class BffConfigureCookieOptions( { // Normally, this is added by AuthenticationBuilder.PostConfigureAuthenticationSchemeOptions // but this is private API, so we need to do it ourselves. + + var schemeName = Scheme.ParseOrDefault(name); options.TimeProvider = timeProvider; - if (currentFrontendAccessor.TryGet(out var frontEnd)) + if (frontendSelector.TryGetFrontendByCookieScheme(schemeName, out var frontEnd)) { if (frontEnd.MatchingCriteria.MatchingPath != null) { diff --git a/bff/src/Bff/DynamicFrontends/BffConfigureOpenIdConnectOptions.cs b/bff/src/Bff/DynamicFrontends/BffConfigureOpenIdConnectOptions.cs index 9dd10460b..f110ac516 100644 --- a/bff/src/Bff/DynamicFrontends/BffConfigureOpenIdConnectOptions.cs +++ b/bff/src/Bff/DynamicFrontends/BffConfigureOpenIdConnectOptions.cs @@ -3,6 +3,7 @@ using Duende.Bff.AccessTokenManagement; using Duende.Bff.Configuration; +using Duende.Bff.DynamicFrontends.Internal; using Duende.Bff.Internal; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.Extensions.Options; @@ -11,7 +12,7 @@ namespace Duende.Bff.DynamicFrontends; internal class BffConfigureOpenIdConnectOptions( TimeProvider timeProvider, - CurrentFrontendAccessor currentFrontendAccessor, + FrontendSelector frontendSelector, ActiveOpenIdConnectAuthenticationScheme activeOpenIdConnectScheme, IOptions bffConfiguration, IOptions bffOptions @@ -21,7 +22,8 @@ internal class BffConfigureOpenIdConnectOptions( public void Configure(string? name, OpenIdConnectOptions options) { - if (!activeOpenIdConnectScheme.ShouldConfigureScheme(Scheme.ParseOrDefault(name))) + var schemeName = Scheme.ParseOrDefault(name); + if (!activeOpenIdConnectScheme.ShouldConfigureScheme(schemeName)) { return; } @@ -45,7 +47,7 @@ internal class BffConfigureOpenIdConnectOptions( // See if there is a frontend selected // If so, apply the frontend's OpenID Connect options - if (!currentFrontendAccessor.TryGet(out var frontEnd)) + if (!frontendSelector.TryGetFrontendByOidcScheme(schemeName, out var frontEnd)) { return; } diff --git a/bff/src/Bff/DynamicFrontends/Internal/BffIndex.cs b/bff/src/Bff/DynamicFrontends/Internal/BffIndex.cs index 2d7a1699b..9470a477f 100644 --- a/bff/src/Bff/DynamicFrontends/Internal/BffIndex.cs +++ b/bff/src/Bff/DynamicFrontends/Internal/BffIndex.cs @@ -2,6 +2,7 @@ // See LICENSE in the project root for license information. using System.Diagnostics.CodeAnalysis; +using Duende.Bff.AccessTokenManagement; using Duende.Bff.Otel; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -11,6 +12,8 @@ namespace Duende.Bff.DynamicFrontends.Internal; internal class BffIndex { private readonly ILogger _logger; + private readonly Dictionary _frontendsByOidcScheme = new(); + private readonly Dictionary _frontendsByCookieScheme = new(); private readonly Dictionary> _perHostHeader = new(); private readonly PathTrie _perPath = new(); private BffFrontend? _defaultFrontend; @@ -24,8 +27,30 @@ internal class BffIndex } } + public bool TryGetFrontendByOidcScheme(Scheme? scheme, [NotNullWhen(true)] out BffFrontend? frontend) + { + if (scheme == null) + { + frontend = null; + return false; + } + return _frontendsByOidcScheme.TryGetValue(scheme.Value, out frontend); + } + + public bool TryGetFrontendByCookieScheme(Scheme? scheme, [NotNullWhen(true)] out BffFrontend? frontend) + { + if (scheme == null) + { + frontend = null; + return false; + } + return _frontendsByCookieScheme.TryGetValue(scheme.Value, out frontend); + } + public void AddFrontend(BffFrontend frontend) { + _frontendsByOidcScheme.Add(frontend.OidcSchemeName, frontend); + _frontendsByCookieScheme.Add(frontend.CookieSchemeName, frontend); var frontendMatchingCriteria = frontend.MatchingCriteria; if (!_registeredCriteria.TryAdd(frontendMatchingCriteria, frontend.Name)) diff --git a/bff/src/Bff/DynamicFrontends/Internal/FrontendSelector.cs b/bff/src/Bff/DynamicFrontends/Internal/FrontendSelector.cs index 703f0f8a7..d077cd5a8 100644 --- a/bff/src/Bff/DynamicFrontends/Internal/FrontendSelector.cs +++ b/bff/src/Bff/DynamicFrontends/Internal/FrontendSelector.cs @@ -2,6 +2,7 @@ // See LICENSE in the project root for license information. using System.Diagnostics.CodeAnalysis; +using Duende.Bff.AccessTokenManagement; using Duende.Bff.Otel; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -45,4 +46,10 @@ internal class FrontendSelector return _bffIndex.TryMatch(request, out selectedFrontend); } + + public bool TryGetFrontendByOidcScheme(Scheme? scheme, [NotNullWhen(true)] out BffFrontend? frontend) => + _bffIndex.TryGetFrontendByOidcScheme(scheme, out frontend); + + public bool TryGetFrontendByCookieScheme(Scheme? scheme, [NotNullWhen(true)] out BffFrontend? frontend) => + _bffIndex.TryGetFrontendByCookieScheme(scheme, out frontend); } diff --git a/bff/src/Bff/Internal/ActiveCookieAuthenticationScheme.cs b/bff/src/Bff/Internal/ActiveCookieAuthenticationScheme.cs index 40733205c..9d108288a 100644 --- a/bff/src/Bff/Internal/ActiveCookieAuthenticationScheme.cs +++ b/bff/src/Bff/Internal/ActiveCookieAuthenticationScheme.cs @@ -2,7 +2,7 @@ // See LICENSE in the project root for license information. using Duende.Bff.AccessTokenManagement; -using Duende.Bff.DynamicFrontends; +using Duende.Bff.DynamicFrontends.Internal; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Options; @@ -11,7 +11,7 @@ namespace Duende.Bff.Internal; /// /// Centralizes the logic for determining if the cookie authentication scheme should be configured based on the currently selected frontend and the default authentication scheme. /// -internal sealed class ActiveCookieAuthenticationScheme(CurrentFrontendAccessor currentFrontendAccessor, IOptions authOptions) +internal sealed class ActiveCookieAuthenticationScheme(FrontendSelector frontendSelector, IOptions authOptions) { private readonly Scheme? _defaultAuthenticationScheme = Scheme.ParseOrDefault(authOptions.Value.DefaultAuthenticateScheme ?? authOptions.Value.DefaultScheme); @@ -25,6 +25,6 @@ internal sealed class ActiveCookieAuthenticationScheme(CurrentFrontendAccessor c // Either the currently selected scheme is the default scheme _defaultAuthenticationScheme == schemeName || - // Or it's the correct scheme for the currently selected frontend - (currentFrontendAccessor.TryGet(out var frontend) && schemeName == frontend.CookieSchemeName); + // Or it's actually a scheme for a specific frontend. + (frontendSelector.TryGetFrontendByCookieScheme(schemeName, out _)); } diff --git a/bff/src/Bff/Internal/ActiveOpenIdConnectAuthenticationScheme.cs b/bff/src/Bff/Internal/ActiveOpenIdConnectAuthenticationScheme.cs index 1d3d8ebac..a703e6c48 100644 --- a/bff/src/Bff/Internal/ActiveOpenIdConnectAuthenticationScheme.cs +++ b/bff/src/Bff/Internal/ActiveOpenIdConnectAuthenticationScheme.cs @@ -2,7 +2,7 @@ // See LICENSE in the project root for license information. using Duende.Bff.AccessTokenManagement; -using Duende.Bff.DynamicFrontends; +using Duende.Bff.DynamicFrontends.Internal; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Options; @@ -11,10 +11,10 @@ namespace Duende.Bff.Internal; /// /// Centralizes the logic for determining if the OpenID Connect authentication scheme should be configured based on the currently selected frontend and the default authentication scheme. /// -/// +/// /// internal sealed class ActiveOpenIdConnectAuthenticationScheme( - CurrentFrontendAccessor currentFrontendAccessor, + FrontendSelector frontendSelector, IOptions authOptions) { private readonly Scheme? _defaultAuthenticationScheme = Scheme.ParseOrDefault(authOptions.Value.DefaultChallengeScheme ?? authOptions.Value.DefaultScheme); @@ -29,6 +29,6 @@ internal sealed class ActiveOpenIdConnectAuthenticationScheme( // Either the currently selected scheme is the default scheme (_defaultAuthenticationScheme == schemeName) // Or it's the correct scheme for the currently selected frontend - || (currentFrontendAccessor.TryGet(out var frontend) && schemeName == frontend.OidcSchemeName); + || frontendSelector.TryGetFrontendByOidcScheme(schemeName, out _); } diff --git a/bff/test/Bff.Tests/BffOptionsConfigurationTests.cs b/bff/test/Bff.Tests/BffOptionsConfigurationTests.cs new file mode 100644 index 000000000..3671566f2 --- /dev/null +++ b/bff/test/Bff.Tests/BffOptionsConfigurationTests.cs @@ -0,0 +1,77 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + +using System.Net; +using Duende.Bff.Tests.TestFramework; +using Duende.Bff.Tests.TestInfra; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.Extensions.Options; +using Xunit.Abstractions; + +namespace Duende.Bff.Tests; + +public class BffOptionsConfigurationTests(ITestOutputHelper output) : BffTestBase(output) +{ + [Theory] + [MemberData(nameof(AllSetups))] + public async Task calls_outside_http_context_can_get_oidc_configuration(BffSetupType setup) + { + Bff.OnConfigureApp += app => + { + app.Map(The.Path, c => ApiHost.ReturnApiCallDetails(c, () => HttpStatusCode.OK)) + .RequireAuthorization() + .AsBffApiEndpoint(); + }; + + await ConfigureBff(setup); + + // this retrieves the openid connect options outside the http context. This shouldn't + // normally happen but AzureAppConfigurationRefreshMiddleware can cause this. + // when this happens, this call would fail with No HTTP Context available, + // but also all subsequent requests, because IOptionsCache caches this. + var opt = Bff.Resolve>(); + opt.Get(Some.BffFrontend().OidcSchemeName); + + await Bff.BrowserClient.Login(); + + ApiCallDetails apiResult = await Bff.BrowserClient.CallBffHostApi( + url: Bff.Url(The.Path) + ); + + apiResult.Method.ShouldBe(HttpMethod.Get); + apiResult.Path.ShouldBe(The.Path); + apiResult.Sub.ShouldBe(The.Sub); + } + + [Theory] + [MemberData(nameof(AllSetups))] + public async Task calls_outside_http_context_can_get_cookie_configuration(BffSetupType setup) + { + Bff.OnConfigureApp += app => + { + app.Map(The.Path, c => ApiHost.ReturnApiCallDetails(c, () => HttpStatusCode.OK)) + .RequireAuthorization() + .AsBffApiEndpoint(); + }; + + await ConfigureBff(setup); + + // this retrieves the cookie connect options outside the http context. This shouldn't + // normally happen but AzureAppConfigurationRefreshMiddleware can cause this. + // when this happens, this call would fail with No HTTP Context available, + // but also all subsequent requests, because IOptionsCache caches this. + var opt = Bff.Resolve>(); + opt.Get(Some.BffFrontend().CookieSchemeName); + + await Bff.BrowserClient.Login(); + + ApiCallDetails apiResult = await Bff.BrowserClient.CallBffHostApi( + url: Bff.Url(The.Path) + ); + + apiResult.Method.ShouldBe(HttpMethod.Get); + apiResult.Path.ShouldBe(The.Path); + apiResult.Sub.ShouldBe(The.Sub); + } +}