YARP extensions
Duende.BFF integrates with Microsoft’s full-featured reverse proxy YARP.
YARP includes many advanced features such as load balancing, service discovery, and session affinity. It also has its
own extensibility mechanism. Duende.BFF includes YARP extensions for token management and anti-forgery protection so
that you can combine the security and identity features of Duende.BFF
with the flexible reverse proxy features of
YARP.
Adding YARP
Section titled “Adding YARP”To enable Duende.BFF’s YARP integration, add a reference to the Duende.BFF.Yarp NuGet package to your project and add YARP and the BFF’s YARP extensions to DI:
builder.Services.AddBff();
// adds YARP with BFF extensionsvar yarpBuilder = services.AddReverseProxy() .AddBffExtensions();
Configuring YARP
Section titled “Configuring YARP”YARP is most commonly configured by a config file. The following simple example forwards a local URL to a remote API:
{ "ReverseProxy": { "Routes": { "todos": { "ClusterId": "cluster1", "Match": { "Path": "/todos/{**catch-all}" } } }, "Clusters": { "cluster1": { "Destinations": { "destination1": { "Address": "https://API.mycompany.com/todos" } } } } }}
See the Microsoft documentation for the complete configuration schema.
Another option is to configure YARP in code using the in-memory config provider included in the BFF extensions for YARP. The above configuration as code would look like this:
yarpBuilder.LoadFromMemory( new[] { new RouteConfig() { RouteId = "todos", ClusterId = "cluster1",
Match = new() { Path = "/todos/{**catch-all}" } } }, new[] { new ClusterConfig { ClusterId = "cluster1",
Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase) { { "destination1", new() { Address = "https://API.mycompany.com/todos" } }, } } });
Token Management
Section titled “Token Management”Duende.BFF’s YARP extensions provide access token management and attach user or client access tokens automatically to proxied API calls. To enable this, add metadata with the name Duende.Bff.Yarp.TokenType to the route or cluster configuration:
{ "ReverseProxy": { "Routes": { "todos": { "ClusterId": "cluster1", "Match": { "Path": "/todos/{**catch-all}" }, "Metadata": { "Duende.Bff.Yarp.TokenType": "User" } } } }}
Similarly to the simple HTTP forwarder, the allowed values for the token type are User, Client, UserOrClient.
Routes that set the Duende.Bff.Yarp.TokenType metadata require the given type of access token. If it is unavailable (for example, if the User token type is specified but the request to the BFF is anonymous), then the proxied request will not be sent, and the BFF will return an HTTP 401: Unauthorized response.
If you are using the code config method, call the WithAccessToken extension method to achieve the same thing:
yarpBuilder.LoadFromMemory( new[] { new RouteConfig() { RouteId = "todos", ClusterId = "cluster1",
Match = new RouteMatch { Path = "/todos/{**catch-all}" } }.WithAccessToken(TokenType.User) }, // rest omitted);
Again, the WithAccessToken method causes the route to require the given type of access token. If it is unavailable, the proxied request will not be made and the BFF will return an HTTP 401: Unauthorized response.
Optional User Access Tokens
Section titled “Optional User Access Tokens”You can also attach user access tokens optionally by adding metadata named “Duende.Bff.Yarp.OptionalUserToken” to a YARP route.
{ "ReverseProxy": { "Routes": { "todos": { "ClusterId": "cluster1", "Match": { "Path": "/todos/{**catch-all}" }, "Metadata": { "Duende.Bff.Yarp.OptionalUserToken": "true" } } } }}
This metadata causes the user’s access token to be sent with the proxied request when the user is logged in, but makes the request anonymously when the user is not logged in. It is an error to set both Duende.Bff.Yarp.TokenType and Duende.Bff.Yarp.OptionalUserToken, since they have conflicting semantics (TokenType requires the token, OptionalUserToken makes it optional).
If you are using the code config method, call the WithOptionalUserAccessToken extension method to achieve the same thing:
yarpBuilder.LoadFromMemory( new[] { new RouteConfig() { RouteId = "todos", ClusterId = "cluster1",
Match = new RouteMatch { Path = "/todos/{**catch-all}" } }.WithOptionalUserAccessToken() }, // rest omitted);
Anti-forgery Protection
Section titled “Anti-forgery Protection”Duende.BFF’s YARP extensions can also add anti-forgery protection to proxied API calls. Anti-forgery protection defends against CSRF attacks by requiring a custom header on API endpoints, for example:
GET /endpoint
x-csrf: 1
The value of the header is not important, but its presence, combined with the cookie requirement, triggers a CORS preflight request for cross-origin calls. This effectively isolates the caller to the same origin as the backend, providing a robust security guarantee.
You can add the anti-forgery protection to all YARP routes by calling the AsBffApiEndpoint extension method:
app.MapReverseProxy() .AsBffApiEndpoint();
// or shorterapp.MapBffReverseProxy();
If you need more fine-grained control over which routes should enforce the anti-forgery header, you can also annotate the route configuration by adding the Duende.Bff.Yarp.AntiforgeryCheck metadata to the route config:
{ "ReverseProxy": { "Routes": { "todos": { "ClusterId": "cluster1", "Match": { "Path": "/todos/{**catch-all}" }, "Metadata": { "Duende.Bff.Yarp.AntiforgeryCheck": "true" } } } }}
This is also possible in code:
yarpBuilder.LoadFromMemory( new[] { new RouteConfig() { RouteId = "todos", ClusterId = "cluster1",
Match = new RouteMatch { Path = "/todos/{**catch-all}" } }.WithAntiforgeryCheck() }, // rest omitted);
To enforce the presence of the anti-forgery headers, you need to add a middleware to the YARP pipeline:
app.MapReverseProxy(proxyApp =>{ proxyApp.UseAntiforgeryCheck();});