How to implement request routing for BFF with YARP
A few months ago I wrote a blog post that illustrated the usage of the Duende BFF component. Duende BFF handles all Backend for Frontend responsibilities behind the scenes automatically. In this blog post, I'm concentrating more on to reverse proxy side of how to re-route requests to the destination API endpoint via BFF.
Microsoft has created an open-sourced project called YARP (Yet-Another-Reverse-Proxy) which is a highly customizable reverse proxy and more than suitable for BFF request routing purposes.
Request routing in the Backend for Frontend scenarios
A reverse proxy is used to re-route requests from the frontend application via BFF to the destination API endpoint. The BFF layer is protected with cookie-based authentication so "No tokens in browser" can be applied. Basically reverse proxy functionality of the BFF layer extracts the access token from the cookie and passes it further to the destination API endpoint.
Reverse proxy in the BFF layer
From the BFF's request routing point of view, the most important questions are related to the following topics:
1) How to re-route the request to the destination API?
2) How to protect the proxy endpoint with authorization?
3) How to extract the access token from the cookie and pass it further to the destination API?
YARP middleware in the ASP.NET middleware pipeline
YARP is added to the ASP.NET pipeline for handling incoming requests. It's just one new middleware in the pipeline.
YARP features
A short summary of YARP features:
- Routing rules (source and destination) can be configured easily in the configuration file (appsettings.json). You can do this also programmatically if you like.
- Simple transformations ex. adding new headers can be determined directly into the configuration file so code changes are not necessarily required.
- Complex transformation logic can be created in code.
- YARP utilizes ASP.NET authorization policies. The usage of the authorization policy can be configured directly to the configuration file.
- YARP ships with built-in load-balancing algorithms, but also offers extensibility for any custom load-balancing approach.
- You can inject custom middleware into the proxy pipeline.
- Health Check support
Check the complete list of YARP features here.
How to use YARP in the BFF layer?
These steps have answers to the questions which was stated earlier.
1. Install Yarp.ReverseProxy nuget packet to your project
Install-Package Yarp.ReverseProxy -ProjectName WeatherForecastApp
2. Configure re-routing rules
Reverse proxy rules can be easily configured in the appsettings file or programmatically. Configuration has two main elements: Routes and Clusters. More detailed configuration information can be found here.
This configuration re-routes all requests from /weatherforecast to https://localhost:7291
"ReverseProxy": {
"Routes": {
"route1": {
"ClusterId": "weatherForecastApi",
"AuthorizationPolicy": "RequireAuthenticatedUserPolicy",
"Match": {
"Path": "/weatherforecast/{**catch-all}"
},
"AllowAnonymous": false
}
},
"Clusters": {
"weatherForecastApi": {
"Destinations": {
"weatherForecastApi/destination1": {
"Address": "https://localhost:7291"
}
}
}
}
}
Term | Description |
Routes |
The routes section is an ordered list of route matches and their associated configuration. Match contains either a Hosts array or a Path pattern string. Path is an ASP.NET route template. In this case rule catches all requests from path /weatherforecast. AuthorizationPolicy determines which ASP.NET authorization policy is required to fulfill. |
Clusters | The clusters section is an unordered collection of named clusters. A cluster primarily contains a collection of named destinations and their addresses, any of which is considered capable of handling requests for a given route. |
3. Add YARP to the ASP.NET middleware pipeline
This service collection extension configures the proxy.
public static void AddProxy(this IServiceCollection services, ConfigurationManager configuration)
{
var reverseProxyConfig = configuration.GetSection("ReverseProxy") ?? throw new ArgumentException("ReverseProxy section is missing!");
services.AddReverseProxy()
.LoadFromConfig(reverseProxyConfig)
.AddTransforms(builderContext =>
{
builderContext.AddRequestTransform(async transformContext =>
{
var accessToken = await transformContext.HttpContext.GetTokenAsync("access_token");
if(accessToken != null)
{
transformContext.ProxyRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
}
});
});
}
Method | Description |
LoadFromConfig | Loads routing rules from the appsettings file which was shown above. |
AddTransforms |
Enables that request can modified before it's forwarded to the destination. Responses can be also modified. More information about transformation capabilities can be found from here. In this BFF case request transformation is used to extract access token (=bearer) from the cookie and attached it to proxyed request. |
4. Add authentication
This service collection extension configures cookie-based authentication (OIDC). OpenId Connect settings are dynamically bound from the appsettings file.
public static void AddAuthentication(this IServiceCollection services, ConfigurationManager configuration, OpenIdConnectEvents events = null)
{
var authenticationConfig = configuration.GetSection("Authentication") ?? throw new ArgumentException("Authentication section is missing!");
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
options.DefaultSignOutScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.Cookie.Name = authenticationConfig["CookieName"] ?? "__BFF";
options.Cookie.SameSite = SameSiteMode.Strict;
options.Cookie.HttpOnly = false;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
})
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
// Bind OIDC authentication configuration
configuration.GetSection("Authentication").Bind(options);
options.Events = events;
// Add dynamically scope values from configuration
var scopes = configuration.GetSection("Authentication")["Scope"].Split(" ").ToList();
options.Scope.Clear();
scopes.ForEach(scope => options.Scope.Add(scope));
options.TokenValidationParameters = new()
{
NameClaimType = "name",
RoleClaimType = "role"
};
});
}
Authentication settings are dynamically bound from the following app setting section:
"Authentication": {
"Authority": "https://demo.duendesoftware.com",
"CookieName": "__BFF",
"ClientId": "interactive.confidential",
"ClientSecret": "secret",
"ResponseType": "code",
"ResponseMode": "query",
"GetClaimsFromUserInfoEndpoint": true,
"MapInboundClaims": false,
"SaveTokens": true,
"Scope": "openid profile api offline_access"
}
5. Add authorization
This service collection extension adds an authorization policy which is referred in the reverse proxy configuration.
public static void AddAuthorizationPolicies(this IServiceCollection services)
{
services.AddAuthorization(options =>
{
// This is a default authorization policy which requires authentication
options.AddPolicy("RequireAuthenticatedUserPolicy", policy =>
{
policy.RequireAuthenticatedUser();
});
});
}
Full configuration
This follows the above chart which shows middleware execution in the pipeline.
var builder = WebApplication.CreateBuilder(args);
// 1. Add reverse proxy configuration (YARP)
builder.Services.AddReverseProxy(builder.Configuration);
// 2. Add authentication configuration
builder.Services.AddAuthentication(builder.Configuration);
// 3. Add authorization configuration
builder.Services.AddAuthorizationPolicies();
var app = builder.Build();
// 4. Enable exception handler middleware in pipeline
app.UseExceptionHandler("/?error");
// 5. Enable HSTS middleware in pipeline
if (!app.Environment.IsDevelopment())
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
// 6. Enable https redirection middleware in pipeline
app.UseHttpsRedirection();
// 7. Enable routing middleware in pipeline
app.UseRouting();
// 8. Enable authentication middleware in pipeline
app.UseAuthentication();
// 9. Enable routing middleware in pipeline
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
// 10. Enable YARP reverse proxy middleware in pipeline
endpoints.MapReverseProxy();
// 11. Enable BFF login, logout and userinfo endpoints
endpoints.AddBffEndpoints();
});
//app.MapReverseProxy();
app.MapFallbackToFile("index.html"); ;
app.Run();
Comments