diff --git a/bff/docs/upgrade-guide.md b/bff/docs/upgrade-guide.md new file mode 100644 index 000000000..c17fd3168 --- /dev/null +++ b/bff/docs/upgrade-guide.md @@ -0,0 +1,101 @@ +# Upgrade guide + +## From v2.x => v3.x + +If you rely on the default extension methods for wiring up the BFF, then V3 should be a drop-in replacement. + +### Migrating from custom implementations of IHttpMessageInvokerFactory + +In Duende.BFF V2, there was an interface called IHttpMessageInvokerFactory. This class was responsible for creating +and wiring up yarp's HttpMessageInvoker. This interface has been removed in favor yarp's IForwarderHttpClientFactory. + +One common scenario for creating a custom implementation of this class was for mocking the http client +during unit testing. + +If you wish to inject a http handler for unit testing, you should now inject a custom IForwarderHttpClientFactory. For example: + +``` c# + // A Forwarder factory that forwards the messages to a message handler (which can be easily retrieved from a testhost) + public class BackChannelHttpMessageInvokerFactory(HttpMessageHandler backChannel) + : IForwarderHttpClientFactory + { + public HttpMessageInvoker CreateClient(ForwarderHttpClientContext context) => + new HttpMessageInvoker(backChannel); + } + + // Wire up the forwarder in your application's test host: + services.AddSingleton( + new BackChannelHttpMessageInvokerFactory(_apiHost.Server.CreateHandler())); + + +``` + +### Migrating from custom implementations IHttpTransformerFactory +The *IHttpTransformerFactory* was a way to globally configure the YARP tranform pipeline. In V3, the way that +the default *endpoints.MapRemoteBffApiEndpoint()* method builds up the YARP transform has been simplified +significantly. Most of the logic has been pushed down to the *AccessTokenRequestTransform*. + +Here are common scenario's for implementing your own *IHttpTransformerFactory* and how to upgrade: + +**Replacing defaults** + +If you used a custom implementation of IHttpTransformerFactory to change the default behavior of *MapRemoteBffApiEndpoint()*, +for example to add additional transforms, then you can now inject a custom delegate into the di container: + +``` +services.AddSingleton(CustomDefaultYarpTransforms); + +//... + +// This is an example of how to add a response header to ALL invocations of MapRemoteBffApiEndpoint() +private void CustomDefaultBffTransformBuilder(string localpath, TransformBuilderContext context) +{ + context.AddResponseHeader("added-by-custom-default-transform", "some-value"); + DefaultBffYarpTransformerBuilders.DirectProxyWithAccessToken(localpath, context); +} +``` + +Another way of doing this is to create a custom extensionmethod *MyCustomMapRemoteBffApiEndpoint()* that wraps +the MapRemoteBffApiEndpoint() and use that everywhere in your application. This is a great way to add other defaults +that should apply to all endpoints, such as requiring a specific type of access token. + +**Configuring transforms for a single route** +Another common usecase for overriding the IHttpTransformerFactory was to have a custom transform for a single route, by +applying a switch statement and testing for specific routes. + +Now, there is an overload on the *endpoints.MapRemoteBffApiEndpoint()* that allows you to configure the pipeline directly: + +``` c# + + endpoints.MapRemoteBffApiEndpoint( + "/local-path", + _apiHost.Url(), + context => + { + // do something custom: IE: copy request headers + context.CopyRequestHeaders = true; + + // wire up the default transformer logic + DefaultTransformers.DirectProxyWithAccessToken("/local-path", context); + }) + // Continue with normal BFF configuration, for example, allowing optional user access tokens + .WithOptionalUserAccessToken(); + +``` + +### Removed method RemoteApiEndpoint.Map(localpath, apiAddress). +The Map method was no longer needed as most of the logic had been moved to either the MapRemoteBffApiEndpoint and the DefaultTransformers. The map method also wasn't very explicit about what it did and a number of test scenario's tried to verify if it wasn't called wrongly. You are now expected to call the method MapRemoteBffApiEndpoint. This method now has a nullable parameter that allows you to inject your own transformers. + +### AccessTokenRetrievalContext properties are now typed +The LocalPath and ApiAddress properties are now typed. They used to be strings. If you rely on these, for example for implementing +a custom IAccessTokenRetriever, then you should adjust their usage accordingly. + + /// + /// The locally requested path. + /// + public required PathString LocalPath { get; set; } + + /// + /// The remote address of the API. + /// + public required Uri ApiAddress { get; set; } diff --git a/bff/samples/Hosts.Tests/TestInfra/AppHostFixture.cs b/bff/samples/Hosts.Tests/TestInfra/AppHostFixture.cs index 81fbe56d8..bbc2dbac8 100644 --- a/bff/samples/Hosts.Tests/TestInfra/AppHostFixture.cs +++ b/bff/samples/Hosts.Tests/TestInfra/AppHostFixture.cs @@ -92,7 +92,8 @@ await resourceNotificationService.WaitForResourceAsync( .WaitAsync(TimeSpan.FromSeconds(30)); } - +#else + _app = null!; #endif //#DEBUG_NCRUNCH } diff --git a/bff/samples/Hosts.Tests/TestInfra/BffClient.cs b/bff/samples/Hosts.Tests/TestInfra/BffClient.cs index 02905066c..2136e2eba 100644 --- a/bff/samples/Hosts.Tests/TestInfra/BffClient.cs +++ b/bff/samples/Hosts.Tests/TestInfra/BffClient.cs @@ -1,5 +1,5 @@ -// // Copyright (c) Duende Software. All rights reserved. -// // See LICENSE in the project root for license information. +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. using System.Text.Json; using AngleSharp; diff --git a/bff/samples/Hosts.Tests/TestInfra/CloningHttpMessageHandler.cs b/bff/samples/Hosts.Tests/TestInfra/CloningHttpMessageHandler.cs index a23db06a4..d72b507d9 100644 --- a/bff/samples/Hosts.Tests/TestInfra/CloningHttpMessageHandler.cs +++ b/bff/samples/Hosts.Tests/TestInfra/CloningHttpMessageHandler.cs @@ -1,5 +1,5 @@ -// // Copyright (c) Duende Software. All rights reserved. -// // See LICENSE in the project root for license information. +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. namespace Hosts.Tests.TestInfra; diff --git a/bff/samples/IdentityServer/Extensions.cs b/bff/samples/IdentityServer/Extensions.cs index 582d6df44..c5c0f5947 100644 --- a/bff/samples/IdentityServer/Extensions.cs +++ b/bff/samples/IdentityServer/Extensions.cs @@ -1,5 +1,5 @@ -// // Copyright (c) Duende Software. All rights reserved. -// // See LICENSE in the project root for license information. +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. using IdentityServerHost; using Serilog; diff --git a/bff/samples/IdentityServer/ServiceDiscoveringClientStore.cs b/bff/samples/IdentityServer/ServiceDiscoveringClientStore.cs index 928c41213..22035d977 100644 --- a/bff/samples/IdentityServer/ServiceDiscoveringClientStore.cs +++ b/bff/samples/IdentityServer/ServiceDiscoveringClientStore.cs @@ -1,5 +1,5 @@ -// // Copyright (c) Duende Software. All rights reserved. -// // See LICENSE in the project root for license information. +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. using Duende.IdentityModel; using Duende.IdentityServer.Models; diff --git a/bff/src/Duende.Bff.Blazor.Client/Duende.Bff.Blazor.Client.csproj b/bff/src/Duende.Bff.Blazor.Client/Duende.Bff.Blazor.Client.csproj index 83be62b93..d575ef270 100644 --- a/bff/src/Duende.Bff.Blazor.Client/Duende.Bff.Blazor.Client.csproj +++ b/bff/src/Duende.Bff.Blazor.Client/Duende.Bff.Blazor.Client.csproj @@ -1,9 +1,10 @@ - + net8.0 enable enable + true diff --git a/bff/src/Duende.Bff.Blazor/Duende.Bff.Blazor.csproj b/bff/src/Duende.Bff.Blazor/Duende.Bff.Blazor.csproj index dbe67c50a..642a1d7af 100644 --- a/bff/src/Duende.Bff.Blazor/Duende.Bff.Blazor.csproj +++ b/bff/src/Duende.Bff.Blazor/Duende.Bff.Blazor.csproj @@ -1,9 +1,11 @@ - + net8.0 enable enable + true + diff --git a/bff/src/Duende.Bff.EntityFramework/Duende.Bff.EntityFramework.csproj b/bff/src/Duende.Bff.EntityFramework/Duende.Bff.EntityFramework.csproj index b516a3bcd..658745722 100644 --- a/bff/src/Duende.Bff.EntityFramework/Duende.Bff.EntityFramework.csproj +++ b/bff/src/Duende.Bff.EntityFramework/Duende.Bff.EntityFramework.csproj @@ -1,4 +1,4 @@ - + net8.0;net9.0 diff --git a/bff/src/Duende.Bff.Yarp/AccessTokenRequestTransform.cs b/bff/src/Duende.Bff.Yarp/AccessTokenRequestTransform.cs index 1cc8c4dd5..6b3ae7543 100644 --- a/bff/src/Duende.Bff.Yarp/AccessTokenRequestTransform.cs +++ b/bff/src/Duende.Bff.Yarp/AccessTokenRequestTransform.cs @@ -2,12 +2,18 @@ // See LICENSE in the project root for license information. using System; +using System.Collections.Generic; using System.Net.Http.Headers; using System.Threading.Tasks; using Duende.AccessTokenManagement; +using Duende.AccessTokenManagement.OpenIdConnect; using Duende.Bff.Logging; using Duende.IdentityModel; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Yarp.ReverseProxy.Model; using Yarp.ReverseProxy.Transforms; namespace Duende.Bff.Yarp; @@ -15,40 +21,53 @@ namespace Duende.Bff.Yarp; /// /// Adds an access token to outgoing requests /// -public class AccessTokenRequestTransform : RequestTransform +public class AccessTokenRequestTransform( + IDPoPProofService proofService, + ILogger logger) : RequestTransform { - private readonly IDPoPProofService _dPoPProofService; - private readonly ILogger _logger; - private readonly AccessTokenResult _token; - private readonly string? _routeId; - private readonly TokenType? _tokenType; - - /// - /// ctor - /// - /// - /// - /// - /// - /// - public AccessTokenRequestTransform( - IDPoPProofService proofService, - ILogger logger, - AccessTokenResult accessToken, - string? routeId = null, - TokenType? tokenType = null) - { - _dPoPProofService = proofService; - _logger = logger; - _token = accessToken ?? throw new ArgumentNullException(nameof(accessToken)); - _routeId = routeId; - _tokenType = tokenType; - } /// public override async ValueTask ApplyAsync(RequestTransformContext context) { - switch (_token) + var endpoint = context.HttpContext.GetEndpoint(); + if (endpoint == null) + { + throw new InvalidOperationException("endpoint not found"); + } + UserTokenRequestParameters? userAccessTokenParameters = null; + + context.HttpContext.RequestServices.CheckLicense(); + + // Get the metadata + var metadata = + // Either from the endpoint directly, when using mapbff + endpoint.Metadata.GetMetadata() + // or from yarp + ?? GetBffMetadataFromYarp(endpoint) + ?? throw new InvalidOperationException("API endpoint is missing BFF metadata"); + + if (metadata.BffUserAccessTokenParameters != null) + { + userAccessTokenParameters = metadata.BffUserAccessTokenParameters.ToUserAccessTokenRequestParameters(); + } + + if (context.HttpContext.RequestServices.GetRequiredService(metadata.AccessTokenRetriever) + is not IAccessTokenRetriever accessTokenRetriever) + { + throw new InvalidOperationException("TokenRetriever is not an IAccessTokenRetriever"); + } + + var accessTokenContext = new AccessTokenRetrievalContext() + { + HttpContext = context.HttpContext, + Metadata = metadata, + UserTokenRequestParameters = userAccessTokenParameters, + ApiAddress = new Uri(context.DestinationPrefix), + LocalPath = context.HttpContext.Request.Path + }; + var result = await accessTokenRetriever.GetAccessToken(accessTokenContext); + + switch (result) { case BearerTokenResult bearerToken: ApplyBearerToken(context, bearerToken); @@ -57,7 +76,7 @@ public override async ValueTask ApplyAsync(RequestTransformContext context) await ApplyDPoPToken(context, dpopToken); break; case AccessTokenRetrievalError tokenError: - ApplyError(context, tokenError, _routeId ?? "Unknown Route", _tokenType); + ApplyError(context, tokenError, metadata.RequiredTokenType); break; case NoAccessTokenResult noToken: break; @@ -66,12 +85,31 @@ public override async ValueTask ApplyAsync(RequestTransformContext context) } } - private void ApplyError(RequestTransformContext context, AccessTokenRetrievalError tokenError, string routeId, TokenType? tokenType) + private static BffRemoteApiEndpointMetadata? GetBffMetadataFromYarp(Endpoint endpoint) + { + var yarp = endpoint.Metadata.GetMetadata(); + if (yarp == null) + return null; + + TokenType? requiredTokenType = null; + if (Enum.TryParse(yarp.Config?.Metadata?.GetValueOrDefault(Constants.Yarp.TokenTypeMetadata), true, out var type)) + { + requiredTokenType = type; + } + + return new BffRemoteApiEndpointMetadata() + { + OptionalUserToken = yarp.Config?.Metadata?.GetValueOrDefault(Constants.Yarp.OptionalUserTokenMetadata) == "true", + RequiredTokenType = requiredTokenType + }; + } + + private void ApplyError(RequestTransformContext context, AccessTokenRetrievalError tokenError, TokenType? tokenType) { // short circuit forwarder and return 401 context.HttpContext.Response.StatusCode = 401; - _logger.AccessTokenMissing(tokenType?.ToString() ?? "Unknown token type", routeId, tokenError.Error); + logger.AccessTokenMissing(tokenType?.ToString() ?? "Unknown token type", context.HttpContext.Request.Path, tokenError.Error); } private void ApplyBearerToken(RequestTransformContext context, BearerTokenResult token) @@ -85,7 +123,7 @@ private async Task ApplyDPoPToken(RequestTransformContext context, DPoPTokenResu ArgumentNullException.ThrowIfNull(token.DPoPJsonWebKey, nameof(token.DPoPJsonWebKey)); var baseUri = new Uri(context.DestinationPrefix); - var proofToken = await _dPoPProofService.CreateProofTokenAsync(new DPoPProofRequest + var proofToken = await proofService.CreateProofTokenAsync(new DPoPProofRequest { AccessToken = token.AccessToken, DPoPJsonWebKey = token.DPoPJsonWebKey, diff --git a/bff/src/Duende.Bff.Yarp/AccessTokenTransformProvider.cs b/bff/src/Duende.Bff.Yarp/AccessTokenTransformProvider.cs index 30f310899..463b30c7c 100644 --- a/bff/src/Duende.Bff.Yarp/AccessTokenTransformProvider.cs +++ b/bff/src/Duende.Bff.Yarp/AccessTokenTransformProvider.cs @@ -1,5 +1,5 @@ -// // Copyright (c) Duende Software. All rights reserved. -// // See LICENSE in the project root for license information. +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. using System; using System.Collections.Generic; @@ -79,8 +79,6 @@ private static bool GetMetadataValue(TransformBuilderContext transformBuildConte /// public void Apply(TransformBuilderContext transformBuildContext) { - TokenType tokenType; - bool optional; if(GetMetadataValue(transformBuildContext, Constants.Yarp.OptionalUserTokenMetadata, out var optionalTokenMetadata)) { if (GetMetadataValue(transformBuildContext, Constants.Yarp.TokenTypeMetadata, out var tokenTypeMetadata)) @@ -94,13 +92,10 @@ public void Apply(TransformBuilderContext transformBuildContext) }); return; } - optional = true; - tokenType = TokenType.User; } else if (GetMetadataValue(transformBuildContext, Constants.Yarp.TokenTypeMetadata, out var tokenTypeMetadata)) { - optional = false; - if (!TokenType.TryParse(tokenTypeMetadata, true, out tokenType)) + if (!Enum.TryParse(tokenTypeMetadata, true, out _)) { throw new ArgumentException("Invalid value for Duende.Bff.Yarp.TokenType metadata"); } @@ -114,13 +109,9 @@ public void Apply(TransformBuilderContext transformBuildContext) { transformContext.HttpContext.CheckForBffMiddleware(_options); - var token = await transformContext.HttpContext.GetManagedAccessToken(tokenType, optional); - var accessTokenTransform = new AccessTokenRequestTransform( _dPoPProofService, - _loggerFactory.CreateLogger(), - token, - transformBuildContext?.Route?.RouteId, tokenType); + _loggerFactory.CreateLogger()); await accessTokenTransform.ApplyAsync(transformContext); }); diff --git a/bff/src/Duende.Bff.Yarp/ActivityPropagationHandler.cs b/bff/src/Duende.Bff.Yarp/ActivityPropagationHandler.cs index 64f67ccd1..8dc4e849e 100644 --- a/bff/src/Duende.Bff.Yarp/ActivityPropagationHandler.cs +++ b/bff/src/Duende.Bff.Yarp/ActivityPropagationHandler.cs @@ -1,5 +1,5 @@ -// // Copyright (c) Duende Software. All rights reserved. -// // See LICENSE in the project root for license information. +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. using System; using System.Diagnostics; diff --git a/bff/src/Duende.Bff.Yarp/AntiforgeryMiddleware.cs b/bff/src/Duende.Bff.Yarp/AntiforgeryMiddleware.cs index 826a7c8e7..b1917907b 100644 --- a/bff/src/Duende.Bff.Yarp/AntiforgeryMiddleware.cs +++ b/bff/src/Duende.Bff.Yarp/AntiforgeryMiddleware.cs @@ -1,5 +1,5 @@ -// // Copyright (c) Duende Software. All rights reserved. -// // See LICENSE in the project root for license information. +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. using System; using System.Threading.Tasks; diff --git a/bff/src/Duende.Bff.Yarp/BffBuilderExtensions.cs b/bff/src/Duende.Bff.Yarp/BffBuilderExtensions.cs index e00abe7ff..efecc49cf 100644 --- a/bff/src/Duende.Bff.Yarp/BffBuilderExtensions.cs +++ b/bff/src/Duende.Bff.Yarp/BffBuilderExtensions.cs @@ -1,5 +1,5 @@ -// // Copyright (c) Duende Software. All rights reserved. -// // See LICENSE in the project root for license information. +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; @@ -20,38 +20,7 @@ public static class BffBuilderExtensions public static BffBuilder AddRemoteApis(this BffBuilder builder) { builder.Services.AddHttpForwarder(); - - builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(); - return builder; } - /// - /// Adds a custom HttpMessageInvokerFactory to DI - /// - /// - /// - /// - public static BffBuilder AddHttpMessageInvokerFactory(this BffBuilder builder) - where T : class, IHttpMessageInvokerFactory - { - builder.Services.AddTransient(); - - return builder; - } - - /// - /// Adds a custom HttpTransformerFactory to DI - /// - /// - /// - /// - public static BffBuilder AddHttpTransformerFactory(this BffBuilder builder) - where T : class, IHttpTransformerFactory - { - builder.Services.AddTransient(); - - return builder; - } } \ No newline at end of file diff --git a/bff/src/Duende.Bff.Yarp/BffYarpEndpointRouteBuilderExtensions.cs b/bff/src/Duende.Bff.Yarp/BffYarpEndpointRouteBuilderExtensions.cs index d5c32a3f7..7b6078053 100644 --- a/bff/src/Duende.Bff.Yarp/BffYarpEndpointRouteBuilderExtensions.cs +++ b/bff/src/Duende.Bff.Yarp/BffYarpEndpointRouteBuilderExtensions.cs @@ -4,7 +4,10 @@ using Duende.Bff; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; +using System; using Duende.Bff.Yarp; +using Microsoft.Extensions.DependencyInjection; +using Yarp.ReverseProxy.Transforms.Builder; namespace Microsoft.AspNetCore.Builder; @@ -19,17 +22,37 @@ public static class BffYarpEndpointRouteBuilderExtensions /// /// /// + /// /// public static IEndpointConventionBuilder MapRemoteBffApiEndpoint( this IEndpointRouteBuilder endpoints, - PathString localPath, - string apiAddress) + PathString localPath, + string apiAddress, + Action? yarpTransformBuilder = null) { endpoints.CheckLicense(); - - return endpoints.Map( - localPath.Add("/{**catch-all}").Value!, - RemoteApiEndpoint.Map(localPath, apiAddress)) + + // Configure the yarp transform pipeline. Either use the one provided or the default + yarpTransformBuilder ??= context => + { + // For the default, either get one from DI (to globally configure a default) + var defaultYarpTransformBuilder = context.Services.GetService() + // or use the built-in default + ?? DefaultBffYarpTransformerBuilders.DirectProxyWithAccessToken; + + // invoke the default transform builder + defaultYarpTransformBuilder(localPath, context); + }; + + return endpoints.MapForwarder( + pattern: localPath.Add("/{**catch-all}").Value!, + destinationPrefix: apiAddress, + configureTransform: context => + { + yarpTransformBuilder(context); + }) .WithMetadata(new BffRemoteApiEndpointMetadata()); } + + } \ No newline at end of file diff --git a/bff/src/Duende.Bff.Yarp/DefaultBffYarpTransformerBuilders.cs b/bff/src/Duende.Bff.Yarp/DefaultBffYarpTransformerBuilders.cs new file mode 100644 index 000000000..92589fe52 --- /dev/null +++ b/bff/src/Duende.Bff.Yarp/DefaultBffYarpTransformerBuilders.cs @@ -0,0 +1,33 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + +using Microsoft.AspNetCore.Builder; +using Yarp.ReverseProxy.Transforms; +using Yarp.ReverseProxy.Transforms.Builder; + +namespace Duende.Bff.Yarp; + +/// +/// Contains the default transformer logic for YARP BFF endpoints. +/// +public static class DefaultBffYarpTransformerBuilders +{ + /// + /// Build a default 'direct proxy' transformer. This removes the 'cookie' header, removes the local path prefix, + /// and adds an access token to the request. The type of access token is determined by the . + /// + public static BffYarpTransformBuilder DirectProxyWithAccessToken = + (string localPath, TransformBuilderContext context) => + { + context.AddRequestHeaderRemove("Cookie"); + context.AddPathRemovePrefix(localPath); + context.AddBffAccessToken(localPath); + }; +} + +/// +/// Delegate for pipeline transformers. +/// +/// The local path that should be proxied. This path will be removed from the proxied request. +/// The transform builder context +public delegate void BffYarpTransformBuilder(string localPath, TransformBuilderContext context); \ No newline at end of file diff --git a/bff/src/Duende.Bff.Yarp/DefaultHttpMessageInvokerFactory.cs b/bff/src/Duende.Bff.Yarp/DefaultHttpMessageInvokerFactory.cs deleted file mode 100644 index af8732f96..000000000 --- a/bff/src/Duende.Bff.Yarp/DefaultHttpMessageInvokerFactory.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Duende Software. All rights reserved. -// See LICENSE in the project root for license information. - -using System.Collections.Concurrent; -using System.Net; -using System.Net.Http; - -namespace Duende.Bff.Yarp; - -/// -/// Default implementation of the message invoker factory. -/// This implementation creates one message invoker per remote API endpoint. -/// -public class DefaultHttpMessageInvokerFactory : IHttpMessageInvokerFactory -{ - /// - /// Dictionary to cache invoker instances - /// - protected readonly ConcurrentDictionary Clients = new(); - - /// - public virtual HttpMessageInvoker CreateClient(string localPath) - { - return Clients.GetOrAdd(localPath, (key) => - { - var handler = CreateHandler(key); - return new HttpMessageInvoker(handler); - }); - } - - /// - /// Creates the HTTP message handler - /// - /// - /// - protected virtual HttpMessageHandler CreateHandler(string localPath) - { - return new SocketsHttpHandler - { - UseProxy = false, - AllowAutoRedirect = false, - AutomaticDecompression = DecompressionMethods.None, - UseCookies = false - }; - } -} diff --git a/bff/src/Duende.Bff.Yarp/DefaultHttpTransformerFactory.cs b/bff/src/Duende.Bff.Yarp/DefaultHttpTransformerFactory.cs deleted file mode 100644 index abc01a557..000000000 --- a/bff/src/Duende.Bff.Yarp/DefaultHttpTransformerFactory.cs +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) Duende Software. All rights reserved. -// See LICENSE in the project root for license information. - -using Duende.AccessTokenManagement; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Yarp.ReverseProxy.Forwarder; -using Yarp.ReverseProxy.Transforms; -using Yarp.ReverseProxy.Transforms.Builder; - -namespace Duende.Bff.Yarp; - -/// -/// Default HTTP transformer implementation -/// -public class DefaultHttpTransformerFactory : IHttpTransformerFactory -{ - /// - /// The options - /// - protected readonly BffOptions Options; - - /// - /// The YARP transform builder - /// - protected readonly ITransformBuilder TransformBuilder; - - /// - /// The DPoP Proof service - /// - protected readonly IDPoPProofService ProofService; - - /// - /// The logger factory - /// - protected readonly ILoggerFactory LoggerFactory; - - /// - /// ctor - /// - /// The BFF options - /// The YARP transform builder - /// - /// - public DefaultHttpTransformerFactory( - IOptions options, - ITransformBuilder transformBuilder, - IDPoPProofService proofService, - ILoggerFactory loggerFactory) - { - Options = options.Value; - TransformBuilder = transformBuilder; - ProofService = proofService; - LoggerFactory = loggerFactory; - } - - /// - public virtual HttpTransformer CreateTransformer(string localPath, AccessTokenResult accessToken) - { - return TransformBuilder.Create(context => - { - // apply default YARP logic for forwarding headers - context.CopyRequestHeaders = true; - - // use YARP default logic for x-forwarded headers - context.UseDefaultForwarders = true; - - // always remove cookie header since this contains the session - context.RequestTransforms.Add(new RequestHeaderRemoveTransform("Cookie")); - - // transform path to remove prefix - context.RequestTransforms.Add(new PathStringTransform(PathStringTransform.PathTransformMode.RemovePrefix, localPath)); - - // add the access token - context.RequestTransforms.Add(new AccessTokenRequestTransform( - ProofService, - LoggerFactory.CreateLogger(), - accessToken)); - }); - } -} \ No newline at end of file diff --git a/bff/src/Duende.Bff.Yarp/IHttpMessageInvokerFactory.cs b/bff/src/Duende.Bff.Yarp/IHttpMessageInvokerFactory.cs deleted file mode 100644 index 0e9983610..000000000 --- a/bff/src/Duende.Bff.Yarp/IHttpMessageInvokerFactory.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Duende Software. All rights reserved. -// See LICENSE in the project root for license information. - -using System.Net.Http; - -namespace Duende.Bff.Yarp; - -/// -/// Factory for creating a HTTP message invoker for outgoing remote BFF API calls -/// -public interface IHttpMessageInvokerFactory -{ - /// - /// Creates a message invoker based on the local path - /// - /// Local path the remote API is mapped to - /// - HttpMessageInvoker CreateClient(string localPath); -} \ No newline at end of file diff --git a/bff/src/Duende.Bff.Yarp/IHttpTransformerFactory.cs b/bff/src/Duende.Bff.Yarp/IHttpTransformerFactory.cs deleted file mode 100644 index 13335edca..000000000 --- a/bff/src/Duende.Bff.Yarp/IHttpTransformerFactory.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Duende Software. All rights reserved. -// See LICENSE in the project root for license information. - -using Yarp.ReverseProxy.Forwarder; - -namespace Duende.Bff.Yarp; - -/// -/// Factory for creating a HTTP transformer for outgoing remote BFF API calls -/// -public interface IHttpTransformerFactory -{ - /// - /// Creates a HTTP transformer based on the local path - /// - /// Local path the remote API is mapped to - /// The access token to attach to the request (if present) - /// - HttpTransformer CreateTransformer(string localPath, AccessTokenResult accessToken); -} \ No newline at end of file diff --git a/bff/src/Duende.Bff.Yarp/ProxyAppBuilderExtensions.cs b/bff/src/Duende.Bff.Yarp/ProxyAppBuilderExtensions.cs index c3af1e151..a58224f0b 100644 --- a/bff/src/Duende.Bff.Yarp/ProxyAppBuilderExtensions.cs +++ b/bff/src/Duende.Bff.Yarp/ProxyAppBuilderExtensions.cs @@ -1,5 +1,5 @@ -// // Copyright (c) Duende Software. All rights reserved. -// // See LICENSE in the project root for license information. +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. using Microsoft.AspNetCore.Builder; diff --git a/bff/src/Duende.Bff.Yarp/ProxyConfigExtensions.cs b/bff/src/Duende.Bff.Yarp/ProxyConfigExtensions.cs index 4373e4fba..a62979857 100644 --- a/bff/src/Duende.Bff.Yarp/ProxyConfigExtensions.cs +++ b/bff/src/Duende.Bff.Yarp/ProxyConfigExtensions.cs @@ -1,5 +1,5 @@ -// // Copyright (c) Duende Software. All rights reserved. -// // See LICENSE in the project root for license information. +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. using System.Collections.Generic; using Yarp.ReverseProxy.Configuration; diff --git a/bff/src/Duende.Bff.Yarp/RemoteApiEndpoint.cs b/bff/src/Duende.Bff.Yarp/RemoteApiEndpoint.cs deleted file mode 100644 index 059d22c09..000000000 --- a/bff/src/Duende.Bff.Yarp/RemoteApiEndpoint.cs +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) Duende Software. All rights reserved. -// See LICENSE in the project root for license information. - -using System; -using Duende.AccessTokenManagement.OpenIdConnect; -using Duende.Bff.Logging; -using Duende.Bff.Yarp.Logging; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Yarp.ReverseProxy.Forwarder; - -namespace Duende.Bff.Yarp; - -/// -/// Remote BFF API endpoint -/// -public static class RemoteApiEndpoint -{ - /// - /// Endpoint logic - /// - /// The local path (e.g. /api) - /// The remote address (e.g. https://api.myapp.com/foo) - /// - /// - public static RequestDelegate Map(string localPath, string apiAddress) - { - return async context => - { - var loggerFactory = context.RequestServices.GetRequiredService(); - var logger = loggerFactory.CreateLogger(LogCategories.RemoteApiEndpoints); - - var endpoint = context.GetEndpoint(); - if (endpoint == null) - { - throw new InvalidOperationException("endpoint not found"); - } - - var metadata = endpoint.Metadata.GetMetadata(); - if (metadata == null) - { - throw new InvalidOperationException("API endpoint is missing BFF metadata"); - } - - UserTokenRequestParameters? userAccessTokenParameters = null; - - if (metadata.BffUserAccessTokenParameters != null) - { - userAccessTokenParameters = metadata.BffUserAccessTokenParameters.ToUserAccessTokenRequestParameters(); - } - - if (context.RequestServices.GetRequiredService(metadata.AccessTokenRetriever) - is not IAccessTokenRetriever accessTokenRetriever) - { - throw new InvalidOperationException("TokenRetriever is not an IAccessTokenRetriever"); - } - - var accessTokenContext = new AccessTokenRetrievalContext() - { - HttpContext = context, - Metadata = metadata, - UserTokenRequestParameters = userAccessTokenParameters, - ApiAddress = apiAddress, - LocalPath = localPath, - }; - var result = await accessTokenRetriever.GetAccessToken(accessTokenContext); - - - var forwarder = context.RequestServices.GetRequiredService(); - var clientFactory = context.RequestServices.GetRequiredService(); - var transformerFactory = context.RequestServices.GetRequiredService(); - - var httpClient = clientFactory.CreateClient(localPath); - var transformer = transformerFactory.CreateTransformer(localPath, result); - - await forwarder.SendAsync(context, apiAddress, httpClient, ForwarderRequestConfig.Empty, transformer); - - var errorFeature = context.Features.Get(); - if (errorFeature != null) - { - var error = errorFeature.Error; - var exception = errorFeature.Exception; - - logger.ProxyResponseError(localPath, exception?.ToString() ?? error.ToString()); - } - }; - } -} \ No newline at end of file diff --git a/bff/src/Duende.Bff.Yarp/ReverseProxyBuilderExtensions.cs b/bff/src/Duende.Bff.Yarp/ReverseProxyBuilderExtensions.cs index e3d2a1636..68d0fabe9 100644 --- a/bff/src/Duende.Bff.Yarp/ReverseProxyBuilderExtensions.cs +++ b/bff/src/Duende.Bff.Yarp/ReverseProxyBuilderExtensions.cs @@ -1,5 +1,5 @@ -// // Copyright (c) Duende Software. All rights reserved. -// // See LICENSE in the project root for license information. +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. using Microsoft.Extensions.DependencyInjection; diff --git a/bff/src/Duende.Bff.Yarp/ReverseProxyEndpointConventionBuilderExtensions.cs b/bff/src/Duende.Bff.Yarp/ReverseProxyEndpointConventionBuilderExtensions.cs index 997e9d7cb..5107b1a98 100644 --- a/bff/src/Duende.Bff.Yarp/ReverseProxyEndpointConventionBuilderExtensions.cs +++ b/bff/src/Duende.Bff.Yarp/ReverseProxyEndpointConventionBuilderExtensions.cs @@ -1,5 +1,5 @@ -// // Copyright (c) Duende Software. All rights reserved. -// // See LICENSE in the project root for license information. +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. using System; using Microsoft.AspNetCore.Builder; diff --git a/bff/src/Duende.Bff.Yarp/YarpTransformExtensions.cs b/bff/src/Duende.Bff.Yarp/YarpTransformExtensions.cs new file mode 100644 index 000000000..c5b7ec8b2 --- /dev/null +++ b/bff/src/Duende.Bff.Yarp/YarpTransformExtensions.cs @@ -0,0 +1,32 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + +using Duende.AccessTokenManagement; +using Duende.Bff.Yarp; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Yarp.ReverseProxy.Transforms.Builder; + +namespace Microsoft.AspNetCore.Builder; + +/// +/// Extensions for YARP transforms +/// +public static class YarpTransformExtensions +{ + /// + /// Adds the transform which will request an access token for the proxied request. + /// + public static TransformBuilderContext AddBffAccessToken(this TransformBuilderContext context, PathString localPath) + { + var proofService = context.Services.GetRequiredService(); + var logger = context.Services.GetRequiredService>(); + context.RequestTransforms.Add( + new AccessTokenRequestTransform( + proofService, + logger + )); + return context; + } +} \ No newline at end of file diff --git a/bff/src/Duende.Bff/Configuration/BffEndpointRouteBuilderExtensions.cs b/bff/src/Duende.Bff/Configuration/BffEndpointRouteBuilderExtensions.cs index 0704b3e34..65278de1c 100644 --- a/bff/src/Duende.Bff/Configuration/BffEndpointRouteBuilderExtensions.cs +++ b/bff/src/Duende.Bff/Configuration/BffEndpointRouteBuilderExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Duende Software. All rights reserved. // See LICENSE in the project root for license information. +using System; using Duende.Bff; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; @@ -132,12 +133,18 @@ public static void MapBffDiagnosticsEndpoint(this IEndpointRouteBuilder endpoint } internal static void CheckLicense(this IEndpointRouteBuilder endpoints) + { + endpoints.ServiceProvider.CheckLicense(); + + } + + internal static void CheckLicense(this IServiceProvider serviceProvider) { if (LicenseChecked == false) { - var loggerFactory = endpoints.ServiceProvider.GetRequiredService(); - var options = endpoints.ServiceProvider.GetRequiredService>().Value; - + var loggerFactory = serviceProvider.GetRequiredService(); + var options = serviceProvider.GetRequiredService>().Value; + LicenseValidator.Initalize(loggerFactory, options); LicenseValidator.ValidateLicense(); } diff --git a/bff/src/Duende.Bff/EndpointServices/Diagnostics/DefaultDiagnosticsService.cs b/bff/src/Duende.Bff/EndpointServices/Diagnostics/DefaultDiagnosticsService.cs index ccfd35e51..7a9e74cb0 100644 --- a/bff/src/Duende.Bff/EndpointServices/Diagnostics/DefaultDiagnosticsService.cs +++ b/bff/src/Duende.Bff/EndpointServices/Diagnostics/DefaultDiagnosticsService.cs @@ -1,5 +1,5 @@ -// // Copyright (c) Duende Software. All rights reserved. -// // See LICENSE in the project root for license information. +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. using System.Text.Json; using System.Text.Json.Serialization; diff --git a/bff/src/Duende.Bff/EndpointServices/Diagnostics/IDiagnosticsService.cs b/bff/src/Duende.Bff/EndpointServices/Diagnostics/IDiagnosticsService.cs index 4cd981bb2..d720171d0 100644 --- a/bff/src/Duende.Bff/EndpointServices/Diagnostics/IDiagnosticsService.cs +++ b/bff/src/Duende.Bff/EndpointServices/Diagnostics/IDiagnosticsService.cs @@ -1,5 +1,5 @@ -// // Copyright (c) Duende Software. All rights reserved. -// // See LICENSE in the project root for license information. +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. namespace Duende.Bff; diff --git a/bff/src/Duende.Bff/General/AccessTokenRetreivalContext.cs b/bff/src/Duende.Bff/General/AccessTokenRetrievalContext.cs similarity index 71% rename from bff/src/Duende.Bff/General/AccessTokenRetreivalContext.cs rename to bff/src/Duende.Bff/General/AccessTokenRetrievalContext.cs index c8e4b587a..1f12e350f 100644 --- a/bff/src/Duende.Bff/General/AccessTokenRetreivalContext.cs +++ b/bff/src/Duende.Bff/General/AccessTokenRetrievalContext.cs @@ -1,6 +1,7 @@ // Copyright (c) Duende Software. All rights reserved. // See LICENSE in the project root for license information. +using System; using Duende.AccessTokenManagement.OpenIdConnect; using Microsoft.AspNetCore.Http; @@ -15,25 +16,26 @@ public class AccessTokenRetrievalContext /// The HttpContext of the incoming HTTP request that will be forwarded to /// the remote API. /// - public HttpContext HttpContext { get; set; } = default!; + public required HttpContext HttpContext { get; set; } /// /// Metadata that describes the remote API. /// - public BffRemoteApiEndpointMetadata Metadata { get; set; } = default!; - + public required BffRemoteApiEndpointMetadata Metadata { get; set; } + /// - /// The locally requested path. + /// Additional optional per request parameters for a user access token request. /// - public string LocalPath { get; set; } = string.Empty; - + public required UserTokenRequestParameters? UserTokenRequestParameters { get; set; } + + /// - /// The remote address of the API. + /// The locally requested path. /// - public string ApiAddress { get; set; } = string.Empty; + public required PathString LocalPath { get; set; } /// - /// Additional optional per request parameters for a user access token request. + /// The remote address of the API. /// - public UserTokenRequestParameters? UserTokenRequestParameters { get; set; } + public required Uri ApiAddress { get; set; } } diff --git a/bff/test/Duende.Bff.Tests/Endpoints/LocalEndpointTests.cs b/bff/test/Duende.Bff.Tests/Endpoints/LocalEndpointTests.cs index 61d4188c5..5654454df 100644 --- a/bff/test/Duende.Bff.Tests/Endpoints/LocalEndpointTests.cs +++ b/bff/test/Duende.Bff.Tests/Endpoints/LocalEndpointTests.cs @@ -21,7 +21,7 @@ public async Task calls_to_authorized_local_endpoint_should_succeed() { await BffHost.BffLoginAsync("alice"); - var apiResult = await BffHost.BrowserClient.CallBffHostApi( + ApiResponse apiResult = await BffHost.BrowserClient.CallBffHostApi( url: BffHost.Url("/local_authz") ); @@ -35,7 +35,7 @@ public async Task calls_to_authorized_local_endpoint_without_csrf_should_succeed { await BffHost.BffLoginAsync("alice"); - var apiResult = await BffHost.BrowserClient.CallBffHostApi( + ApiResponse apiResult = await BffHost.BrowserClient.CallBffHostApi( url: BffHost.Url("/local_authz_no_csrf") ); @@ -75,7 +75,7 @@ public async Task calls_to_local_endpoint_without_csrf_should_not_require_antifo [Fact] public async Task calls_to_anon_endpoint_should_allow_anonymous() { - var apiResult = await BffHost.BrowserClient.CallBffHostApi( + ApiResponse apiResult = await BffHost.BrowserClient.CallBffHostApi( url: BffHost.Url("/local_anon") ); @@ -89,7 +89,7 @@ public async Task put_to_local_endpoint_should_succeed() { await BffHost.BffLoginAsync("alice"); - var apiResult = await BffHost.BrowserClient.CallBffHostApi( + ApiResponse apiResult = await BffHost.BrowserClient.CallBffHostApi( url: BffHost.Url("/local_authz"), method: HttpMethod.Put, content: JsonContent.Create(new TestPayload("hello test api")) diff --git a/bff/test/Duende.Bff.Tests/Endpoints/RemoteEndpointTests.cs b/bff/test/Duende.Bff.Tests/Endpoints/RemoteEndpointTests.cs index 486785a24..adf7596f6 100644 --- a/bff/test/Duende.Bff.Tests/Endpoints/RemoteEndpointTests.cs +++ b/bff/test/Duende.Bff.Tests/Endpoints/RemoteEndpointTests.cs @@ -31,7 +31,7 @@ public async Task calls_to_remote_endpoint_should_forward_user_to_api() { await BffHost.BffLoginAsync("alice"); - var apiResult = await BffHost.BrowserClient.CallBffHostApi( + var (response, apiResult) = await BffHost.BrowserClient.CallBffHostApi( url: BffHost.Url("/api_user/test") ); @@ -39,6 +39,9 @@ public async Task calls_to_remote_endpoint_should_forward_user_to_api() apiResult.Path.ShouldBe("/test"); apiResult.Sub.ShouldBe("alice"); apiResult.ClientId.ShouldBe("spa"); + + response.Headers.GetValues("added-by-custom-default-transform").ShouldBe(["some-value"], + "this value is added by the CustomDefaultBffTransformBuilder()"); } [Fact] @@ -46,7 +49,7 @@ public async Task calls_to_remote_endpoint_with_useraccesstokenparameters_having { await BffHostWithNamedTokens.BffLoginAsync("alice"); - var apiResult = await BffHostWithNamedTokens.BrowserClient.CallBffHostApi( + ApiResponse apiResult = await BffHostWithNamedTokens.BrowserClient.CallBffHostApi( url: BffHostWithNamedTokens.Url("/api_user_with_useraccesstokenparameters_having_stored_named_token/test") ); @@ -72,7 +75,7 @@ public async Task put_to_remote_endpoint_should_forward_user_to_api() { await BffHost.BffLoginAsync("alice"); - var apiResult = await BffHost.BrowserClient.CallBffHostApi( + ApiResponse apiResult = await BffHost.BrowserClient.CallBffHostApi( url: BffHost.Url("/api_user/test"), method: HttpMethod.Put, content: JsonContent.Create(new TestPayload("hello test api")) @@ -91,7 +94,7 @@ public async Task post_to_remote_endpoint_should_forward_user_to_api() { await BffHost.BffLoginAsync("alice"); - var apiResult = await BffHost.BrowserClient.CallBffHostApi( + ApiResponse apiResult = await BffHost.BrowserClient.CallBffHostApi( url: BffHost.Url("/api_user/test"), method: HttpMethod.Post, content: JsonContent.Create(new TestPayload("hello test api")) @@ -109,7 +112,7 @@ public async Task post_to_remote_endpoint_should_forward_user_to_api() public async Task calls_to_remote_endpoint_should_forward_user_or_anonymous_to_api() { { - var apiResult = await BffHost.BrowserClient.CallBffHostApi( + ApiResponse apiResult = await BffHost.BrowserClient.CallBffHostApi( url: BffHost.Url("/api_user_or_anon/test") ); @@ -122,7 +125,7 @@ public async Task calls_to_remote_endpoint_should_forward_user_or_anonymous_to_a { await BffHost.BffLoginAsync("alice"); - var apiResult = await BffHost.BrowserClient.CallBffHostApi( + ApiResponse apiResult = await BffHost.BrowserClient.CallBffHostApi( url: BffHost.Url("/api_user_or_anon/test") ); @@ -138,7 +141,7 @@ public async Task calls_to_remote_endpoint_should_forward_client_token_to_api() { await BffHost.BffLoginAsync("alice"); - var apiResult = await BffHost.BrowserClient.CallBffHostApi( + ApiResponse apiResult = await BffHost.BrowserClient.CallBffHostApi( url: BffHost.Url("/api_client/test") ); @@ -164,7 +167,7 @@ public async Task calls_to_remote_endpoint_should_send_token_from_token_retrieve { await BffHost.BffLoginAsync("alice"); - var apiResult = await BffHost.BrowserClient.CallBffHostApi( + ApiResponse apiResult = await BffHost.BrowserClient.CallBffHostApi( url: BffHost.Url("/api_with_access_token_retriever") ); @@ -176,7 +179,7 @@ public async Task calls_to_remote_endpoint_should_send_token_from_token_retrieve public async Task calls_to_remote_endpoint_should_forward_user_or_client_to_api() { { - var apiResult = await BffHost.BrowserClient.CallBffHostApi( + ApiResponse apiResult = await BffHost.BrowserClient.CallBffHostApi( url: BffHost.Url("/api_user_or_client/test") ); @@ -189,7 +192,7 @@ public async Task calls_to_remote_endpoint_should_forward_user_or_client_to_api( { await BffHost.BffLoginAsync("alice"); - var apiResult = await BffHost.BrowserClient.CallBffHostApi( + ApiResponse apiResult = await BffHost.BrowserClient.CallBffHostApi( url: BffHost.Url("/api_user_or_client/test") ); @@ -204,7 +207,7 @@ public async Task calls_to_remote_endpoint_should_forward_user_or_client_to_api( public async Task calls_to_remote_endpoint_with_anon_should_be_anon() { { - var apiResult = await BffHost.BrowserClient.CallBffHostApi( + ApiResponse apiResult = await BffHost.BrowserClient.CallBffHostApi( url: BffHost.Url("/api_anon_only/test") ); @@ -217,7 +220,7 @@ public async Task calls_to_remote_endpoint_with_anon_should_be_anon() { await BffHost.BffLoginAsync("alice"); - var apiResult = await BffHost.BrowserClient.CallBffHostApi( + ApiResponse apiResult = await BffHost.BrowserClient.CallBffHostApi( url: BffHost.Url("/api_anon_only/test") ); @@ -282,7 +285,7 @@ public async Task endpoints_that_disable_csrf_should_not_require_csrf_header() { await BffHost.BffLoginAsync("alice"); - var apiResult = await BffHost.BrowserClient.CallBffHostApi( + ApiResponse apiResult = await BffHost.BrowserClient.CallBffHostApi( url: BffHost.Url("/api_user_no_csrf/test") ); @@ -293,22 +296,23 @@ public async Task endpoints_that_disable_csrf_should_not_require_csrf_header() } [Fact] - public async Task calls_to_endpoint_without_bff_metadata_should_fail() + public async Task endpoint_can_be_configured_with_custom_transform() { - Func f = () => BffHost.BrowserClient.CallBffHostApi( - url: BffHost.Url("/not_bff_endpoint") - ); - await f.ShouldThrowAsync(); - } + await BffHost.BffLoginAsync("alice"); - [Fact] - public async Task calls_to_bff_not_in_endpoint_routing_should_fail() - { - Func f = () => BffHost.BrowserClient.CallBffHostApi( - url: BffHost.Url("/invalid_endpoint/test") - ); - await f.ShouldThrowAsync(); - } + var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/api_custom_transform/test")); + req.Headers.Add("x-csrf", "1"); + req.Headers.Add("my-header-to-be-copied-by-yarp", "copied-value"); + var response = await BffHost.BrowserClient.SendAsync(req); + response.IsSuccessStatusCode.ShouldBeTrue(); + response.Content.Headers.ContentType!.MediaType.ShouldBe("application/json"); + var json = await response.Content.ReadAsStringAsync(); + ApiResponse apiResult = JsonSerializer.Deserialize(json).ShouldNotBeNull(); + apiResult.RequestHeaders["my-header-to-be-copied-by-yarp"].First().ShouldBe("copied-value"); + + response.Content.Headers.Select(x => x.Key).ShouldNotContain("added-by-custom-default-transform", + "a custom transform doesn't run the defaults"); + } } } \ No newline at end of file diff --git a/bff/test/Duende.Bff.Tests/Endpoints/YarpRemoteEndpointTests.cs b/bff/test/Duende.Bff.Tests/Endpoints/YarpRemoteEndpointTests.cs index 148208e2b..596d7694d 100644 --- a/bff/test/Duende.Bff.Tests/Endpoints/YarpRemoteEndpointTests.cs +++ b/bff/test/Duende.Bff.Tests/Endpoints/YarpRemoteEndpointTests.cs @@ -5,6 +5,7 @@ using System.Net; using System.Net.Http; using System.Threading.Tasks; +using Duende.Bff.Tests.TestFramework; using Shouldly; using Xunit; using Xunit.Abstractions; @@ -16,8 +17,8 @@ public class YarpRemoteEndpointTests(ITestOutputHelper output) : YarpBffIntegrat [Fact] public async Task anonymous_call_with_no_csrf_header_to_no_token_requirement_no_csrf_route_should_succeed() { - await BffHost.BrowserClient.CallBffHostApi( - url: BffHost.Url("/api_anon_no_csrf/test"), + await YarpBasedBffHost.BrowserClient.CallBffHostApi( + url: YarpBasedBffHost.Url("/api_anon_no_csrf/test"), expectedStatusCode: HttpStatusCode.OK ); } @@ -25,8 +26,8 @@ await BffHost.BrowserClient.CallBffHostApi( [Fact] public async Task anonymous_call_with_no_csrf_header_to_csrf_route_should_fail() { - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/api_anon/test")); - var response = await BffHost.BrowserClient.SendAsync(req); + var req = new HttpRequestMessage(HttpMethod.Get, YarpBasedBffHost.Url("/api_anon/test")); + var response = await YarpBasedBffHost.BrowserClient.SendAsync(req); response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); } @@ -35,8 +36,8 @@ public async Task anonymous_call_with_no_csrf_header_to_csrf_route_should_fail() [Fact] public async Task anonymous_call_to_no_token_requirement_route_should_succeed() { - await BffHost.BrowserClient.CallBffHostApi( - url: BffHost.Url("/api_anon/test"), + await YarpBasedBffHost.BrowserClient.CallBffHostApi( + url: YarpBasedBffHost.Url("/api_anon/test"), expectedStatusCode: HttpStatusCode.OK ); } @@ -44,8 +45,8 @@ await BffHost.BrowserClient.CallBffHostApi( [Fact] public async Task anonymous_call_to_user_token_requirement_route_should_fail() { - await BffHost.BrowserClient.CallBffHostApi( - url: BffHost.Url("/api_user/test"), + await YarpBasedBffHost.BrowserClient.CallBffHostApi( + url: YarpBasedBffHost.Url("/api_user/test"), expectedStatusCode: HttpStatusCode.Unauthorized ); } @@ -53,8 +54,8 @@ await BffHost.BrowserClient.CallBffHostApi( [Fact] public async Task anonymous_call_to_optional_user_token_route_should_succeed() { - var apiResult = await BffHost.BrowserClient.CallBffHostApi( - url: BffHost.Url("/api_optional_user/test") + ApiResponse apiResult = await YarpBasedBffHost.BrowserClient.CallBffHostApi( + url: YarpBasedBffHost.Url("/api_optional_user/test") ); apiResult.Method.ShouldBe("GET"); @@ -68,10 +69,10 @@ public async Task anonymous_call_to_optional_user_token_route_should_succeed() [InlineData("/api_optional_user/test")] public async Task authenticated_GET_should_forward_user_to_api(string route) { - await BffHost.BffLoginAsync("alice"); + await YarpBasedBffHost.BffLoginAsync("alice"); - var apiResult = await BffHost.BrowserClient.CallBffHostApi( - url: BffHost.Url(route) + ApiResponse apiResult = await YarpBasedBffHost.BrowserClient.CallBffHostApi( + url: YarpBasedBffHost.Url(route) ); apiResult.Method.ShouldBe("GET"); @@ -85,10 +86,10 @@ public async Task authenticated_GET_should_forward_user_to_api(string route) [InlineData("/api_optional_user/test")] public async Task authenticated_PUT_should_forward_user_to_api(string route) { - await BffHost.BffLoginAsync("alice"); + await YarpBasedBffHost.BffLoginAsync("alice"); - var apiResult = await BffHost.BrowserClient.CallBffHostApi( - url: BffHost.Url(route), + ApiResponse apiResult = await YarpBasedBffHost.BrowserClient.CallBffHostApi( + url: YarpBasedBffHost.Url(route), method: HttpMethod.Put ); @@ -103,10 +104,10 @@ public async Task authenticated_PUT_should_forward_user_to_api(string route) [InlineData("/api_optional_user/test")] public async Task authenticated_POST_should_forward_user_to_api(string route) { - await BffHost.BffLoginAsync("alice"); + await YarpBasedBffHost.BffLoginAsync("alice"); - var apiResult = await BffHost.BrowserClient.CallBffHostApi( - url: BffHost.Url(route), + ApiResponse apiResult = await YarpBasedBffHost.BrowserClient.CallBffHostApi( + url: YarpBasedBffHost.Url(route), method: HttpMethod.Post ); @@ -119,10 +120,10 @@ public async Task authenticated_POST_should_forward_user_to_api(string route) [Fact] public async Task call_to_client_token_route_should_forward_client_token_to_api() { - await BffHost.BffLoginAsync("alice"); + await YarpBasedBffHost.BffLoginAsync("alice"); - var apiResult = await BffHost.BrowserClient.CallBffHostApi( - url: BffHost.Url("/api_client/test") + ApiResponse apiResult = await YarpBasedBffHost.BrowserClient.CallBffHostApi( + url: YarpBasedBffHost.Url("/api_client/test") ); apiResult.Method.ShouldBe("GET"); @@ -135,8 +136,8 @@ public async Task call_to_client_token_route_should_forward_client_token_to_api( public async Task call_to_user_or_client_token_route_should_forward_user_or_client_token_to_api() { { - var apiResult = await BffHost.BrowserClient.CallBffHostApi( - url: BffHost.Url("/api_user_or_client/test") + ApiResponse apiResult = await YarpBasedBffHost.BrowserClient.CallBffHostApi( + url: YarpBasedBffHost.Url("/api_user_or_client/test") ); apiResult.Method.ShouldBe("GET"); @@ -146,10 +147,10 @@ public async Task call_to_user_or_client_token_route_should_forward_user_or_clie } { - await BffHost.BffLoginAsync("alice"); + await YarpBasedBffHost.BffLoginAsync("alice"); - var apiResult = await BffHost.BrowserClient.CallBffHostApi( - url: BffHost.Url("/api_user_or_client/test") + ApiResponse apiResult = await YarpBasedBffHost.BrowserClient.CallBffHostApi( + url: YarpBasedBffHost.Url("/api_user_or_client/test") ); apiResult.Method.ShouldBe("GET"); @@ -162,11 +163,11 @@ public async Task call_to_user_or_client_token_route_should_forward_user_or_clie [Fact] public async Task response_status_401_from_remote_endpoint_should_return_401_from_bff() { - await BffHost.BffLoginAsync("alice"); + await YarpBasedBffHost.BffLoginAsync("alice"); ApiHost.ApiStatusCodeToReturn = 401; - var response = await BffHost.BrowserClient.CallBffHostApi( - url: BffHost.Url("/api_user/test"), + var response = await YarpBasedBffHost.BrowserClient.CallBffHostApi( + url: YarpBasedBffHost.Url("/api_user/test"), expectedStatusCode: HttpStatusCode.Unauthorized ); } @@ -174,11 +175,11 @@ public async Task response_status_401_from_remote_endpoint_should_return_401_fro [Fact] public async Task response_status_403_from_remote_endpoint_should_return_403_from_bff() { - await BffHost.BffLoginAsync("alice"); + await YarpBasedBffHost.BffLoginAsync("alice"); ApiHost.ApiStatusCodeToReturn = 403; - var response = await BffHost.BrowserClient.CallBffHostApi( - url: BffHost.Url("/api_user/test"), + var response = await YarpBasedBffHost.BrowserClient.CallBffHostApi( + url: YarpBasedBffHost.Url("/api_user/test"), expectedStatusCode: HttpStatusCode.Forbidden ); } @@ -186,8 +187,8 @@ public async Task response_status_403_from_remote_endpoint_should_return_403_fro [Fact] public async Task invalid_configuration_of_routes_should_return_500() { - var response = await BffHost.BrowserClient.CallBffHostApi( - url: BffHost.Url("/api_invalid/test"), + var response = await YarpBasedBffHost.BrowserClient.CallBffHostApi( + url: YarpBasedBffHost.Url("/api_invalid/test"), expectedStatusCode: HttpStatusCode.InternalServerError ); } diff --git a/bff/test/Duende.Bff.Tests/Exploration/YarpTests.cs b/bff/test/Duende.Bff.Tests/Exploration/YarpTests.cs deleted file mode 100644 index 01cd92231..000000000 --- a/bff/test/Duende.Bff.Tests/Exploration/YarpTests.cs +++ /dev/null @@ -1,173 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Xunit; -using Yarp.ReverseProxy.Configuration; -using Yarp.ReverseProxy.Forwarder; -using Yarp.ReverseProxy.Transforms; -using Yarp.ReverseProxy.Transforms.Builder; - -namespace Duende.Bff.Tests.Exploration -{ - public class YarpTests - { - [Fact] - public async Task Can_proxy_two_requests() - { - await using var apiServer = await GetApiServer(); - await using var proxy = await GetProxyServer(apiServer.GetServerUri()); - - var client = new HttpClient() - { - BaseAddress = proxy.GetServerUri() - }; - var result = await client.GetAsync("/proxy/api"); - result.EnsureSuccessStatusCode(); - } - - private static async Task GetApiServer() - { - Server apiServer = null; - try - { - apiServer = new Server( - configureServices: (services) => - { - - }, - configure: app => - { - app.MapGet("/api", () => "ok"); - }); - - await apiServer.Start(); - return apiServer; - } - catch - { - if (apiServer != null) await apiServer.DisposeAsync(); - throw; - } - } - - - private static async Task GetProxyServer(Uri api) - { - Server apiServer = null; - - var requestConfig = new ForwarderRequestConfig { ActivityTimeout = TimeSpan.FromSeconds(100) }; - - - try - { - apiServer = new Server( - configureServices: (services) => - { - services.AddHttpForwarder(); - services.AddReverseProxy(); - }, - configure: app => - { - var transformer = app.Services.GetRequiredService() - .Create(context => - { - context.RequestTransforms.Add(new PathStringTransform(PathStringTransform.PathTransformMode.RemovePrefix, "/proxy")); - }); - - var httpClient = app.Services.GetRequiredService() - .CreateClient(new ForwarderHttpClientContext { NewConfig = HttpClientConfig.Empty }); - - // When using IHttpForwarder for direct forwarding you are responsible for routing, destination discovery, load balancing, affinity, etc.. - // For an alternate example that includes those features see BasicYarpSample. - app.Map("/proxy/{**catch-all}", async (HttpContext httpContext, IHttpForwarder forwarder) => - { - var error = await forwarder.SendAsync(httpContext, api.ToString(), - httpClient, requestConfig, transformer); - // Check if the operation was successful - if (error != ForwarderError.None) - { - var errorFeature = httpContext.GetForwarderErrorFeature(); - var exception = errorFeature.Exception; - } - }); - - }); - - await apiServer.Start(); - return apiServer; - } - catch - { - if (apiServer != null) await apiServer.DisposeAsync(); - throw; - } - } - } - - public class Server : IAsyncDisposable - { - private WebApplicationBuilder _builder = WebApplication.CreateBuilder(); - private WebApplication App; - - public Server(Action configureServices, Action configure) - { - - _builder.WebHost.UseKestrel( - opt => - { - opt.Limits.KeepAliveTimeout = TimeSpan.MaxValue; - opt.Limits.RequestHeadersTimeout = TimeSpan.MaxValue; - }) - .UseUrls("http://127.0.0.1:0") // port zero to use random dynamic port - ; - - - var services = _builder.Services; - services.AddRouting(); - configureServices(services); - - App = _builder.Build(); - - App.UseRouting(); - configure(App); - } - - public Uri GetServerUri() - { - var address = (App as IApplicationBuilder).ServerFeatures.Get(); - var uri = new Uri(address!.Addresses.First()); - return uri; - } - - public async Task Start(CancellationToken ct = default) - { - await App.StartAsync(ct); - } - - public async Task StopAsync(CancellationToken ct = default) - { - if (App != null) - { - await App.StopAsync(ct); - await App.DisposeAsync(); - App = null; - } - } - - public async ValueTask DisposeAsync() - { - await StopAsync(CancellationToken.None); - } - } -} diff --git a/bff/test/Duende.Bff.Tests/IAccessTokenRetriever_Extensibility_tests.cs b/bff/test/Duende.Bff.Tests/IAccessTokenRetriever_Extensibility_tests.cs new file mode 100644 index 000000000..310786d71 --- /dev/null +++ b/bff/test/Duende.Bff.Tests/IAccessTokenRetriever_Extensibility_tests.cs @@ -0,0 +1,103 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + +using System; +using Duende.Bff.Tests.TestHosts; +using Microsoft.Extensions.DependencyInjection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Logging; +using Xunit; +using Xunit.Abstractions; +using Microsoft.Extensions.Logging.Abstractions; +using Shouldly; + +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 +{ + + private ContextCapturingAccessTokenRetriever _customAccessTokenReceiver { get; } = new(NullLogger.Instance); + + public IAccessTokenRetriever_Extensibility_tests(ITestOutputHelper output) : base(output) + { + BffHost.OnConfigureServices += services => + { + services.AddSingleton(_customAccessTokenReceiver); + }; + + BffHost.OnConfigure += app => + { + app.UseEndpoints((endpoints) => + { + endpoints.MapRemoteBffApiEndpoint("/custom", ApiHost.Url("/some/path")) + .RequireAccessToken() + .WithAccessTokenRetriever(); + + }); + + app.Map("/subPath", + subPath => + { + subPath.UseRouting(); + subPath.UseEndpoints((endpoints) => + { + endpoints.MapRemoteBffApiEndpoint("/custom_within_subpath", ApiHost.Url("/some/path")) + .RequireAccessToken() + .WithAccessTokenRetriever(); + }); + }); + + }; + } + + [Fact] + public async Task When_calling_custom_endpoint_then_AccessTokenRetrievalContext_has_api_address_and_localpath() + { + await BffHost.BffLoginAsync("alice"); + + await BffHost.BrowserClient.CallBffHostApi(BffHost.Url("/custom")); + + var usedContext = _customAccessTokenReceiver.UsedContext.ShouldNotBeNull(); + + usedContext.Metadata.RequiredTokenType.ShouldBe(TokenType.User); + + usedContext.ApiAddress.ShouldBe(new Uri(ApiHost.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 BffHost.BffLoginAsync("alice"); + + await BffHost.BrowserClient.CallBffHostApi(BffHost.Url("/subPath/custom_within_subpath")); + + var usedContext = _customAccessTokenReceiver.UsedContext.ShouldNotBeNull(); + + usedContext.ApiAddress.ShouldBe(new Uri(ApiHost.Url("/some/path"))); + usedContext.LocalPath.ToString().ShouldBe("/custom_within_subpath"); + + } + + /// + /// Captures the context in which the access token retriever is called, so we can assert on it + /// + private class ContextCapturingAccessTokenRetriever : DefaultAccessTokenRetriever + { + public AccessTokenRetrievalContext? UsedContext { get; private set; } + public ContextCapturingAccessTokenRetriever(ILogger logger) : base(logger) + { + } + + public override Task GetAccessToken(AccessTokenRetrievalContext context) + { + UsedContext = context; + return base.GetAccessToken(context); + } + } +} diff --git a/bff/test/Duende.Bff.Tests/TestFramework/TestBrowserClient.cs b/bff/test/Duende.Bff.Tests/TestFramework/TestBrowserClient.cs index ddd89e6fc..416db71d3 100644 --- a/bff/test/Duende.Bff.Tests/TestFramework/TestBrowserClient.cs +++ b/bff/test/Duende.Bff.Tests/TestFramework/TestBrowserClient.cs @@ -86,7 +86,7 @@ private void RemoveCookie(string uri, string name) /// If specified, the system will verify that this reponse code was given /// Cancellation token /// The specified api response - public async Task CallBffHostApi( + public async Task CallBffHostApi( string url, HttpStatusCode? expectedStatusCode = null, CancellationToken ct = default) @@ -104,17 +104,17 @@ public async Task CallBffHostApi( apiResult.Method.ShouldBe("GET", StringCompareShould.IgnoreCase); - return apiResult; + return new (response, apiResult); } else { response.StatusCode.ToString().ShouldBe(expectedStatusCode.ToString()); - return null!; + return new (response, null!); } } - public async Task CallBffHostApi( + public async Task CallBffHostApi( string url, HttpMethod method, HttpContent? content = null, @@ -136,15 +136,20 @@ public async Task CallBffHostApi( var apiResult = JsonSerializer.Deserialize(json).ShouldNotBeNull(); apiResult.Method.ShouldBe(method.ToString(), StringCompareShould.IgnoreCase); - return apiResult; + return new(response, apiResult); } else { response.StatusCode.ToString().ShouldBe(expectedStatusCode.ToString()); - return null!; + return new(response, null!); } } + public record BffHostResponse(HttpResponseMessage HttpResponse, ApiResponse ApiResponse) + { + public static implicit operator HttpResponseMessage(BffHostResponse response) => response.HttpResponse; + public static implicit operator ApiResponse(BffHostResponse response) => response.ApiResponse; + } } \ No newline at end of file diff --git a/bff/test/Duende.Bff.Tests/TestFramework/TestSerializerOptions.cs b/bff/test/Duende.Bff.Tests/TestFramework/TestSerializerOptions.cs index 7d6d11ddb..0970f45a9 100644 --- a/bff/test/Duende.Bff.Tests/TestFramework/TestSerializerOptions.cs +++ b/bff/test/Duende.Bff.Tests/TestFramework/TestSerializerOptions.cs @@ -1,5 +1,5 @@ -// // Copyright (c) Duende Software. All rights reserved. -// // See LICENSE in the project root for license information. +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. using System.Text.Json; diff --git a/bff/test/Duende.Bff.Tests/TestHosts/ApiHost.cs b/bff/test/Duende.Bff.Tests/TestHosts/ApiHost.cs index 66a469fa1..3154ce224 100644 --- a/bff/test/Duende.Bff.Tests/TestHosts/ApiHost.cs +++ b/bff/test/Duende.Bff.Tests/TestHosts/ApiHost.cs @@ -1,6 +1,7 @@ // Copyright (c) Duende Software. All rights reserved. // See LICENSE in the project root for license information. +using System; using System.Collections.Generic; using Duende.Bff.Tests.TestFramework; using Duende.IdentityServer.Models; diff --git a/bff/test/Duende.Bff.Tests/TestHosts/BffHost.cs b/bff/test/Duende.Bff.Tests/TestHosts/BffHost.cs index 30f26690e..45a6076f6 100644 --- a/bff/test/Duende.Bff.Tests/TestHosts/BffHost.cs +++ b/bff/test/Duende.Bff.Tests/TestHosts/BffHost.cs @@ -17,6 +17,10 @@ using Duende.Bff.Yarp; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Yarp.ReverseProxy.Forwarder; +using Yarp.ReverseProxy.Transforms; +using Yarp.ReverseProxy.Transforms.Builder; namespace Duende.Bff.Tests.TestHosts; @@ -61,9 +65,9 @@ private void ConfigureServices(IServiceCollection services) BffOptions = options; }); - services.AddSingleton( + services.AddSingleton( new CallbackHttpMessageInvokerFactory( - path => new HttpMessageInvoker(_apiHost.Server.CreateHandler()))); + () => new HttpMessageInvoker(_apiHost.Server.CreateHandler()))); services.AddAuthentication("cookie") .AddCookie("cookie", options => @@ -71,6 +75,8 @@ private void ConfigureServices(IServiceCollection services) options.Cookie.Name = "bff"; }); + services.AddSingleton(CustomDefaultBffTransformBuilder); + bff.AddServerSideSessions(); bff.AddRemoteApis(); @@ -118,6 +124,13 @@ private void ConfigureServices(IServiceCollection services) => await _identityServerHost.CreateJwtAccessTokenAsync())); } + + private void CustomDefaultBffTransformBuilder(string localpath, TransformBuilderContext context) + { + context.AddResponseHeader("added-by-custom-default-transform", "some-value"); + DefaultBffYarpTransformerBuilders.DirectProxyWithAccessToken(localpath, context); + } + private void Configure(IApplicationBuilder app) { app.UseAuthentication(); @@ -421,6 +434,16 @@ private void Configure(IApplicationBuilder app) endpoints.MapRemoteBffApiEndpoint( "/api_anon_only", _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(), + c => + { + c.CopyRequestHeaders = true; + DefaultBffYarpTransformerBuilders.DirectProxyWithAccessToken("/api_custom_transform", c); + }); + endpoints.MapRemoteBffApiEndpoint( "/api_with_access_token_retriever", _apiHost.Url()) .RequireAccessToken(TokenType.UserOrClient) @@ -430,14 +453,7 @@ private void Configure(IApplicationBuilder app) "/api_with_access_token_retrieval_that_fails", _apiHost.Url()) .RequireAccessToken(TokenType.UserOrClient) .WithAccessTokenRetriever(); - - endpoints.Map( - "/not_bff_endpoint", - RemoteApiEndpoint.Map("/not_bff_endpoint", _apiHost.Url())); }); - - app.Map("/invalid_endpoint", - invalid => invalid.Use(next => RemoteApiEndpoint.Map("/invalid_endpoint", _apiHost.Url()))); } public async Task GetIsUserLoggedInAsync(string? userQuery = null) @@ -519,13 +535,18 @@ public async Task BffLogoutAsync(string? sid = null) return response; } - private class CallbackHttpMessageInvokerFactory(Func callback) - : IHttpMessageInvokerFactory + public class CallbackHttpMessageInvokerFactory : IForwarderHttpClientFactory { + public CallbackHttpMessageInvokerFactory(Func callback) + { + CreateInvoker = callback; + } + + public Func CreateInvoker { get; set; } - public HttpMessageInvoker CreateClient(string localPath) + public HttpMessageInvoker CreateClient(ForwarderHttpClientContext context) { - return callback.Invoke(localPath); + return CreateInvoker.Invoke(); } } } \ No newline at end of file diff --git a/bff/test/Duende.Bff.Tests/TestHosts/BffHostUsingResourceNamedTokens.cs b/bff/test/Duende.Bff.Tests/TestHosts/BffHostUsingResourceNamedTokens.cs index 25ce6cc8c..858a124c8 100644 --- a/bff/test/Duende.Bff.Tests/TestHosts/BffHostUsingResourceNamedTokens.cs +++ b/bff/test/Duende.Bff.Tests/TestHosts/BffHostUsingResourceNamedTokens.cs @@ -1,11 +1,11 @@ // Copyright (c) Duende Software. All rights reserved. // See LICENSE in the project root for license information. +using System; using Duende.Bff.Tests.TestFramework; using Shouldly; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; -using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -17,7 +17,7 @@ using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Authentication; using Microsoft.IdentityModel.Protocols.OpenIdConnect; -using Xunit.Abstractions; +using Yarp.ReverseProxy.Forwarder; namespace Duende.Bff.Tests.TestHosts { @@ -59,9 +59,8 @@ private void ConfigureServices(IServiceCollection services) BffOptions = options; }); - services.AddSingleton( - new CallbackHttpMessageInvokerFactory( - path => new HttpMessageInvoker(_apiHost.Server.CreateHandler()))); + services.AddSingleton( + new BackChannelHttpMessageInvokerFactory(_apiHost.Server.CreateHandler())); services.AddAuthentication("cookie") .AddCookie("cookie", options => @@ -159,9 +158,6 @@ private void Configure(IApplicationBuilder app) .WithUserAccessTokenParameter(new BffUserAccessTokenParameters("cookie", null, true, "named_token_not_stored")) .RequireAccessToken(); }); - - app.Map("/invalid_endpoint", - invalid => invalid.Use(next => RemoteApiEndpoint.Map("/invalid_endpoint", _apiHost.Url()))); } public async Task GetIsUserLoggedInAsync(string? userQuery = null) @@ -226,20 +222,12 @@ public async Task BffLogoutAsync(string? sid = null) response = await BrowserClient.GetAsync(Url(response.Headers.Location.ToString())); return response; } + } - public class CallbackHttpMessageInvokerFactory : IHttpMessageInvokerFactory - { - public CallbackHttpMessageInvokerFactory(Func callback) - { - CreateInvoker = callback; - } - - public Func CreateInvoker { get; set; } - - public HttpMessageInvoker CreateClient(string localPath) - { - return CreateInvoker.Invoke(localPath); - } - } + public class BackChannelHttpMessageInvokerFactory(HttpMessageHandler backChannel) + : IForwarderHttpClientFactory + { + public HttpMessageInvoker CreateClient(ForwarderHttpClientContext context) => + new HttpMessageInvoker(backChannel); } } \ No newline at end of file diff --git a/bff/test/Duende.Bff.Tests/TestHosts/YarpBffHost.cs b/bff/test/Duende.Bff.Tests/TestHosts/YarpBffHost.cs index a4e082475..385cd2e4d 100644 --- a/bff/test/Duende.Bff.Tests/TestHosts/YarpBffHost.cs +++ b/bff/test/Duende.Bff.Tests/TestHosts/YarpBffHost.cs @@ -245,31 +245,6 @@ private void Configure(IApplicationBuilder app) endpoints.MapBffManagementEndpoints(); endpoints.MapReverseProxy(proxyApp => { proxyApp.UseAntiforgeryCheck(); }); - - // replace with YARP endpoints - // endpoints.MapRemoteBffApiEndpoint( - // "/api_user", _apiHost.Url()) - // .RequireAccessToken(); - // - // endpoints.MapRemoteBffApiEndpoint( - // "/api_user_no_csrf", _apiHost.Url()) - // .SkipAntiforgery() - // .RequireAccessToken(); - // - // endpoints.MapRemoteBffApiEndpoint( - // "/api_client", _apiHost.Url()) - // .RequireAccessToken(TokenType.Client); - // - // endpoints.MapRemoteBffApiEndpoint( - // "/api_user_or_client", _apiHost.Url()) - // .RequireAccessToken(TokenType.UserOrClient); - // - // endpoints.MapRemoteBffApiEndpoint( - // "/api_user_or_anon", _apiHost.Url()) - // .WithOptionalUserAccessToken(); - // - // endpoints.MapRemoteBffApiEndpoint( - // "/api_anon_only", _apiHost.Url()); }); } diff --git a/bff/test/Duende.Bff.Tests/TestHosts/YarpBffIntegrationTestBase.cs b/bff/test/Duende.Bff.Tests/TestHosts/YarpBffIntegrationTestBase.cs index a0954d2f8..0355c1535 100644 --- a/bff/test/Duende.Bff.Tests/TestHosts/YarpBffIntegrationTestBase.cs +++ b/bff/test/Duende.Bff.Tests/TestHosts/YarpBffIntegrationTestBase.cs @@ -17,7 +17,7 @@ public class YarpBffIntegrationTestBase : IAsyncLifetime { private readonly IdentityServerHost _identityServerHost; protected readonly ApiHost ApiHost; - protected readonly YarpBffHost BffHost; + protected readonly YarpBffHost YarpBasedBffHost; private BffHostUsingResourceNamedTokens _bffHostWithNamedTokens; protected YarpBffIntegrationTestBase(ITestOutputHelper output) @@ -40,14 +40,14 @@ protected YarpBffIntegrationTestBase(ITestOutputHelper output) _identityServerHost.OnConfigureServices += services => { services.AddTransient(provider => new DefaultBackChannelLogoutHttpClient( - BffHost!.HttpClient, + YarpBasedBffHost!.HttpClient, provider.GetRequiredService(), provider.GetRequiredService())); }; ApiHost = new ApiHost(output.WriteLine, _identityServerHost, "scope1"); - BffHost = new YarpBffHost(output.WriteLine, _identityServerHost, ApiHost, "spa"); + YarpBasedBffHost = new YarpBffHost(output.WriteLine, _identityServerHost, ApiHost, "spa"); _bffHostWithNamedTokens = new BffHostUsingResourceNamedTokens(output.WriteLine, _identityServerHost, ApiHost, "spa"); } @@ -61,7 +61,7 @@ public async Task InitializeAsync() { await _identityServerHost.InitializeAsync(); await ApiHost.InitializeAsync(); - await BffHost.InitializeAsync(); + await YarpBasedBffHost.InitializeAsync(); await _bffHostWithNamedTokens.InitializeAsync(); } @@ -70,7 +70,7 @@ public async Task DisposeAsync() { await _identityServerHost.DisposeAsync(); await ApiHost.DisposeAsync(); - await BffHost.DisposeAsync(); + await YarpBasedBffHost.DisposeAsync(); await _bffHostWithNamedTokens.DisposeAsync(); } }