From e47064fa1b3471e4d894c1ec4a9f9c17c22c4391 Mon Sep 17 00:00:00 2001 From: Erwin van der Valk Date: Thu, 12 Jun 2025 16:32:55 +0200 Subject: [PATCH] Rewriting the tests to use new test setup (#2052) * refactor IAccessTokenRetrieverTests * refactoring tests * ManagementBasePathTests * LogoutEndpointTests --- .../Hosts.Bff.Blazor.PerComponent/Program.cs | 2 +- bff/hosts/Hosts.Bff.DPoP/Extensions.cs | 10 +- bff/hosts/Hosts.Bff.EF/Extensions.cs | 2 +- bff/hosts/Hosts.Bff.InMemory/Extensions.cs | 14 +- .../Bff.Benchmarks/Hosts/BffHost.cs | 6 +- bff/src/Bff.Yarp/RouteBuilderExtensions.cs | 5 +- .../OpenIdConnectCallbackMiddleware.cs | 9 + .../Bff/Endpoints/Internal/IUserEndpoint.cs | 0 .../DPoPTestsWithManualAuthentication.cs | 2 +- .../Endpoints/DpopRemoteEndpointTests.cs | 2 +- .../Management/LogoutEndpointTests.cs | 167 ++++++++++-------- .../Management/ManagementBasePathTests.cs | 36 ++-- .../Endpoints/Management/UserEndpointTests.cs | 60 +++++-- .../Headers/ApiAndBffUseForwardedHeaders.cs | 2 +- .../Headers/ApiUseForwardedHeaders.cs | 2 +- ...ccessTokenRetriever_Extensibility_tests.cs | 74 ++++---- ...ests.VerifyPublicApi_Bff_Yarp.verified.txt | 2 +- bff/test/Bff.Tests/TestHosts/BffHost.cs | 22 +-- .../BffHostUsingResourceNamedTokens.cs | 4 +- bff/test/Bff.Tests/TestInfra/BffHttpClient.cs | 49 ++++- bff/test/Bff.Tests/TestInfra/BffTestBase.cs | 26 ++- bff/test/Bff.Tests/TestInfra/BffTestHost.cs | 37 ++-- .../TestInfra/IdentityServerTestHost.cs | 3 +- .../Bff.Tests/TestInfra/SimulatedInternet.cs | 6 +- bff/test/Bff.Tests/TestInfra/TestData.cs | 1 + bff/test/Bff.Tests/TestInfra/TestHost.cs | 5 +- start-influxdb-grafana.ps1 | 0 27 files changed, 350 insertions(+), 198 deletions(-) create mode 100644 bff/src/Bff/Endpoints/Internal/IUserEndpoint.cs create mode 100644 start-influxdb-grafana.ps1 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 e69d067ba..704da896d 100644 --- a/bff/hosts/Blazor/PerComponent/Hosts.Bff.Blazor.PerComponent/Program.cs +++ b/bff/hosts/Blazor/PerComponent/Hosts.Bff.Blazor.PerComponent/Program.cs @@ -98,7 +98,7 @@ app.UseAntiforgery(); app.MapBffManagementEndpoints(); -app.MapRemoteBffApiEndpoint("/remote-apis/user-token", "https://localhost:5010") +app.MapRemoteBffApiEndpoint("/remote-apis/user-token", new Uri("https://localhost:5010")) .WithAccessToken(RequiredTokenType.User); app.MapRazorComponents() diff --git a/bff/hosts/Hosts.Bff.DPoP/Extensions.cs b/bff/hosts/Hosts.Bff.DPoP/Extensions.cs index 17927b66f..083ba573c 100644 --- a/bff/hosts/Hosts.Bff.DPoP/Extensions.cs +++ b/bff/hosts/Hosts.Bff.DPoP/Extensions.cs @@ -184,22 +184,22 @@ internal static class Extensions private static void MapRemoteUrls(IEndpointRouteBuilder app) { // On this path, we use a client credentials token - app.MapRemoteBffApiEndpoint("/api/client-token", "https://localhost:5011") + app.MapRemoteBffApiEndpoint("/api/client-token", new Uri("https://localhost:5011")) .WithAccessToken(RequiredTokenType.Client); // On this path, we use a user token if logged in, and fall back to a client credentials token if not - app.MapRemoteBffApiEndpoint("/api/user-or-client-token", "https://localhost:5011") + app.MapRemoteBffApiEndpoint("/api/user-or-client-token", new Uri("https://localhost:5011")) .WithAccessToken(RequiredTokenType.UserOrClient); // On this path, we make anonymous requests - app.MapRemoteBffApiEndpoint("/api/anonymous", "https://localhost:5011"); + app.MapRemoteBffApiEndpoint("/api/anonymous", new Uri("https://localhost:5011")); // On this path, we use the client token only if the user is logged in - app.MapRemoteBffApiEndpoint("/api/optional-user-token", "https://localhost:5011") + app.MapRemoteBffApiEndpoint("/api/optional-user-token", new Uri("https://localhost:5011")) .WithAccessToken(RequiredTokenType.UserOrNone); // On this path, we require the user token - app.MapRemoteBffApiEndpoint("/api/user-token", "https://localhost:5011") + app.MapRemoteBffApiEndpoint("/api/user-token", new Uri("https://localhost:5011")) .WithAccessToken(); } } diff --git a/bff/hosts/Hosts.Bff.EF/Extensions.cs b/bff/hosts/Hosts.Bff.EF/Extensions.cs index 1d0695169..045ae9f95 100644 --- a/bff/hosts/Hosts.Bff.EF/Extensions.cs +++ b/bff/hosts/Hosts.Bff.EF/Extensions.cs @@ -107,7 +107,7 @@ internal static class Extensions // all calls to /api/* will be forwarded to the remote API // user or client access token will be attached in API call // user access token will be managed automatically using the refresh token - app.MapRemoteBffApiEndpoint("/api", "https://localhost:5010") + app.MapRemoteBffApiEndpoint("/api", new Uri("https://localhost:5010")) .WithAccessToken(RequiredTokenType.UserOrClient); return app; diff --git a/bff/hosts/Hosts.Bff.InMemory/Extensions.cs b/bff/hosts/Hosts.Bff.InMemory/Extensions.cs index 70efc0e07..af01635d1 100644 --- a/bff/hosts/Hosts.Bff.InMemory/Extensions.cs +++ b/bff/hosts/Hosts.Bff.InMemory/Extensions.cs @@ -128,33 +128,33 @@ internal static class Extensions ////////////////////////////////////// // On this path, we use a client credentials token - app.MapRemoteBffApiEndpoint("/api/client-token", "https://localhost:5010") + app.MapRemoteBffApiEndpoint("/api/client-token", new Uri("https://localhost:5010")) .WithAccessToken(RequiredTokenType.Client); // On this path, we use a user token if logged in, and fall back to a client credentials token if not - app.MapRemoteBffApiEndpoint("/api/user-or-client-token", "https://localhost:5010") + app.MapRemoteBffApiEndpoint("/api/user-or-client-token", new Uri("https://localhost:5010")) .WithAccessToken(RequiredTokenType.UserOrClient); // On this path, we make anonymous requests - app.MapRemoteBffApiEndpoint("/api/anonymous", "https://localhost:5010"); + app.MapRemoteBffApiEndpoint("/api/anonymous", new Uri("https://localhost:5010")); // On this path, we use the client token only if the user is logged in - app.MapRemoteBffApiEndpoint("/api/optional-user-token", "https://localhost:5010") + app.MapRemoteBffApiEndpoint("/api/optional-user-token", new Uri("https://localhost:5010")) .WithAccessToken(RequiredTokenType.UserOrNone); // On this path, we require the user token - app.MapRemoteBffApiEndpoint("/api/user-token", "https://localhost:5010") + app.MapRemoteBffApiEndpoint("/api/user-token", new Uri("https://localhost:5010")) .WithAccessToken(); // On this path, we perform token exchange to impersonate a different user // before making the api request - app.MapRemoteBffApiEndpoint("/api/impersonation", "https://localhost:5010") + app.MapRemoteBffApiEndpoint("/api/impersonation", new Uri("https://localhost:5010")) .WithAccessToken() .WithAccessTokenRetriever(); // On this path, we obtain an audience constrained token and invoke // a different api that requires such a token - app.MapRemoteBffApiEndpoint("/api/audience-constrained", "https://localhost:5012") + app.MapRemoteBffApiEndpoint("/api/audience-constrained", new Uri("https://localhost:5012")) .WithAccessToken() .WithUserAccessTokenParameter(new BffUserAccessTokenParameters { Resource = Resource.Parse("urn:isolated-api") }); diff --git a/bff/performance/Bff.Benchmarks/Hosts/BffHost.cs b/bff/performance/Bff.Benchmarks/Hosts/BffHost.cs index 960ad5155..dad65d90f 100644 --- a/bff/performance/Bff.Benchmarks/Hosts/BffHost.cs +++ b/bff/performance/Bff.Benchmarks/Hosts/BffHost.cs @@ -51,11 +51,11 @@ public class BffHost : Host app.MapBffManagementEndpoints(); - app.MapRemoteBffApiEndpoint("/allow_anon", apiUri.ToString()); - app.MapRemoteBffApiEndpoint("/client_token", apiUri.ToString()) + app.MapRemoteBffApiEndpoint("/allow_anon", apiUri); + app.MapRemoteBffApiEndpoint("/client_token", apiUri) .WithAccessToken(RequiredTokenType.Client); - app.MapRemoteBffApiEndpoint("/user_token", apiUri.ToString()) + app.MapRemoteBffApiEndpoint("/user_token", apiUri) .WithAccessToken(RequiredTokenType.User); }; } diff --git a/bff/src/Bff.Yarp/RouteBuilderExtensions.cs b/bff/src/Bff.Yarp/RouteBuilderExtensions.cs index ff15baae4..10c9bcf66 100644 --- a/bff/src/Bff.Yarp/RouteBuilderExtensions.cs +++ b/bff/src/Bff.Yarp/RouteBuilderExtensions.cs @@ -26,7 +26,7 @@ public static class RouteBuilderExtensions public static IEndpointConventionBuilder MapRemoteBffApiEndpoint( this IEndpointRouteBuilder endpoints, PathString localPath, - string apiAddress, + Uri apiAddress, Action? yarpTransformBuilder = null) { endpoints.CheckLicense(); @@ -45,11 +45,12 @@ public static class RouteBuilderExtensions return endpoints.MapForwarder( pattern: localPath.Add("/{**catch-all}").Value!, - destinationPrefix: apiAddress, + destinationPrefix: apiAddress.ToString(), configureTransform: context => { yarpTransformBuilder(context); }) .WithMetadata(new BffRemoteApiEndpointMetadata()); } + } diff --git a/bff/src/Bff/DynamicFrontends/Internal/OpenIdConnectCallbackMiddleware.cs b/bff/src/Bff/DynamicFrontends/Internal/OpenIdConnectCallbackMiddleware.cs index e1aa5b32f..45e306762 100644 --- a/bff/src/Bff/DynamicFrontends/Internal/OpenIdConnectCallbackMiddleware.cs +++ b/bff/src/Bff/DynamicFrontends/Internal/OpenIdConnectCallbackMiddleware.cs @@ -32,6 +32,15 @@ internal class OpenIdConnectCallbackMiddleware(RequestDelegate next, return; } } + if (context.Request.Path.StartsWithSegments(options.SignedOutCallbackPath)) + { + var handlers = context.RequestServices.GetRequiredService(); + if (await handlers.GetHandlerAsync(context, frontend.OidcSchemeName) is IAuthenticationRequestHandler handler) + { + await handler.HandleRequestAsync(); + return; + } + } await next(context); } diff --git a/bff/src/Bff/Endpoints/Internal/IUserEndpoint.cs b/bff/src/Bff/Endpoints/Internal/IUserEndpoint.cs new file mode 100644 index 000000000..e69de29bb diff --git a/bff/test/Bff.Tests/Endpoints/DPoPTestsWithManualAuthentication.cs b/bff/test/Bff.Tests/Endpoints/DPoPTestsWithManualAuthentication.cs index 747f03871..bb8aa37db 100644 --- a/bff/test/Bff.Tests/Endpoints/DPoPTestsWithManualAuthentication.cs +++ b/bff/test/Bff.Tests/Endpoints/DPoPTestsWithManualAuthentication.cs @@ -44,7 +44,7 @@ public class DPoPTestsWithManualAuthentication : BffTestBase, IAsyncLifetime Bff.OnConfigureBff += bff => bff.AddRemoteApis(); Bff.OnConfigureEndpoints += endpoints => { - endpoints.MapRemoteBffApiEndpoint(The.Path, Api.Url().ToString()) + endpoints.MapRemoteBffApiEndpoint(The.Path, Api.Url()) .WithAccessToken(RequiredTokenType.Client) ; }; diff --git a/bff/test/Bff.Tests/Endpoints/DpopRemoteEndpointTests.cs b/bff/test/Bff.Tests/Endpoints/DpopRemoteEndpointTests.cs index e7e46e8ad..c5c5d9bcc 100644 --- a/bff/test/Bff.Tests/Endpoints/DpopRemoteEndpointTests.cs +++ b/bff/test/Bff.Tests/Endpoints/DpopRemoteEndpointTests.cs @@ -29,7 +29,7 @@ public class DpopRemoteEndpointTests : BffTestBase, IAsyncLifetime Bff.OnConfigureBff += bff => bff.AddRemoteApis(); Bff.OnConfigureEndpoints += endpoints => { - endpoints.MapRemoteBffApiEndpoint(The.Path, Api.Url().ToString()) + endpoints.MapRemoteBffApiEndpoint(The.Path, Api.Url()) .WithAccessToken(RequiredTokenType.Client) ; }; diff --git a/bff/test/Bff.Tests/Endpoints/Management/LogoutEndpointTests.cs b/bff/test/Bff.Tests/Endpoints/Management/LogoutEndpointTests.cs index 6717b1eec..dc0ded94f 100644 --- a/bff/test/Bff.Tests/Endpoints/Management/LogoutEndpointTests.cs +++ b/bff/test/Bff.Tests/Endpoints/Management/LogoutEndpointTests.cs @@ -2,19 +2,21 @@ // See LICENSE in the project root for license information. using System.Net; -using Duende.Bff.Configuration; -using Duende.Bff.Tests.TestHosts; +using System.Security.Claims; using Duende.Bff.Tests.TestInfra; using Duende.IdentityModel; +using Microsoft.AspNetCore.Authentication; using Xunit.Abstractions; namespace Duende.Bff.Tests.Endpoints.Management; -public class LogoutEndpointTests(ITestOutputHelper output) : BffIntegrationTestBase(output) +public class LogoutEndpointTests(ITestOutputHelper output) : BffTestBase(output) { - [Fact] - public async Task logout_endpoint_should_allow_anonymous() + [Theory] + [MemberData(nameof(AllSetups))] + public async Task logout_endpoint_should_allow_anonymous(BffSetupType setup) { + ConfigureBff(setup); Bff.OnConfigureServices += svcs => { svcs.AddAuthorization(opts => @@ -25,134 +27,155 @@ public class LogoutEndpointTests(ITestOutputHelper output) : BffIntegrationTestB .Build(); }); }; - await Bff.InitializeAsync(); + + await InitializeAsync(); var response = await Bff.BrowserClient.GetAsync(Bff.Url("/bff/logout")); response.StatusCode.ShouldNotBe(HttpStatusCode.Unauthorized); } - [Fact] - public async Task logout_endpoint_should_signout() + [Theory] + [MemberData(nameof(AllSetups))] + public async Task logout_endpoint_should_signout(BffSetupType setup) { - await Bff.BffLoginAsync("alice", "sid123"); + ConfigureBff(setup); + await InitializeAsync(); - await Bff.BffLogoutAsync("sid123"); + await Bff.BrowserClient.Login(); - (await Bff.GetIsUserLoggedInAsync()).ShouldBeFalse(); + var response = await Bff.BrowserClient.Logout(); + response.StatusCode.ShouldBe(HttpStatusCode.OK); + response.RequestMessage!.RequestUri.ShouldBe(Bff.Url()); + + (await Bff.BrowserClient.GetIsUserLoggedInAsync()).ShouldBeFalse(); } - [Fact] - public async Task logout_endpoint_for_authenticated_should_require_sid() + [Theory] + [MemberData(nameof(AllSetups))] + public async Task logout_endpoint_for_authenticated_should_require_sid(BffSetupType setup) { - await Bff.BffLoginAsync("alice", "sid123"); + ConfigureBff(setup); + await InitializeAsync(); + await Bff.BrowserClient.Login(); var problem = await Bff.BrowserClient.GetAsync(Bff.Url("/bff/logout")) .ShouldBeProblem(); problem.Errors.ShouldContainKey(JwtClaimTypes.SessionId); - (await Bff.GetIsUserLoggedInAsync()).ShouldBeTrue(); + (await Bff.BrowserClient.GetIsUserLoggedInAsync()).ShouldBeTrue(); } - [Fact] - public async Task logout_endpoint_for_authenticated_when_require_option_is_false_should_not_require_sid() + [Theory] + [MemberData(nameof(AllSetups))] + public async Task logout_endpoint_for_authenticated_when_require_option_is_false_should_not_require_sid(BffSetupType setup) { - await Bff.BffLoginAsync("alice", "sid123"); + ConfigureBff(setup); + await InitializeAsync(); + await Bff.BrowserClient.Login(); Bff.BffOptions.RequireLogoutSessionId = false; + Bff.BrowserClient.RedirectHandler.AutoFollowRedirects = false; var response = await Bff.BrowserClient.GetAsync(Bff.Url("/bff/logout")); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // endsession - response.Headers.Location!.ToString().ToLowerInvariant().ShouldStartWith(IdentityServer.Url("/connect/endsession")); + response.Headers.Location!.ToString().ToLowerInvariant().ShouldStartWith(IdentityServer.Url("/connect/endsession").ToString()); } - [Fact] - public async Task logout_endpoint_for_authenticated_user_without_sid_should_succeed() + [Theory] + [MemberData(nameof(AllSetups))] + public async Task logout_endpoint_for_authenticated_user_without_sid_should_succeed(BffSetupType setup) { - // workaround for RevokeUserRefreshTokenAsync throwing when no RT in session - Bff.OnConfigureServices += svcs => + + // Workaround to place a session cookie in the BFF without a session id claim. + Bff.OnConfigureEndpoints += endpoints => { - svcs.Configure(options => + endpoints.MapGet("/__signin", async ctx => { - options.RevokeRefreshTokenOnLogout = false; + var props = new AuthenticationProperties(); + await ctx.SignInAsync(new ClaimsPrincipal(new ClaimsIdentity([new Claim(JwtClaimTypes.Subject, The.Sub)], "test", "name", "role")), props); + + ctx.Response.StatusCode = 204; }); }; - await Bff.InitializeAsync(); - await Bff.IssueSessionCookieAsync("alice"); + ConfigureBff(setup); + await InitializeAsync(); + await Bff.BrowserClient.GetAsync("__signin"); + + // workaround for RevokeUserRefreshTokenAsync throwing when no RT in session + Bff.BffOptions.RevokeRefreshTokenOnLogout = false; + + Bff.BrowserClient.RedirectHandler.AutoFollowRedirects = false; var response = await Bff.BrowserClient.GetAsync(Bff.Url("/bff/logout")); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // endsession - response.Headers.Location!.ToString().ToLowerInvariant().ShouldStartWith(IdentityServer.Url("/connect/endsession")); + response.Headers.Location!.ToString().ToLowerInvariant().ShouldStartWith(IdentityServer.Url("/connect/endsession").ToString()); } - [Fact] - public async Task logout_endpoint_for_anonymous_user_without_sid_should_succeed() + [Theory] + [MemberData(nameof(AllSetups))] + public async Task logout_endpoint_for_anonymous_user_without_sid_should_succeed(BffSetupType setup) { + ConfigureBff(setup); + await InitializeAsync(); + + Bff.BrowserClient.RedirectHandler.AutoFollowRedirects = false; + var response = await Bff.BrowserClient.GetAsync(Bff.Url("/bff/logout")); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // endsession - response.Headers.Location!.ToString().ToLowerInvariant().ShouldStartWith(IdentityServer.Url("/connect/endsession")); + response.Headers.Location!.ToString().ToLowerInvariant().ShouldStartWith(IdentityServer.Url("/connect/endsession").ToString()); } - [Fact] - public async Task logout_endpoint_should_redirect_to_external_signout_and_return_to_root() + [Theory] + [MemberData(nameof(AllSetups))] + public async Task can_logout_twice(BffSetupType setup) { - await Bff.BffLoginAsync("alice", "sid123"); + ConfigureBff(setup); + await InitializeAsync(); + await Bff.BrowserClient.Login(); - await Bff.BffLogoutAsync("sid123"); + var sid = await Bff.BrowserClient.GetSid(); + await Bff.BrowserClient.Logout(sid) + .CheckHttpStatusCode(); - Bff.BrowserClient.CurrentUri - .ShouldNotBeNull() - .ToString() - .ToLowerInvariant() - .ShouldBe(Bff.Url("/")); + Bff.BrowserClient.RedirectHandler.AutoFollowRedirects = false; + var response = await Bff.BrowserClient.Logout(sid); - (await Bff.GetIsUserLoggedInAsync()).ShouldBeFalse(); - } - - [Fact] - public async Task can_logout_twice() - { - await Bff.BffLoginAsync("alice", "sid123"); - - await Bff.BffLogoutAsync("sid123"); - - var response = await Bff.BrowserClient.GetAsync(Bff.Url("/bff/logout") + "?sid=123"); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // endsession - response.Headers.Location!.ToString().ToLowerInvariant().ShouldStartWith(IdentityServer.Url("/connect/endsession")); + response.Headers.Location!.ToString().ToLowerInvariant().ShouldStartWith(IdentityServer.Url("/connect/endsession").ToString()); - (await Bff.GetIsUserLoggedInAsync()).ShouldBeFalse(); + (await Bff.BrowserClient.GetIsUserLoggedInAsync()).ShouldBeFalse(); } - [Fact] - public async Task logout_endpoint_should_accept_returnUrl() + + [Theory] + [MemberData(nameof(AllSetups))] + public async Task logout_endpoint_should_accept_returnUrl(BffSetupType setup) { - await Bff.BffLoginAsync("alice", "sid123"); + Bff.OnConfigureEndpoints += endpoints => endpoints.MapGet("/foo", () => "foo'd you"); - var response = await Bff.BrowserClient.GetAsync(Bff.Url("/bff/logout") + "?sid=sid123&returnUrl=/foo"); - response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // endsession - response.Headers.Location!.ToString().ToLowerInvariant().ShouldStartWith(IdentityServer.Url("/connect/endsession")); + ConfigureBff(setup); + await InitializeAsync(); + await Bff.BrowserClient.Login(); - response = await IdentityServer.BrowserClient.GetAsync(response.Headers.Location!.ToString()); - response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // logout - response.Headers.Location!.ToString().ToLowerInvariant().ShouldStartWith(IdentityServer.Url("/account/logout")); + var response = await Bff.BrowserClient.Logout(returnUrl: new Uri("/foo", UriKind.Relative)) + .CheckHttpStatusCode(); - response = await IdentityServer.BrowserClient.GetAsync(response.Headers.Location!.ToString()); - response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // post logout redirect uri - response.Headers.Location!.ToString().ToLowerInvariant().ShouldStartWith(Bff.Url("/signout-callback-oidc")); + response.RequestMessage!.RequestUri.ShouldBe(Bff.Url("/foo")); - response = await Bff.BrowserClient.GetAsync(response.Headers.Location!.ToString()); - response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // root - response.Headers.Location!.ToString().ToLowerInvariant().ShouldBe("/foo"); } - [Fact] - public async Task logout_endpoint_should_reject_non_local_returnUrl() + [Theory] + [MemberData(nameof(AllSetups))] + public async Task logout_endpoint_should_reject_non_local_returnUrl(BffSetupType setup) { - await Bff.BffLoginAsync("alice", "sid123"); + ConfigureBff(setup); + await InitializeAsync(); + await Bff.BrowserClient.Login(); - var problem = await Bff.BrowserClient.GetAsync(Bff.Url("/bff/logout") + "?sid=sid123&returnUrl=https://foo") + var problem = await Bff.BrowserClient.Logout(returnUrl: new Uri("https://foo")) .ShouldBeProblem(); problem.Errors.ShouldContainKey(Constants.RequestParameters.ReturnUrl); diff --git a/bff/test/Bff.Tests/Endpoints/Management/ManagementBasePathTests.cs b/bff/test/Bff.Tests/Endpoints/Management/ManagementBasePathTests.cs index 4dba0f055..8b782fb38 100644 --- a/bff/test/Bff.Tests/Endpoints/Management/ManagementBasePathTests.cs +++ b/bff/test/Bff.Tests/Endpoints/Management/ManagementBasePathTests.cs @@ -3,23 +3,24 @@ using System.Net; using Duende.Bff.Configuration; -using Duende.Bff.Tests.TestHosts; +using Duende.Bff.Tests.TestInfra; using Xunit.Abstractions; namespace Duende.Bff.Tests.Endpoints.Management; -public class ManagementBasePathTests(ITestOutputHelper output) : BffIntegrationTestBase(output) +public class ManagementBasePathTests(ITestOutputHelper output) : BffTestBase(output) { [Theory] - [InlineData(Constants.ManagementEndpoints.Login)] - [InlineData(Constants.ManagementEndpoints.Logout)] + [InlineData(Constants.ManagementEndpoints.Login, HttpStatusCode.Redirect)] + [InlineData(Constants.ManagementEndpoints.Logout, HttpStatusCode.Redirect)] #pragma warning disable CS0618 // Type or member is obsolete - [InlineData(Constants.ManagementEndpoints.SilentLogin)] + [InlineData(Constants.ManagementEndpoints.SilentLogin, HttpStatusCode.Redirect)] #pragma warning restore CS0618 // Type or member is obsolete - [InlineData(Constants.ManagementEndpoints.SilentLoginCallback)] - [InlineData(Constants.ManagementEndpoints.User)] - public async Task custom_ManagementBasePath_should_affect_basepath(string path) + [InlineData(Constants.ManagementEndpoints.SilentLoginCallback, HttpStatusCode.OK)] + [InlineData(Constants.ManagementEndpoints.User, HttpStatusCode.Unauthorized)] + public async Task custom_ManagementBasePath_should_affect_basepath(string path, HttpStatusCode expectedStatusCode) { + SetupDefaultBffAuthentication(); Bff.OnConfigureServices += svcs => { svcs.Configure(options => @@ -27,13 +28,26 @@ public class ManagementBasePathTests(ITestOutputHelper output) : BffIntegrationT options.ManagementBasePath = new PathString("/{path:regex(^[a-zA-Z\\d-]+$)}/bff"); }); }; - await Bff.InitializeAsync(); + await InitializeAsync(); - var req = new HttpRequestMessage(HttpMethod.Get, Bff.Url("/custom/bff" + path)); + // Don't follow the redirects, becuase otherwise we might folow a redirect flow that ends up in a 404 + Bff.BrowserClient.RedirectHandler.AutoFollowRedirects = false; + + + // Make sure the 'original path' doesn't work + await VerifyRoute(path, HttpStatusCode.NotFound); + + // but the custom path does work + await VerifyRoute(path, expectedStatusCode, "/custom/bff"); + } + + private async Task VerifyRoute(string path, HttpStatusCode expectedStatusCode, string? prefix = null) + { + var req = new HttpRequestMessage(HttpMethod.Get, Bff.Url(prefix + path)); req.Headers.Add("x-csrf", "1"); var response = await Bff.BrowserClient.SendAsync(req); + response.StatusCode.ShouldBe(expectedStatusCode); - response.StatusCode.ShouldNotBe(HttpStatusCode.NotFound); } } diff --git a/bff/test/Bff.Tests/Endpoints/Management/UserEndpointTests.cs b/bff/test/Bff.Tests/Endpoints/Management/UserEndpointTests.cs index a99921be4..5fdb84ff2 100644 --- a/bff/test/Bff.Tests/Endpoints/Management/UserEndpointTests.cs +++ b/bff/test/Bff.Tests/Endpoints/Management/UserEndpointTests.cs @@ -4,25 +4,47 @@ using System.Net; using System.Security.Claims; using Duende.Bff.Configuration; -using Duende.Bff.Tests.TestHosts; +using Duende.Bff.Tests.TestInfra; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Options; using Xunit.Abstractions; namespace Duende.Bff.Tests.Endpoints.Management; -public class UserEndpointTests(ITestOutputHelper output) : BffIntegrationTestBase(output) +public class UserEndpointTests : BffTestBase, IAsyncLifetime { + private List ClaimsToAdd = []; + + public UserEndpointTests(ITestOutputHelper output) : base(output) + { + SetupDefaultBffAuthentication(ClaimsToAdd); + + Bff.OnConfigureEndpoints += endpoints => + { + // Setup a login endpoint that allows you to simulate signing in as a specific + // user in the BFF. + endpoints.MapGet("/__signin", async ctx => + { + var props = new AuthenticationProperties(); + await ctx.SignInAsync(UserToSignIn!, props); + + ctx.Response.StatusCode = 204; + }); + }; + } + + public ClaimsPrincipal? UserToSignIn { get; set; } + [Fact] public async Task user_endpoint_for_authenticated_user_should_return_claims() { - await Bff.IssueSessionCookieAsync( - new Claim("sub", "alice"), - new Claim("foo", "foo1"), - new Claim("foo", "foo2")); + ClaimsToAdd.Add(new Claim("foo", "foo1")); + ClaimsToAdd.Add(new Claim("foo", "foo2")); + await Bff.BrowserClient.Login(); - var data = await Bff.CallUserEndpointAsync(); + var data = await Bff.BrowserClient.CallUserEndpointAsync(); - data.Count.ShouldBe(5); - data.First(d => d.Type == "sub").Value.GetString().ShouldBe("alice"); + data.First(d => d.Type == "sub").Value.GetString().ShouldBe(The.Sub); var foos = data.Where(d => d.Type == "foo"); foos.Count().ShouldBe(2); @@ -30,17 +52,20 @@ public class UserEndpointTests(ITestOutputHelper output) : BffIntegrationTestBas foos.Skip(1).First().Value.GetString().ShouldBe("foo2"); data.First(d => d.Type == Constants.ClaimTypes.SessionExpiresIn).Value.GetInt32().ShouldBePositive(); - data.First(d => d.Type == Constants.ClaimTypes.LogoutUrl).Value.GetString().ShouldBe("/bff/logout"); + data.First(d => d.Type == Constants.ClaimTypes.LogoutUrl).Value.GetString().ShouldStartWith("/bff/logout?sid="); } [Fact] public async Task user_endpoint_for_authenticated_user_with_sid_should_return_claims_including_logout() { - await Bff.IssueSessionCookieAsync( + UserToSignIn = new ClaimsPrincipal(new ClaimsIdentity([ new Claim("sub", "alice"), - new Claim("sid", "123")); + new Claim("sid", "123"), + ], "test", "name", "role")); - var data = await Bff.CallUserEndpointAsync(); + await Bff.BrowserClient.GetAsync("/__signin"); + + var data = await Bff.BrowserClient.CallUserEndpointAsync(); data.Count.ShouldBe(4); data.First(d => d.Type == "sub").Value.GetString().ShouldBe("alice"); @@ -52,7 +77,7 @@ public class UserEndpointTests(ITestOutputHelper output) : BffIntegrationTestBas [Fact] public async Task user_endpoint_for_authenticated_user_without_csrf_header_should_fail() { - await Bff.IssueSessionCookieAsync(new Claim("sub", "alice"), new Claim("foo", "foo1"), new Claim("foo", "foo2")); + await Bff.BrowserClient.IssueSessionCookieAsync(new Claim("sub", "alice"), new Claim("foo", "foo1"), new Claim("foo", "foo2")); var req = new HttpRequestMessage(HttpMethod.Get, Bff.Url("/bff/user")); var response = await Bff.BrowserClient.SendAsync(req); @@ -73,9 +98,12 @@ public class UserEndpointTests(ITestOutputHelper output) : BffIntegrationTestBas [Fact] public async Task when_configured_user_endpoint_for_unauthenticated_user_should_return_200_and_empty() { - Bff.BffOptions.AnonymousSessionResponse = AnonymousSessionResponse.Response200; - var data = await Bff.CallUserEndpointAsync(); + var options = Bff.Resolve>(); + + options.Value.AnonymousSessionResponse = AnonymousSessionResponse.Response200; + + var data = await Bff.BrowserClient.CallUserEndpointAsync(); data.ShouldBeEmpty(); } } diff --git a/bff/test/Bff.Tests/Headers/ApiAndBffUseForwardedHeaders.cs b/bff/test/Bff.Tests/Headers/ApiAndBffUseForwardedHeaders.cs index 241552286..4fffd106c 100644 --- a/bff/test/Bff.Tests/Headers/ApiAndBffUseForwardedHeaders.cs +++ b/bff/test/Bff.Tests/Headers/ApiAndBffUseForwardedHeaders.cs @@ -37,7 +37,7 @@ public class ApiAndBffUseForwardedHeaders : BffTestBase, IAsyncLifetime Bff.OnConfigureEndpoints += endpoints => { - endpoints.MapRemoteBffApiEndpoint(The.Path, Api.Url().ToString()); + endpoints.MapRemoteBffApiEndpoint(The.Path, Api.Url()); }; } diff --git a/bff/test/Bff.Tests/Headers/ApiUseForwardedHeaders.cs b/bff/test/Bff.Tests/Headers/ApiUseForwardedHeaders.cs index b900ffe00..1b7fc7caf 100644 --- a/bff/test/Bff.Tests/Headers/ApiUseForwardedHeaders.cs +++ b/bff/test/Bff.Tests/Headers/ApiUseForwardedHeaders.cs @@ -27,7 +27,7 @@ public class ApiUseForwardedHeaders : BffTestBase Bff.OnConfigureEndpoints += endpoints => { - endpoints.MapRemoteBffApiEndpoint(The.Path, Api.Url().ToString()); + endpoints.MapRemoteBffApiEndpoint(The.Path, Api.Url()); }; } diff --git a/bff/test/Bff.Tests/IAccessTokenRetriever_Extensibility_tests.cs b/bff/test/Bff.Tests/IAccessTokenRetriever_Extensibility_tests.cs index 886c84eb6..e5ff01bf0 100644 --- a/bff/test/Bff.Tests/IAccessTokenRetriever_Extensibility_tests.cs +++ b/bff/test/Bff.Tests/IAccessTokenRetriever_Extensibility_tests.cs @@ -3,7 +3,7 @@ using Duende.Bff.AccessTokenManagement; using Duende.Bff.Internal; -using Duende.Bff.Tests.TestHosts; +using Duende.Bff.Tests.TestInfra; using Duende.Bff.Yarp; using Microsoft.Extensions.Logging.Abstractions; using Xunit.Abstractions; @@ -13,26 +13,52 @@ namespace Duende.Bff.Tests; /// /// These tests prove that you can use a custom IAccessTokenRetriever and that the context is populated correctly. /// -public class IAccessTokenRetriever_Extensibility_tests : BffIntegrationTestBase +public class IAccessTokenRetriever_Extensibility_tests : BffTestBase { private ContextCapturingAccessTokenRetriever _customAccessTokenReceiver { get; } = new(NullLogger.Instance); public IAccessTokenRetriever_Extensibility_tests(ITestOutputHelper output) : base(output) { + IdentityServer.AddClient(The.ClientId, Bff.Url()); + Bff.OnConfigureBff += bff => bff + .WithDefaultOpenIdConnectOptions(The.DefaultOpenIdConnectConfiguration) + .AddRemoteApis(); + Bff.OnConfigureServices += services => { services.AddSingleton(_customAccessTokenReceiver); }; + } + [Fact] + public async Task When_calling_custom_endpoint_then_AccessTokenRetrievalContext_has_api_address_and_localpath() + { + Bff.OnConfigureEndpoints += endpoints => + { + endpoints.MapRemoteBffApiEndpoint("/custom", Api.Url("/some/path")) + .WithAccessToken() + .WithAccessTokenRetriever(); + }; + + await InitializeAsync(); + await Bff.BrowserClient.Login(); + + await Bff.BrowserClient.CallBffHostApi(Bff.Url("/custom")); + + var usedContext = _customAccessTokenReceiver.UsedContext.ShouldNotBeNull(); + + usedContext.Metadata.TokenType.ShouldBe(RequiredTokenType.User); + + usedContext.ApiAddress.ShouldBe(Api.Url("/some/path")); + usedContext.LocalPath.ToString().ShouldBe("/custom"); + + } + + [Fact] + public async Task When_calling_sub_custom_endpoint_then_AccessTokenRetrievalContext_has_api_address_and_localpath() + { Bff.OnConfigure += app => { - app.UseEndpoints((endpoints) => - { - endpoints.MapRemoteBffApiEndpoint("/custom", Api.Url("/some/path")) - .WithAccessToken() - .WithAccessTokenRetriever(); - - }); app.Map("/subPath", subPath => @@ -47,34 +73,13 @@ public class IAccessTokenRetriever_Extensibility_tests : BffIntegrationTestBase }); }; - } - - [Fact] - public async Task When_calling_custom_endpoint_then_AccessTokenRetrievalContext_has_api_address_and_localpath() - { - await Bff.BffLoginAsync("alice"); - - await Bff.BrowserClient.CallBffHostApi(Bff.Url("/custom")); - - var usedContext = _customAccessTokenReceiver.UsedContext.ShouldNotBeNull(); - - usedContext.Metadata.TokenType.ShouldBe(RequiredTokenType.User); - - usedContext.ApiAddress.ShouldBe(new Uri(Api.Url("/some/path"))); - usedContext.LocalPath.ToString().ShouldBe("/custom"); - - } - - [Fact] - public async Task When_calling_sub_custom_endpoint_then_AccessTokenRetrievalContext_has_api_address_and_localpath() - { - await Bff.BffLoginAsync("alice"); - + await InitializeAsync(); + await Bff.BrowserClient.Login(); await Bff.BrowserClient.CallBffHostApi(Bff.Url("/subPath/custom_within_subpath")); var usedContext = _customAccessTokenReceiver.UsedContext.ShouldNotBeNull(); - usedContext.ApiAddress.ShouldBe(new Uri(Api.Url("/some/path"))); + usedContext.ApiAddress.ShouldBe(Api.Url("/some/path")); usedContext.LocalPath.ToString().ShouldBe("/custom_within_subpath"); } @@ -94,9 +99,10 @@ public class IAccessTokenRetriever_Extensibility_tests : BffIntegrationTestBase UsedContext = context; if (context.Metadata.TokenType.HasValue) { - return await context.HttpContext.GetManagedAccessToken( + var managedAccessToken = await context.HttpContext.GetManagedAccessToken( requiredTokenType: context.Metadata.TokenType.Value, context.UserTokenRequestParameters, ct: ct); + return managedAccessToken; } else { diff --git a/bff/test/Bff.Tests/PublicApiVerificationTests.VerifyPublicApi_Bff_Yarp.verified.txt b/bff/test/Bff.Tests/PublicApiVerificationTests.VerifyPublicApi_Bff_Yarp.verified.txt index 68b16c09a..98e9709e3 100644 --- a/bff/test/Bff.Tests/PublicApiVerificationTests.VerifyPublicApi_Bff_Yarp.verified.txt +++ b/bff/test/Bff.Tests/PublicApiVerificationTests.VerifyPublicApi_Bff_Yarp.verified.txt @@ -65,7 +65,7 @@ } public static class RouteBuilderExtensions { - public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapRemoteBffApiEndpoint(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, Microsoft.AspNetCore.Http.PathString localPath, string apiAddress, System.Action? yarpTransformBuilder = null) { } + public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapRemoteBffApiEndpoint(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, Microsoft.AspNetCore.Http.PathString localPath, System.Uri apiAddress, System.Action? yarpTransformBuilder = null) { } } public sealed class UserAccessTokenParameters : System.IEquatable { diff --git a/bff/test/Bff.Tests/TestHosts/BffHost.cs b/bff/test/Bff.Tests/TestHosts/BffHost.cs index c8c792654..89f291fc5 100644 --- a/bff/test/Bff.Tests/TestHosts/BffHost.cs +++ b/bff/test/Bff.Tests/TestHosts/BffHost.cs @@ -407,45 +407,45 @@ public class BffHost : GenericHost .RequireAuthorization("AlwaysFail"); endpoints.MapRemoteBffApiEndpoint( - "/api_user", _apiHost.Url()) + "/api_user", new Uri(_apiHost.Url())) .WithAccessToken(); endpoints.MapRemoteBffApiEndpoint( - "/api_user_no_csrf", _apiHost.Url()) + "/api_user_no_csrf", new Uri(_apiHost.Url())) .SkipAntiforgery() .WithAccessToken(); endpoints.MapRemoteBffApiEndpoint( - "/api_client", _apiHost.Url()) + "/api_client", new Uri(_apiHost.Url())) .WithAccessToken(RequiredTokenType.Client); endpoints.MapRemoteBffApiEndpoint( - "/api_user_or_client", _apiHost.Url()) + "/api_user_or_client", new Uri(_apiHost.Url())) .WithAccessToken(RequiredTokenType.UserOrClient); endpoints.MapRemoteBffApiEndpoint( - "/api_unauthenticated", _apiHost.Url() + "return_unauthenticated") + "/api_unauthenticated", new Uri(_apiHost.Url() + "return_unauthenticated")) .WithAccessToken(RequiredTokenType.UserOrClient); endpoints.MapRemoteBffApiEndpoint( - "/api_forbidden", _apiHost.Url() + "return_forbidden") + "/api_forbidden", new Uri(_apiHost.Url() + "return_forbidden")) .WithAccessToken(RequiredTokenType.UserOrClient); #pragma warning disable CS0618 // Type or member is obsolete endpoints.MapRemoteBffApiEndpoint( - "/api_user_or_anon", _apiHost.Url()) + "/api_user_or_anon", new Uri(_apiHost.Url())) .WithOptionalUserAccessToken(); #pragma warning restore CS0618 // Type or member is obsolete endpoints.MapRemoteBffApiEndpoint( - "/api_anon_only", _apiHost.Url()); + "/api_anon_only", new Uri(_apiHost.Url())); // Add a custom transform. This transform just copies the request headers // which allows the tests to see if this custom transform works endpoints.MapRemoteBffApiEndpoint( - "/api_custom_transform", _apiHost.Url(), + "/api_custom_transform", new Uri(_apiHost.Url()), c => { c.CopyRequestHeaders = true; @@ -453,12 +453,12 @@ public class BffHost : GenericHost }); endpoints.MapRemoteBffApiEndpoint( - "/api_with_access_token_retriever", _apiHost.Url()) + "/api_with_access_token_retriever", new Uri(_apiHost.Url())) .WithAccessToken(RequiredTokenType.UserOrClient) .WithAccessTokenRetriever(); endpoints.MapRemoteBffApiEndpoint( - "/api_with_access_token_retrieval_that_fails", _apiHost.Url()) + "/api_with_access_token_retrieval_that_fails", new Uri(_apiHost.Url())) .WithAccessToken(RequiredTokenType.UserOrClient) .WithAccessTokenRetriever(); }); diff --git a/bff/test/Bff.Tests/TestHosts/BffHostUsingResourceNamedTokens.cs b/bff/test/Bff.Tests/TestHosts/BffHostUsingResourceNamedTokens.cs index b1695238d..dc45126e2 100644 --- a/bff/test/Bff.Tests/TestHosts/BffHostUsingResourceNamedTokens.cs +++ b/bff/test/Bff.Tests/TestHosts/BffHostUsingResourceNamedTokens.cs @@ -142,7 +142,7 @@ public class BffHostUsingResourceNamedTokens : GenericHost endpoints.MapBffManagementEndpoints(); endpoints.MapRemoteBffApiEndpoint( - "/api_user_with_useraccesstokenparameters_having_stored_named_token", _apiHost.Url()) + "/api_user_with_useraccesstokenparameters_having_stored_named_token", new Uri(_apiHost.Url())) .WithUserAccessTokenParameter(new BffUserAccessTokenParameters { SignInScheme = Scheme.Parse("cookie"), @@ -152,7 +152,7 @@ public class BffHostUsingResourceNamedTokens : GenericHost .WithAccessToken(); endpoints.MapRemoteBffApiEndpoint( - "/api_user_with_useraccesstokenparameters_having_not_stored_named_token", _apiHost.Url()) + "/api_user_with_useraccesstokenparameters_having_not_stored_named_token", new Uri(_apiHost.Url())) .WithUserAccessTokenParameter(new BffUserAccessTokenParameters { SignInScheme = Scheme.Parse("cookie"), diff --git a/bff/test/Bff.Tests/TestInfra/BffHttpClient.cs b/bff/test/Bff.Tests/TestInfra/BffHttpClient.cs index a1ea45482..d0325fbc0 100644 --- a/bff/test/Bff.Tests/TestInfra/BffHttpClient.cs +++ b/bff/test/Bff.Tests/TestInfra/BffHttpClient.cs @@ -20,7 +20,7 @@ public static class CookieContainerExtensions } } -public class BffHttpClient(RedirectHandler handler, CookieContainer cookies) : HttpClient(handler), IHttpClient +public class BffHttpClient(RedirectHandler handler, CookieContainer cookies, IdentityServerTestHost identityServer) : HttpClient(handler) { public CookieContainer Cookies { get; } = cookies; @@ -30,8 +30,20 @@ public class BffHttpClient(RedirectHandler handler, CookieContainer cookies) : H .CheckHttpStatusCode(expectedStatusCode); - public static BffHttpClient Build(RedirectHandler handler, CookieContainer cookies) => new(handler, cookies); + public async Task> CallUserEndpointAsync() + { + var req = new HttpRequestMessage(HttpMethod.Get, "/bff/user"); + req.Headers.Add("x-csrf", "1"); + + var response = await SendAsync(req); + + response.StatusCode.ShouldBe(HttpStatusCode.OK); + response.Content.Headers.ContentType?.MediaType.ShouldBe("application/json"); + + var json = await response.Content.ReadAsStringAsync(); + return JsonSerializer.Deserialize>(json, TestSerializerOptions.Default) ?? []; + } internal Task CallBffHostApi( PathString path, @@ -115,23 +127,44 @@ public class BffHttpClient(RedirectHandler handler, CookieContainer cookies) : H host.PropsToSignIn.Items.Add("session_id", sid); } - await IssueSessionCookieAsync(host, new Claim("sub", sub)); + await IssueSessionCookieAsync(new Claim("sub", sub)); } - public async Task IssueSessionCookieAsync(IdentityServerTestHost host, params Claim[] claims) + public async Task IssueSessionCookieAsync(params Claim[] claims) { - var previousUser = host.UserToSignIn; + var previousUser = identityServer.UserToSignIn; try { - host.UserToSignIn = new ClaimsPrincipal(new ClaimsIdentity(claims, "test", "name", "role")); - var response = await GetAsync(host.Url("__signin")); + identityServer.UserToSignIn = new ClaimsPrincipal(new ClaimsIdentity(claims, "test", "name", "role")); + var response = await GetAsync(identityServer.Url("__signin")); response.StatusCode.ShouldBe(HttpStatusCode.NoContent); } finally { - host.UserToSignIn = previousUser; + identityServer.UserToSignIn = previousUser; } } public async Task RevokeIdentityServerSession(Uri url) => await GetAsync(new Uri(url, "__signout")).CheckHttpStatusCode(HttpStatusCode.NoContent); + + public async Task Logout(string? sid = null, Uri? returnUrl = null) + { + sid ??= await GetSid(); + + var returnParams = returnUrl == null ? null : $"&returnUrl={Uri.EscapeDataString(returnUrl.ToString())}"; + + var req = new HttpRequestMessage(HttpMethod.Get, "/bff/logout?sid=" + sid + returnParams); + req.Headers.Add("x-csrf", "1"); + return await SendAsync(req); + } + + public async Task GetSid() + { + var claims = await CallUserEndpointAsync(); + + var sidClaim = claims.FirstOrDefault(c => c.Type == "sid")?.Value; + sidClaim.ShouldNotBeNull(); + var sid = sidClaim.Value.ToString(); + return sid; + } } diff --git a/bff/test/Bff.Tests/TestInfra/BffTestBase.cs b/bff/test/Bff.Tests/TestInfra/BffTestBase.cs index 75960b8bd..ab32ec85f 100644 --- a/bff/test/Bff.Tests/TestInfra/BffTestBase.cs +++ b/bff/test/Bff.Tests/TestInfra/BffTestBase.cs @@ -1,6 +1,7 @@ // Copyright (c) Duende Software. All rights reserved. // See LICENSE in the project root for license information. +using System.Security.Claims; using Duende.Bff.DynamicFrontends; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.OpenIdConnect; @@ -39,7 +40,7 @@ public abstract class BffTestBase : IAsyncDisposable Api = new ApiHost(Context, IdentityServer); - Bff = new BffTestHost(Context); + Bff = new BffTestHost(Context, IdentityServer); Cdn = new CdnHost(Context); IdentityServer.AddClient(DefaultOidcClient.ClientId, Bff.Url()); Some = Context.Some; @@ -188,5 +189,28 @@ public abstract class BffTestBase : IAsyncDisposable yield return [BffSetupType.ManuallyConfiguredBff]; } + protected void SetupDefaultBffAuthentication(IEnumerable? claimsToAdd = null) + { + Bff.OnConfigureBff += bff => bff + .WithDefaultOpenIdConnectOptions(opt => + { + if (claimsToAdd != null) + { + opt.Events.OnTokenValidated = context => + { + // Add custom claims to the identity + var identity = (ClaimsIdentity)context.Principal!.Identity!; + foreach (var claim in claimsToAdd) + { + identity.AddClaim(claim); + } + + return Task.CompletedTask; + }; The.DefaultOpenIdConnectConfiguration(opt); + } + }); + + AddOrUpdateFrontend(Some.BffFrontend()); + } } diff --git a/bff/test/Bff.Tests/TestInfra/BffTestHost.cs b/bff/test/Bff.Tests/TestInfra/BffTestHost.cs index 323936209..a2bb6dbd9 100644 --- a/bff/test/Bff.Tests/TestInfra/BffTestHost.cs +++ b/bff/test/Bff.Tests/TestInfra/BffTestHost.cs @@ -1,17 +1,20 @@ // Copyright (c) Duende Software. All rights reserved. // See LICENSE in the project root for license information. +using System.Net; using Duende.Bff.Configuration; using Duende.Bff.DynamicFrontends; using Duende.Bff.DynamicFrontends.Internal; +using Microsoft.Extensions.Options; using Yarp.ReverseProxy.Forwarder; namespace Duende.Bff.Tests.TestInfra; -public class BffTestHost(TestHostContext context) : TestHost(context, new Uri("https://bff")) +public class BffTestHost(TestHostContext context, IdentityServerTestHost identityServer) : TestHost(context, new Uri("https://bff")) { public readonly string DefaultRootResponse = "Default response from root"; private BffHttpClient _browserClient = null!; + public BffOptions BffOptions => Resolve>().Value; /// /// Should a default response for "/" be mapped? @@ -25,7 +28,17 @@ public class BffTestHost(TestHostContext context) : TestHost(context, new Uri("h public override void Initialize() { - BrowserClient = Internet.BuildHttpClient(Url()); + var cookieContainer = new CookieContainer(); + var cookieHandler = new CookieHandler(Internet, cookieContainer); + var redirectHandler = new RedirectHandler(WriteOutput) + { + InnerHandler = cookieHandler + }; + BrowserClient = new BffHttpClient(redirectHandler, cookieContainer, identityServer) + { + BaseAddress = Url() + }; + OnConfigureServices += services => { if (EnableBackChannelHandler) @@ -46,15 +59,6 @@ public class BffTestHost(TestHostContext context) : TestHost(context, new Uri("h OnConfigureBff(builder); }; - OnConfigure += app => - { - app.UseRouting(); - - app.UseAuthentication(); - app.UseAuthorization(); - - app.UseBff(); - }; OnConfigureEndpoints += endpoints => { if (MapGetForRoot) @@ -67,6 +71,17 @@ public class BffTestHost(TestHostContext context) : TestHost(context, new Uri("h }; } + protected override void ConfigureApp(IApplicationBuilder app) + { + app.UseRouting(); + + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseBff(); + base.ConfigureApp(app); + } + public BffHttpClient BrowserClient { get => _browserClient ?? throw new InvalidOperationException("Not yet initialized"); diff --git a/bff/test/Bff.Tests/TestInfra/IdentityServerTestHost.cs b/bff/test/Bff.Tests/TestInfra/IdentityServerTestHost.cs index e6c91e822..6d00d3c10 100644 --- a/bff/test/Bff.Tests/TestInfra/IdentityServerTestHost.cs +++ b/bff/test/Bff.Tests/TestInfra/IdentityServerTestHost.cs @@ -142,8 +142,7 @@ public class IdentityServerTestHost : TestHost ClientSecrets = { new Secret(clientSecret.Sha256()) }, AllowedGrantTypes = GrantTypes.CodeAndClientCredentials, RedirectUris = [redirectUri.ToString()], - PostLogoutRedirectUris = ["/"], // not implemented - BackChannelLogoutUri = "/", // not implemented + PostLogoutRedirectUris = [new Uri(baseUri, "signout-callback-oidc").ToString()], AllowOfflineAccess = true, AllowedScopes = options.Scope.Any() ? options.Scope diff --git a/bff/test/Bff.Tests/TestInfra/SimulatedInternet.cs b/bff/test/Bff.Tests/TestInfra/SimulatedInternet.cs index 3dbdd7ded..60208c574 100644 --- a/bff/test/Bff.Tests/TestInfra/SimulatedInternet.cs +++ b/bff/test/Bff.Tests/TestInfra/SimulatedInternet.cs @@ -33,11 +33,11 @@ public class SimulatedInternet : DelegatingHandler public T BuildHttpClient(Uri baseUrl) where T : HttpClient, IHttpClient { - var handler = new RedirectHandler(_outputWriter); + var recirectHandler = new RedirectHandler(_outputWriter); var cookieContainer = new CookieContainer(); - handler.InnerHandler = new CookieHandler(this, cookieContainer); + recirectHandler.InnerHandler = new CookieHandler(this, cookieContainer); - var client = T.Build(handler, cookieContainer); + var client = T.Build(recirectHandler, cookieContainer); client.BaseAddress = baseUrl; return client; diff --git a/bff/test/Bff.Tests/TestInfra/TestData.cs b/bff/test/Bff.Tests/TestInfra/TestData.cs index 6f77debec..8f2bbe4b2 100644 --- a/bff/test/Bff.Tests/TestInfra/TestData.cs +++ b/bff/test/Bff.Tests/TestInfra/TestData.cs @@ -88,6 +88,7 @@ public class TestData opt.Scope.Add("openid"); opt.Scope.Add("profile"); opt.Scope.Add(Scope); + opt.SignedOutRedirectUri = "/"; }; DefaultOpenIdConnectConfiguration(OpenIdConnectOptions); diff --git a/bff/test/Bff.Tests/TestInfra/TestHost.cs b/bff/test/Bff.Tests/TestInfra/TestHost.cs index 41132f55d..910f0e328 100644 --- a/bff/test/Bff.Tests/TestInfra/TestHost.cs +++ b/bff/test/Bff.Tests/TestInfra/TestHost.cs @@ -7,15 +7,14 @@ namespace Duende.Bff.Tests.TestInfra; public class TestHost(TestHostContext context, Uri baseAddress) : IAsyncDisposable { - public TestHost(TestHostContext context) : this(context, new("https://server")) - { - } internal TestDataBuilder Some => context.Some; public TestData The => context.The; protected SimulatedInternet Internet => context.Internet; + protected void WriteOutput(string output) => context.WriteOutput(output); + IServiceProvider? _appServices = null!; public TestServer Server { get; private set; } = null!; diff --git a/start-influxdb-grafana.ps1 b/start-influxdb-grafana.ps1 new file mode 100644 index 000000000..e69de29bb