make sure the options configuration code doesn't fail if it runs outside the http context

This commit is contained in:
Erwin van der Valk 2026-02-09 12:15:28 +01:00
parent e65c97126d
commit d046ff590f
7 changed files with 128 additions and 13 deletions

View file

@ -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> bffConfiguration,
IOptions<BffOptions> bffOptions,
CurrentFrontendAccessor currentFrontendAccessor
FrontendSelector frontendSelector
) : IConfigureNamedOptions<CookieAuthenticationOptions>
{
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)
{

View file

@ -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> bffConfiguration,
IOptions<BffOptions> 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;
}

View file

@ -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<Scheme, BffFrontend> _frontendsByOidcScheme = new();
private readonly Dictionary<Scheme, BffFrontend> _frontendsByCookieScheme = new();
private readonly Dictionary<HostString, PathTrie<BffFrontend>> _perHostHeader = new();
private readonly PathTrie<BffFrontend> _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))

View file

@ -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);
}

View file

@ -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;
/// <summary>
/// Centralizes the logic for determining if the cookie authentication scheme should be configured based on the currently selected frontend and the default authentication scheme.
/// </summary>
internal sealed class ActiveCookieAuthenticationScheme(CurrentFrontendAccessor currentFrontendAccessor, IOptions<AuthenticationOptions> authOptions)
internal sealed class ActiveCookieAuthenticationScheme(FrontendSelector frontendSelector, IOptions<AuthenticationOptions> 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 _));
}

View file

@ -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;
/// <summary>
/// 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.
/// </summary>
/// <param name="currentFrontendAccessor"></param>
/// <param name="frontendSelector"></param>
/// <param name="authOptions"></param>
internal sealed class ActiveOpenIdConnectAuthenticationScheme(
CurrentFrontendAccessor currentFrontendAccessor,
FrontendSelector frontendSelector,
IOptions<AuthenticationOptions> 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 _);
}

View file

@ -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<IOptionsMonitor<OpenIdConnectOptions>>();
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<IOptionsMonitor<CookieAuthenticationOptions>>();
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);
}
}