mirror of
https://github.com/DuendeSoftware/products
synced 2026-05-24 09:28:24 +00:00
Bff yarp proxy improvements (#1734)
* exploration test for yarp * BFF: Simplify the wireup of YARP.
This commit is contained in:
parent
68ef1f8da4
commit
9039293dfc
40 changed files with 564 additions and 523 deletions
101
bff/docs/upgrade-guide.md
Normal file
101
bff/docs/upgrade-guide.md
Normal file
|
|
@ -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<IForwarderHttpClientFactory>(
|
||||
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<BffYarpTransformBuilder>(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.
|
||||
|
||||
/// <summary>
|
||||
/// The locally requested path.
|
||||
/// </summary>
|
||||
public required PathString LocalPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The remote address of the API.
|
||||
/// </summary>
|
||||
public required Uri ApiAddress { get; set; }
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
namespace IdentityServer;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<TargetFrameworks>net8.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
|
||||
|
|
|
|||
|
|
@ -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;
|
|||
/// <summary>
|
||||
/// Adds an access token to outgoing requests
|
||||
/// </summary>
|
||||
public class AccessTokenRequestTransform : RequestTransform
|
||||
public class AccessTokenRequestTransform(
|
||||
IDPoPProofService proofService,
|
||||
ILogger<AccessTokenRequestTransform> logger) : RequestTransform
|
||||
{
|
||||
private readonly IDPoPProofService _dPoPProofService;
|
||||
private readonly ILogger<AccessTokenRequestTransform> _logger;
|
||||
private readonly AccessTokenResult _token;
|
||||
private readonly string? _routeId;
|
||||
private readonly TokenType? _tokenType;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="proofService"></param>
|
||||
/// <param name="logger"></param>
|
||||
/// <param name="accessToken"></param>
|
||||
/// <param name="routeId"></param>
|
||||
/// <param name="tokenType"></param>
|
||||
public AccessTokenRequestTransform(
|
||||
IDPoPProofService proofService,
|
||||
ILogger<AccessTokenRequestTransform> logger,
|
||||
AccessTokenResult accessToken,
|
||||
string? routeId = null,
|
||||
TokenType? tokenType = null)
|
||||
{
|
||||
_dPoPProofService = proofService;
|
||||
_logger = logger;
|
||||
_token = accessToken ?? throw new ArgumentNullException(nameof(accessToken));
|
||||
_routeId = routeId;
|
||||
_tokenType = tokenType;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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<BffRemoteApiEndpointMetadata>()
|
||||
// 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 class AccessTokenRequestTransform : RequestTransform
|
|||
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 class AccessTokenRequestTransform : RequestTransform
|
|||
}
|
||||
}
|
||||
|
||||
private void ApplyError(RequestTransformContext context, AccessTokenRetrievalError tokenError, string routeId, TokenType? tokenType)
|
||||
private static BffRemoteApiEndpointMetadata? GetBffMetadataFromYarp(Endpoint endpoint)
|
||||
{
|
||||
var yarp = endpoint.Metadata.GetMetadata<RouteModel>();
|
||||
if (yarp == null)
|
||||
return null;
|
||||
|
||||
TokenType? requiredTokenType = null;
|
||||
if (Enum.TryParse<TokenType>(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 @@ public class AccessTokenRequestTransform : RequestTransform
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -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 @@ public class AccessTokenTransformProvider : ITransformProvider
|
|||
/// <inheritdoc />
|
||||
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 class AccessTokenTransformProvider : ITransformProvider
|
|||
});
|
||||
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<TokenType>(tokenTypeMetadata, true, out _))
|
||||
{
|
||||
throw new ArgumentException("Invalid value for Duende.Bff.Yarp.TokenType metadata");
|
||||
}
|
||||
|
|
@ -114,13 +109,9 @@ public class AccessTokenTransformProvider : ITransformProvider
|
|||
{
|
||||
transformContext.HttpContext.CheckForBffMiddleware(_options);
|
||||
|
||||
var token = await transformContext.HttpContext.GetManagedAccessToken(tokenType, optional);
|
||||
|
||||
var accessTokenTransform = new AccessTokenRequestTransform(
|
||||
_dPoPProofService,
|
||||
_loggerFactory.CreateLogger<AccessTokenRequestTransform>(),
|
||||
token,
|
||||
transformBuildContext?.Route?.RouteId, tokenType);
|
||||
_loggerFactory.CreateLogger<AccessTokenRequestTransform>());
|
||||
|
||||
await accessTokenTransform.ApplyAsync(transformContext);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<IHttpMessageInvokerFactory, DefaultHttpMessageInvokerFactory>();
|
||||
builder.Services.TryAddSingleton<IHttpTransformerFactory, DefaultHttpTransformerFactory>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a custom HttpMessageInvokerFactory to DI
|
||||
/// </summary>
|
||||
/// <param name="builder"></param>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public static BffBuilder AddHttpMessageInvokerFactory<T>(this BffBuilder builder)
|
||||
where T : class, IHttpMessageInvokerFactory
|
||||
{
|
||||
builder.Services.AddTransient<IHttpMessageInvokerFactory, T>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a custom HttpTransformerFactory to DI
|
||||
/// </summary>
|
||||
/// <param name="builder"></param>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public static BffBuilder AddHttpTransformerFactory<T>(this BffBuilder builder)
|
||||
where T : class, IHttpTransformerFactory
|
||||
{
|
||||
builder.Services.AddTransient<IHttpTransformerFactory, T>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|||
/// <param name="endpoints"></param>
|
||||
/// <param name="localPath"></param>
|
||||
/// <param name="apiAddress"></param>
|
||||
/// <param name="yarpTransformBuilder"></param>
|
||||
/// <returns></returns>
|
||||
public static IEndpointConventionBuilder MapRemoteBffApiEndpoint(
|
||||
this IEndpointRouteBuilder endpoints,
|
||||
PathString localPath,
|
||||
string apiAddress)
|
||||
PathString localPath,
|
||||
string apiAddress,
|
||||
Action<TransformBuilderContext>? 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<BffYarpTransformBuilder>()
|
||||
// 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());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
33
bff/src/Duende.Bff.Yarp/DefaultBffYarpTransformerBuilders.cs
Normal file
33
bff/src/Duende.Bff.Yarp/DefaultBffYarpTransformerBuilders.cs
Normal file
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Contains the default transformer logic for YARP BFF endpoints.
|
||||
/// </summary>
|
||||
public static class DefaultBffYarpTransformerBuilders
|
||||
{
|
||||
/// <summary>
|
||||
/// 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 <see cref="BffRemoteApiEndpointMetadata"/>.
|
||||
/// </summary>
|
||||
public static BffYarpTransformBuilder DirectProxyWithAccessToken =
|
||||
(string localPath, TransformBuilderContext context) =>
|
||||
{
|
||||
context.AddRequestHeaderRemove("Cookie");
|
||||
context.AddPathRemovePrefix(localPath);
|
||||
context.AddBffAccessToken(localPath);
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delegate for pipeline transformers.
|
||||
/// </summary>
|
||||
/// <param name="localPath">The local path that should be proxied. This path will be removed from the proxied request. </param>
|
||||
/// <param name="context">The transform builder context</param>
|
||||
public delegate void BffYarpTransformBuilder(string localPath, TransformBuilderContext context);
|
||||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of the message invoker factory.
|
||||
/// This implementation creates one message invoker per remote API endpoint.
|
||||
/// </summary>
|
||||
public class DefaultHttpMessageInvokerFactory : IHttpMessageInvokerFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Dictionary to cache invoker instances
|
||||
/// </summary>
|
||||
protected readonly ConcurrentDictionary<string, HttpMessageInvoker> Clients = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual HttpMessageInvoker CreateClient(string localPath)
|
||||
{
|
||||
return Clients.GetOrAdd(localPath, (key) =>
|
||||
{
|
||||
var handler = CreateHandler(key);
|
||||
return new HttpMessageInvoker(handler);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the HTTP message handler
|
||||
/// </summary>
|
||||
/// <param name="localPath"></param>
|
||||
/// <returns></returns>
|
||||
protected virtual HttpMessageHandler CreateHandler(string localPath)
|
||||
{
|
||||
return new SocketsHttpHandler
|
||||
{
|
||||
UseProxy = false,
|
||||
AllowAutoRedirect = false,
|
||||
AutomaticDecompression = DecompressionMethods.None,
|
||||
UseCookies = false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Default HTTP transformer implementation
|
||||
/// </summary>
|
||||
public class DefaultHttpTransformerFactory : IHttpTransformerFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// The options
|
||||
/// </summary>
|
||||
protected readonly BffOptions Options;
|
||||
|
||||
/// <summary>
|
||||
/// The YARP transform builder
|
||||
/// </summary>
|
||||
protected readonly ITransformBuilder TransformBuilder;
|
||||
|
||||
/// <summary>
|
||||
/// The DPoP Proof service
|
||||
/// </summary>
|
||||
protected readonly IDPoPProofService ProofService;
|
||||
|
||||
/// <summary>
|
||||
/// The logger factory
|
||||
/// </summary>
|
||||
protected readonly ILoggerFactory LoggerFactory;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="options">The BFF options</param>
|
||||
/// <param name="transformBuilder">The YARP transform builder</param>
|
||||
/// <param name="proofService"></param>
|
||||
/// <param name="loggerFactory"></param>
|
||||
public DefaultHttpTransformerFactory(
|
||||
IOptions<BffOptions> options,
|
||||
ITransformBuilder transformBuilder,
|
||||
IDPoPProofService proofService,
|
||||
ILoggerFactory loggerFactory)
|
||||
{
|
||||
Options = options.Value;
|
||||
TransformBuilder = transformBuilder;
|
||||
ProofService = proofService;
|
||||
LoggerFactory = loggerFactory;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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<AccessTokenRequestTransform>(),
|
||||
accessToken));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating a HTTP message invoker for outgoing remote BFF API calls
|
||||
/// </summary>
|
||||
public interface IHttpMessageInvokerFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a message invoker based on the local path
|
||||
/// </summary>
|
||||
/// <param name="localPath">Local path the remote API is mapped to</param>
|
||||
/// <returns></returns>
|
||||
HttpMessageInvoker CreateClient(string localPath);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating a HTTP transformer for outgoing remote BFF API calls
|
||||
/// </summary>
|
||||
public interface IHttpTransformerFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a HTTP transformer based on the local path
|
||||
/// </summary>
|
||||
/// <param name="localPath">Local path the remote API is mapped to</param>
|
||||
/// <param name="accessToken">The access token to attach to the request (if present)</param>
|
||||
/// <returns></returns>
|
||||
HttpTransformer CreateTransformer(string localPath, AccessTokenResult accessToken);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Remote BFF API endpoint
|
||||
/// </summary>
|
||||
public static class RemoteApiEndpoint
|
||||
{
|
||||
/// <summary>
|
||||
/// Endpoint logic
|
||||
/// </summary>
|
||||
/// <param name="localPath">The local path (e.g. /api)</param>
|
||||
/// <param name="apiAddress">The remote address (e.g. https://api.myapp.com/foo)</param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
public static RequestDelegate Map(string localPath, string apiAddress)
|
||||
{
|
||||
return async context =>
|
||||
{
|
||||
var loggerFactory = context.RequestServices.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger(LogCategories.RemoteApiEndpoints);
|
||||
|
||||
var endpoint = context.GetEndpoint();
|
||||
if (endpoint == null)
|
||||
{
|
||||
throw new InvalidOperationException("endpoint not found");
|
||||
}
|
||||
|
||||
var metadata = endpoint.Metadata.GetMetadata<BffRemoteApiEndpointMetadata>();
|
||||
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<IHttpForwarder>();
|
||||
var clientFactory = context.RequestServices.GetRequiredService<IHttpMessageInvokerFactory>();
|
||||
var transformerFactory = context.RequestServices.GetRequiredService<IHttpTransformerFactory>();
|
||||
|
||||
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<IForwarderErrorFeature>();
|
||||
if (errorFeature != null)
|
||||
{
|
||||
var error = errorFeature.Error;
|
||||
var exception = errorFeature.Exception;
|
||||
|
||||
logger.ProxyResponseError(localPath, exception?.ToString() ?? error.ToString());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
32
bff/src/Duende.Bff.Yarp/YarpTransformExtensions.cs
Normal file
32
bff/src/Duende.Bff.Yarp/YarpTransformExtensions.cs
Normal file
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for YARP transforms
|
||||
/// </summary>
|
||||
public static class YarpTransformExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the transform which will request an access token for the proxied request.
|
||||
/// </summary>
|
||||
public static TransformBuilderContext AddBffAccessToken(this TransformBuilderContext context, PathString localPath)
|
||||
{
|
||||
var proofService = context.Services.GetRequiredService<IDPoPProofService>();
|
||||
var logger = context.Services.GetRequiredService<ILogger<AccessTokenRequestTransform>>();
|
||||
context.RequestTransforms.Add(
|
||||
new AccessTokenRequestTransform(
|
||||
proofService,
|
||||
logger
|
||||
));
|
||||
return context;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 class BffEndpointRouteBuilderExtensions
|
|||
}
|
||||
|
||||
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<ILoggerFactory>();
|
||||
var options = endpoints.ServiceProvider.GetRequiredService<IOptions<BffOptions>>().Value;
|
||||
|
||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var options = serviceProvider.GetRequiredService<IOptions<BffOptions>>().Value;
|
||||
|
||||
LicenseValidator.Initalize(loggerFactory, options);
|
||||
LicenseValidator.ValidateLicense();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
/// </summary>
|
||||
public HttpContext HttpContext { get; set; } = default!;
|
||||
public required HttpContext HttpContext { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Metadata that describes the remote API.
|
||||
/// </summary>
|
||||
public BffRemoteApiEndpointMetadata Metadata { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The locally requested path.
|
||||
/// </summary>
|
||||
public string LocalPath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The remote address of the API.
|
||||
/// </summary>
|
||||
public string ApiAddress { get; set; } = string.Empty;
|
||||
public required BffRemoteApiEndpointMetadata Metadata { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional optional per request parameters for a user access token request.
|
||||
/// </summary>
|
||||
public UserTokenRequestParameters? UserTokenRequestParameters { get; set; }
|
||||
public required UserTokenRequestParameters? UserTokenRequestParameters { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The locally requested path.
|
||||
/// </summary>
|
||||
public required PathString LocalPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The remote address of the API.
|
||||
/// </summary>
|
||||
public required Uri ApiAddress { get; set; }
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ namespace Duende.Bff.Tests.Endpoints
|
|||
{
|
||||
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 @@ namespace Duende.Bff.Tests.Endpoints
|
|||
{
|
||||
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 @@ namespace Duende.Bff.Tests.Endpoints
|
|||
[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 @@ namespace Duende.Bff.Tests.Endpoints
|
|||
{
|
||||
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"))
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ namespace Duende.Bff.Tests.Endpoints
|
|||
{
|
||||
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 @@ namespace Duende.Bff.Tests.Endpoints
|
|||
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 @@ namespace Duende.Bff.Tests.Endpoints
|
|||
{
|
||||
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 @@ namespace Duende.Bff.Tests.Endpoints
|
|||
{
|
||||
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 @@ namespace Duende.Bff.Tests.Endpoints
|
|||
{
|
||||
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 @@ namespace Duende.Bff.Tests.Endpoints
|
|||
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 @@ namespace Duende.Bff.Tests.Endpoints
|
|||
{
|
||||
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 @@ namespace Duende.Bff.Tests.Endpoints
|
|||
{
|
||||
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 @@ namespace Duende.Bff.Tests.Endpoints
|
|||
{
|
||||
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 @@ namespace Duende.Bff.Tests.Endpoints
|
|||
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 @@ namespace Duende.Bff.Tests.Endpoints
|
|||
{
|
||||
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 @@ namespace Duende.Bff.Tests.Endpoints
|
|||
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 @@ namespace Duende.Bff.Tests.Endpoints
|
|||
{
|
||||
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 @@ namespace Duende.Bff.Tests.Endpoints
|
|||
{
|
||||
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 @@ namespace Duende.Bff.Tests.Endpoints
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task calls_to_endpoint_without_bff_metadata_should_fail()
|
||||
public async Task endpoint_can_be_configured_with_custom_transform()
|
||||
{
|
||||
Func<Task> f = () => BffHost.BrowserClient.CallBffHostApi(
|
||||
url: BffHost.Url("/not_bff_endpoint")
|
||||
);
|
||||
await f.ShouldThrowAsync<Exception>();
|
||||
}
|
||||
await BffHost.BffLoginAsync("alice");
|
||||
|
||||
[Fact]
|
||||
public async Task calls_to_bff_not_in_endpoint_routing_should_fail()
|
||||
{
|
||||
Func<Task> f = () => BffHost.BrowserClient.CallBffHostApi(
|
||||
url: BffHost.Url("/invalid_endpoint/test")
|
||||
);
|
||||
await f.ShouldThrowAsync<Exception>();
|
||||
}
|
||||
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<ApiResponse>(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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ using Duende.Bff.Tests.TestHosts;
|
|||
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 @@ namespace Duende.Bff.Tests.Endpoints
|
|||
[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 @@ namespace Duende.Bff.Tests.Endpoints
|
|||
[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 @@ namespace Duende.Bff.Tests.Endpoints
|
|||
[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 @@ namespace Duende.Bff.Tests.Endpoints
|
|||
[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 @@ namespace Duende.Bff.Tests.Endpoints
|
|||
[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 @@ namespace Duende.Bff.Tests.Endpoints
|
|||
[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 @@ namespace Duende.Bff.Tests.Endpoints
|
|||
[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 @@ namespace Duende.Bff.Tests.Endpoints
|
|||
[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 @@ namespace Duende.Bff.Tests.Endpoints
|
|||
[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 @@ namespace Duende.Bff.Tests.Endpoints
|
|||
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 @@ namespace Duende.Bff.Tests.Endpoints
|
|||
}
|
||||
|
||||
{
|
||||
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 @@ namespace Duende.Bff.Tests.Endpoints
|
|||
[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 @@ namespace Duende.Bff.Tests.Endpoints
|
|||
[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 @@ namespace Duende.Bff.Tests.Endpoints
|
|||
[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
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// These tests prove that you can use a custom IAccessTokenRetriever and that the context is populated correctly.
|
||||
/// </summary>
|
||||
public class IAccessTokenRetriever_Extensibility_tests : BffIntegrationTestBase
|
||||
{
|
||||
|
||||
private ContextCapturingAccessTokenRetriever _customAccessTokenReceiver { get; } = new(NullLogger<DefaultAccessTokenRetriever>.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<ContextCapturingAccessTokenRetriever>();
|
||||
|
||||
});
|
||||
|
||||
app.Map("/subPath",
|
||||
subPath =>
|
||||
{
|
||||
subPath.UseRouting();
|
||||
subPath.UseEndpoints((endpoints) =>
|
||||
{
|
||||
endpoints.MapRemoteBffApiEndpoint("/custom_within_subpath", ApiHost.Url("/some/path"))
|
||||
.RequireAccessToken()
|
||||
.WithAccessTokenRetriever<ContextCapturingAccessTokenRetriever>();
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
[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");
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures the context in which the access token retriever is called, so we can assert on it
|
||||
/// </summary>
|
||||
private class ContextCapturingAccessTokenRetriever : DefaultAccessTokenRetriever
|
||||
{
|
||||
public AccessTokenRetrievalContext? UsedContext { get; private set; }
|
||||
public ContextCapturingAccessTokenRetriever(ILogger<DefaultAccessTokenRetriever> logger) : base(logger)
|
||||
{
|
||||
}
|
||||
|
||||
public override Task<AccessTokenResult> GetAccessToken(AccessTokenRetrievalContext context)
|
||||
{
|
||||
UsedContext = context;
|
||||
return base.GetAccessToken(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -86,7 +86,7 @@ public class TestBrowserClient : HttpClient
|
|||
/// <param name="expectedStatusCode">If specified, the system will verify that this reponse code was given</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns>The specified api response</returns>
|
||||
public async Task<ApiResponse> CallBffHostApi(
|
||||
public async Task<BffHostResponse> CallBffHostApi(
|
||||
string url,
|
||||
HttpStatusCode? expectedStatusCode = null,
|
||||
CancellationToken ct = default)
|
||||
|
|
@ -104,17 +104,17 @@ public class TestBrowserClient : HttpClient
|
|||
|
||||
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<ApiResponse> CallBffHostApi(
|
||||
public async Task<BffHostResponse> CallBffHostApi(
|
||||
string url,
|
||||
HttpMethod method,
|
||||
HttpContent? content = null,
|
||||
|
|
@ -136,15 +136,20 @@ public class TestBrowserClient : HttpClient
|
|||
var apiResult = JsonSerializer.Deserialize<ApiResponse>(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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,10 @@ using System.Threading.Tasks;
|
|||
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 @@ public class BffHost : GenericHost
|
|||
BffOptions = options;
|
||||
});
|
||||
|
||||
services.AddSingleton<IHttpMessageInvokerFactory>(
|
||||
services.AddSingleton<IForwarderHttpClientFactory>(
|
||||
new CallbackHttpMessageInvokerFactory(
|
||||
path => new HttpMessageInvoker(_apiHost.Server.CreateHandler())));
|
||||
() => new HttpMessageInvoker(_apiHost.Server.CreateHandler())));
|
||||
|
||||
services.AddAuthentication("cookie")
|
||||
.AddCookie("cookie", options =>
|
||||
|
|
@ -71,6 +75,8 @@ public class BffHost : GenericHost
|
|||
options.Cookie.Name = "bff";
|
||||
});
|
||||
|
||||
services.AddSingleton<BffYarpTransformBuilder>(CustomDefaultBffTransformBuilder);
|
||||
|
||||
bff.AddServerSideSessions();
|
||||
bff.AddRemoteApis();
|
||||
|
||||
|
|
@ -118,6 +124,13 @@ public class BffHost : GenericHost
|
|||
=> 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 @@ public class BffHost : GenericHost
|
|||
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 @@ public class BffHost : GenericHost
|
|||
"/api_with_access_token_retrieval_that_fails", _apiHost.Url())
|
||||
.RequireAccessToken(TokenType.UserOrClient)
|
||||
.WithAccessTokenRetriever<FailureAccessTokenRetriever>();
|
||||
|
||||
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<bool> GetIsUserLoggedInAsync(string? userQuery = null)
|
||||
|
|
@ -519,13 +535,18 @@ public class BffHost : GenericHost
|
|||
return response;
|
||||
}
|
||||
|
||||
private class CallbackHttpMessageInvokerFactory(Func<string, HttpMessageInvoker> callback)
|
||||
: IHttpMessageInvokerFactory
|
||||
public class CallbackHttpMessageInvokerFactory : IForwarderHttpClientFactory
|
||||
{
|
||||
|
||||
public HttpMessageInvoker CreateClient(string localPath)
|
||||
public CallbackHttpMessageInvokerFactory(Func<HttpMessageInvoker> callback)
|
||||
{
|
||||
return callback.Invoke(localPath);
|
||||
CreateInvoker = callback;
|
||||
}
|
||||
|
||||
public Func<HttpMessageInvoker> CreateInvoker { get; set; }
|
||||
|
||||
public HttpMessageInvoker CreateClient(ForwarderHttpClientContext context)
|
||||
{
|
||||
return CreateInvoker.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 Duende.Bff.Yarp;
|
|||
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 @@ namespace Duende.Bff.Tests.TestHosts
|
|||
BffOptions = options;
|
||||
});
|
||||
|
||||
services.AddSingleton<IHttpMessageInvokerFactory>(
|
||||
new CallbackHttpMessageInvokerFactory(
|
||||
path => new HttpMessageInvoker(_apiHost.Server.CreateHandler())));
|
||||
services.AddSingleton<IForwarderHttpClientFactory>(
|
||||
new BackChannelHttpMessageInvokerFactory(_apiHost.Server.CreateHandler()));
|
||||
|
||||
services.AddAuthentication("cookie")
|
||||
.AddCookie("cookie", options =>
|
||||
|
|
@ -159,9 +158,6 @@ namespace Duende.Bff.Tests.TestHosts
|
|||
.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<bool> GetIsUserLoggedInAsync(string? userQuery = null)
|
||||
|
|
@ -226,20 +222,12 @@ namespace Duende.Bff.Tests.TestHosts
|
|||
response = await BrowserClient.GetAsync(Url(response.Headers.Location.ToString()));
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
public class CallbackHttpMessageInvokerFactory : IHttpMessageInvokerFactory
|
||||
{
|
||||
public CallbackHttpMessageInvokerFactory(Func<string, HttpMessageInvoker> callback)
|
||||
{
|
||||
CreateInvoker = callback;
|
||||
}
|
||||
|
||||
public Func<string, HttpMessageInvoker> 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -245,31 +245,6 @@ public class YarpBffHost : GenericHost
|
|||
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());
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ namespace Duende.Bff.Tests.TestHosts
|
|||
{
|
||||
private readonly IdentityServerHost _identityServerHost;
|
||||
protected readonly ApiHost ApiHost;
|
||||
protected readonly YarpBffHost BffHost;
|
||||
protected readonly YarpBffHost YarpBasedBffHost;
|
||||
private BffHostUsingResourceNamedTokens _bffHostWithNamedTokens;
|
||||
|
||||
protected YarpBffIntegrationTestBase(ITestOutputHelper output) : base(output)
|
||||
|
|
@ -40,15 +40,14 @@ namespace Duende.Bff.Tests.TestHosts
|
|||
_identityServerHost.OnConfigureServices += services => {
|
||||
services.AddTransient<IBackChannelLogoutHttpClient>(provider =>
|
||||
new DefaultBackChannelLogoutHttpClient(
|
||||
BffHost!.HttpClient,
|
||||
YarpBasedBffHost!.HttpClient,
|
||||
provider.GetRequiredService<ILoggerFactory>(),
|
||||
provider.GetRequiredService<ICancellationTokenProvider>()));
|
||||
};
|
||||
|
||||
ApiHost = new ApiHost(WriteLine, _identityServerHost, "scope1");
|
||||
|
||||
BffHost = new YarpBffHost(WriteLine, _identityServerHost, ApiHost, "spa");
|
||||
|
||||
YarpBasedBffHost = new YarpBffHost(output.WriteLine, _identityServerHost, ApiHost, "spa");
|
||||
_bffHostWithNamedTokens = new BffHostUsingResourceNamedTokens(WriteLine, _identityServerHost, ApiHost, "spa");
|
||||
}
|
||||
|
||||
|
|
@ -61,7 +60,7 @@ namespace Duende.Bff.Tests.TestHosts
|
|||
{
|
||||
await _identityServerHost.InitializeAsync();
|
||||
await ApiHost.InitializeAsync();
|
||||
await BffHost.InitializeAsync();
|
||||
await YarpBasedBffHost.InitializeAsync();
|
||||
await _bffHostWithNamedTokens.InitializeAsync();
|
||||
await base.InitializeAsync();
|
||||
}
|
||||
|
|
@ -70,7 +69,7 @@ namespace Duende.Bff.Tests.TestHosts
|
|||
{
|
||||
await _identityServerHost.DisposeAsync();
|
||||
await ApiHost.DisposeAsync();
|
||||
await BffHost.DisposeAsync();
|
||||
await YarpBasedBffHost.DisposeAsync();
|
||||
await _bffHostWithNamedTokens.DisposeAsync();
|
||||
await base.DisposeAsync();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue