Menu

How to implement request routing for BFF with YARP

A few months ago I wrote a blog post which illustrated usage of Duende BFF component. Duende BFF handles all Backend for Frontend responsibilities behind the scenes automatically. In this blog post I'm concentrating more to reverse proxy side how to re-route requests to destination API endpoint via BFF.

Microsoft has created and open-sourced project called YARP (Yet-Another-Reverse-Proxy) which is highly customizable reverse proxy and more than suitable for BFF request routing purposes.

Request routing in Backend for Frontend scenarios

Reverse proxy is used to re-route requests from frontend application via BFF to destination API endpoint. BFF layer is protected with cookie based authentication so "No tokens in browser" can be applied. Basically reverse proxy functionality of BFF layer extracts access token from the cookie and passes it further to destination API endpoint.

Reverse proxy in the BFF layer

undefined

From the BFF's request routing point of view the most important questions are related to the following topics:

1) How to re-route request to destination API?

2) How to protect proxy endpoint with authorization?

3) How to extract access token from the cookie and pass it further to 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 a one new middleware in the pipeline.

undefined

YARP features

A short summary about 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 configuration file so code changes are not necessary required.
  • Complex transformation logic can be created in code. 
  • YARP utilizes ASP.NET authorization policies. Usage of 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 middlewares to the proxy pipeline.
  • Health Check support

Check complete list of YARP features from 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 information about configuration can be found from 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 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 binded 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 binded 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 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