diff --git a/bff/hosts/Blazor/PerComponent/Hosts.Bff.Blazor.PerComponent/Program.cs b/bff/hosts/Blazor/PerComponent/Hosts.Bff.Blazor.PerComponent/Program.cs index 704da896d..14314fc20 100644 --- a/bff/hosts/Blazor/PerComponent/Hosts.Bff.Blazor.PerComponent/Program.cs +++ b/bff/hosts/Blazor/PerComponent/Hosts.Bff.Blazor.PerComponent/Program.cs @@ -96,8 +96,6 @@ app.UseBff(); app.UseAuthorization(); app.UseAntiforgery(); -app.MapBffManagementEndpoints(); - app.MapRemoteBffApiEndpoint("/remote-apis/user-token", new Uri("https://localhost:5010")) .WithAccessToken(RequiredTokenType.User); diff --git a/bff/hosts/Blazor/WebAssembly/Hosts.Bff.Blazor.WebAssembly/Program.cs b/bff/hosts/Blazor/WebAssembly/Hosts.Bff.Blazor.WebAssembly/Program.cs index ec7e83741..2f1e8fcb3 100644 --- a/bff/hosts/Blazor/WebAssembly/Hosts.Bff.Blazor.WebAssembly/Program.cs +++ b/bff/hosts/Blazor/WebAssembly/Hosts.Bff.Blazor.WebAssembly/Program.cs @@ -83,7 +83,6 @@ app.MapRazorComponents() .AddInteractiveWebAssemblyRenderMode() .AddAdditionalAssemblies(typeof(Hosts.Bff.Blazor.WebAssembly.Client._Imports).Assembly); -app.MapBffManagementEndpoints(); WeatherEndpoints.Map(app); app.Run(); diff --git a/bff/hosts/Hosts.Bff.EF/Extensions.cs b/bff/hosts/Hosts.Bff.EF/Extensions.cs index 045ae9f95..54d11d2a2 100644 --- a/bff/hosts/Hosts.Bff.EF/Extensions.cs +++ b/bff/hosts/Hosts.Bff.EF/Extensions.cs @@ -100,9 +100,6 @@ internal static class Extensions .RequireAuthorization() .AsBffApiEndpoint(); - // login, logout, user, backchannel logout... - app.MapBffManagementEndpoints(); - // proxy endpoint for cross-site APIs // all calls to /api/* will be forwarded to the remote API // user or client access token will be attached in API call diff --git a/bff/hosts/Hosts.Bff.InMemory/Program.cs b/bff/hosts/Hosts.Bff.InMemory/Program.cs index 509784249..79a6b3352 100644 --- a/bff/hosts/Hosts.Bff.InMemory/Program.cs +++ b/bff/hosts/Hosts.Bff.InMemory/Program.cs @@ -76,9 +76,6 @@ app.MapControllers() .RequireAuthorization() .AsBffApiEndpoint(); -// login, logout, user, backchannel logout... -app.MapBffManagementEndpoints(); - ////////////////////////////////////// // proxy app for cross-site APIs ////////////////////////////////////// diff --git a/bff/hosts/Hosts.Bff.MultiFrontend/Program.cs b/bff/hosts/Hosts.Bff.MultiFrontend/Program.cs index 2dd8c9fcc..bf894b115 100644 --- a/bff/hosts/Hosts.Bff.MultiFrontend/Program.cs +++ b/bff/hosts/Hosts.Bff.MultiFrontend/Program.cs @@ -182,8 +182,6 @@ app.MapGet("/local/invokes-external-api", async (SelectedFrontend frontend, IHtt return data; }); -app.MapBffManagementEndpoints(); - app.Run(); RouteConfig[] BuildYarpRoutes() diff --git a/bff/performance/Bff.Benchmarks/Hosts/BffHost.cs b/bff/performance/Bff.Benchmarks/Hosts/BffHost.cs index dad65d90f..20f3b94be 100644 --- a/bff/performance/Bff.Benchmarks/Hosts/BffHost.cs +++ b/bff/performance/Bff.Benchmarks/Hosts/BffHost.cs @@ -49,8 +49,6 @@ public class BffHost : Host app.UseBff(); app.MapGet("/", () => "bff"); - app.MapBffManagementEndpoints(); - app.MapRemoteBffApiEndpoint("/allow_anon", apiUri); app.MapRemoteBffApiEndpoint("/client_token", apiUri) .WithAccessToken(RequiredTokenType.Client); diff --git a/bff/src/Bff/BffEndpointRouteBuilderExtensions.cs b/bff/src/Bff/BffEndpointRouteBuilderExtensions.cs index f816987ef..329da6e01 100644 --- a/bff/src/Bff/BffEndpointRouteBuilderExtensions.cs +++ b/bff/src/Bff/BffEndpointRouteBuilderExtensions.cs @@ -35,6 +35,12 @@ public static class BffEndpointRouteBuilderExtensions /// public static void MapBffManagementEndpoints(this IEndpointRouteBuilder endpoints) { + var options = endpoints.ServiceProvider.GetRequiredService>().Value; + if (endpoints.AlreadyMappedManagementEndpoint(options.LoginPath, "Login")) + { + return; + } + endpoints.MapBffManagementLoginEndpoint(); #pragma warning disable CS0618 // Type or member is obsolete endpoints.MapBffManagementSilentLoginEndpoints(); @@ -60,6 +66,18 @@ public static class BffEndpointRouteBuilderExtensions .AllowAnonymous(); } + internal static bool AlreadyMappedManagementEndpoint(this IEndpointRouteBuilder endpoints, PathString route, string name) + { + if (endpoints.DataSources.Any(x => + x.Endpoints.OfType().Any(x => x.RoutePattern.RawText == route.ToString()))) + { + endpoints.ServiceProvider.GetRequiredService>().LogWarning("Already mapped {name} endpoint, so the call to MapBffManagementEndpoints will be ignored. If you're using BffOptions.AutomaticallyRegisterBffMiddleware, you don't need to call endpoints.MapBffManagementEndpoints()", name); + return true; + } + + return false; + } + /// /// Adds the silent login BFF management endpoints /// @@ -72,6 +90,7 @@ public static class BffEndpointRouteBuilderExtensions var options = endpoints.ServiceProvider.GetRequiredService>().Value; endpoints.MapGet(options.SilentLoginPath.Value!, ProcessWith) + .WithName("SilentLogin") .WithMetadata(new BffUiEndpointAttribute()) .AllowAnonymous(); @@ -91,6 +110,7 @@ public static class BffEndpointRouteBuilderExtensions var options = endpoints.ServiceProvider.GetRequiredService>().Value; endpoints.MapGet(options.LogoutPath.Value!, ProcessWith) + .WithName("Logout") .WithMetadata(new BffUiEndpointAttribute()) .AllowAnonymous(); } diff --git a/bff/src/Bff/DynamicFrontends/Internal/ConfigureBffStartupFilter.cs b/bff/src/Bff/DynamicFrontends/Internal/ConfigureBffStartupFilter.cs index 5efa09b21..4cd94eb74 100644 --- a/bff/src/Bff/DynamicFrontends/Internal/ConfigureBffStartupFilter.cs +++ b/bff/src/Bff/DynamicFrontends/Internal/ConfigureBffStartupFilter.cs @@ -34,7 +34,15 @@ internal class ConfigureBffStartupFilter : IStartupFilter } if (bffOptions.AutomaticallyRegisterBffMiddleware) { + app.UseEndpoints(endpoints => + { + if (!endpoints.AlreadyMappedManagementEndpoint(bffOptions.LoginPath, "Login")) + { + endpoints.MapBffManagementEndpoints(); + } + }); app.UseBffIndexPages(); + } ConfigureOpenIdConfigurationCacheExpiration(app); diff --git a/bff/test/Bff.Tests/Headers/General.cs b/bff/test/Bff.Tests/Headers/GeneralTests.cs similarity index 78% rename from bff/test/Bff.Tests/Headers/General.cs rename to bff/test/Bff.Tests/Headers/GeneralTests.cs index b66fc9ec5..a4e00e827 100644 --- a/bff/test/Bff.Tests/Headers/General.cs +++ b/bff/test/Bff.Tests/Headers/GeneralTests.cs @@ -11,7 +11,7 @@ using ApiHost = Duende.Bff.Tests.TestInfra.ApiHost; namespace Duende.Bff.Tests.Headers; -public class General(ITestOutputHelper output) : BffTestBase(output) +public class GeneralTests(ITestOutputHelper output) : BffTestBase(output) { [Theory, MemberData(nameof(AllSetups))] public async Task local_endpoint_should_receive_standard_headers(BffSetupType setup) @@ -90,4 +90,31 @@ public class General(ITestOutputHelper output) : BffTestBase(output) apiResult.RequestHeaders["Host"].Single().ShouldBe("api"); apiResult.RequestHeaders["x-custom"].Single().ShouldBe("custom"); } + + [Theory, MemberData(nameof(AllSetups))] + public async Task Will_auto_register_login_endpoints(BffSetupType setup) + { + ConfigureBff(setup); + await InitializeAsync(); + + Context.LogMessages.ToString().ShouldNotContain("Already mapped Login endpoint"); + + // And we can log in, which means the login endpoint was registered + await Bff.BrowserClient.Login(); + } + + [Theory, MemberData(nameof(AllSetups))] + public async Task If_management_endpoints_are_mapped_manually_then_warning_is_written(BffSetupType setup) + { + Bff.OnConfigureEndpoints += endpoints => endpoints.MapBffManagementEndpoints(); + ConfigureBff(setup); + await InitializeAsync(); + + // And we can log in, which means the login endpoint was registered + await Bff.BrowserClient.Login(); + + Context.LogMessages.ToString().ShouldContain("Already mapped Login endpoint"); + } + + } diff --git a/bff/test/Bff.Tests/TestInfra/BffTestHost.cs b/bff/test/Bff.Tests/TestInfra/BffTestHost.cs index a2bb6dbd9..e653b7254 100644 --- a/bff/test/Bff.Tests/TestInfra/BffTestHost.cs +++ b/bff/test/Bff.Tests/TestInfra/BffTestHost.cs @@ -66,8 +66,6 @@ public class BffTestHost(TestHostContext context, IdentityServerTestHost identit endpoints.MapGet("/", () => DefaultRootResponse); } - endpoints.MapBffManagementEndpoints(); - }; } diff --git a/bff/test/Bff.Tests/TestInfra/TestHostContext.cs b/bff/test/Bff.Tests/TestInfra/TestHostContext.cs index db214ee8e..83042efbf 100644 --- a/bff/test/Bff.Tests/TestInfra/TestHostContext.cs +++ b/bff/test/Bff.Tests/TestInfra/TestHostContext.cs @@ -1,6 +1,7 @@ // Copyright (c) Duende Software. All rights reserved. // See LICENSE in the project root for license information. +using System.Text; using Xunit.Abstractions; namespace Duende.Bff.Tests.TestInfra; @@ -8,7 +9,14 @@ namespace Duende.Bff.Tests.TestInfra; public record TestHostContext(ITestOutputHelper OutputHelper) { public readonly SimulatedInternet Internet = new SimulatedInternet(OutputHelper.WriteLine); - public WriteTestOutput WriteOutput => OutputHelper.WriteLine; + public WriteTestOutput WriteOutput => s => + { + OutputHelper.WriteLine(s); + LogMessages.AppendLine(s); + }; public readonly TestData The = new TestData(); public TestDataBuilder Some => new TestDataBuilder(The); + + public readonly StringBuilder LogMessages = new StringBuilder(); + }