No tokens in the browser implementation inspired by Duende BFF and .NET 6 RC1

I recently started to investigate what kind of BFF (backend-for-frontend) options are available if you want to apply the "no tokens in the browser" policy for your application. Backend for frontend is not a new thing but nowadays it's the recommended way to keep tokens out of browser context. This blog post has general information about the BFF pattern and shows how to create a SPA application that follows the "no tokens in the browser" policy. BFF implementation is built on the .NET 6 RC1 application with Duende BFF.

Why do we need BFF?

Currently, most of the SPA applications are built in a way where tokens (access & refresh) are persisted in the browser. Typically tokens are stored in a session storage which exposes tokens to vulnerabilities and malicious code. Identity & access management and security specialized company called Curity has summarized the problems of the SPA like this:

"Currently, SPAs have no means of keeping access and refresh tokens secure from malicious code. Even if developers attempt to protect their apps from XSS attacks (as they should), such an attack can still occur through a vulnerability in a third-party library. The only way to protect tokens from being accessed by any malicious code is to keep them away from the browser". Source: The Token Handler Pattern for Single Page Applications

Curity and Duende recommend BFF implementation for all SPA applications.

What is BFF in shortly?

BFF is an intermediate layer (reverse proxy) between your SPA frontend and API services. BFF enables the handling of the tokens and communication to the API services is handled in the backend. BFF layer is protected with cookie-based authentication. This approach enables that tokens are not required to persist in the browser.

Traditional SPA architecture

SPA architecture with BFF

Read also these articles

What is Duende BFF?

Duende BFF is a BFF (backend-for-frontend) library that is created by Duende (creators of the Identity Server). Duende BFF does all the magic behind the scenes like cookie/session management, encryption of the cookies, reverse proxy functionality, endpoints for login, logout, and returning user information. You can find the source code of the Duende BFF component from here. Security is hard so utilizing the ready-made component is highly recommended.

When utilizing Duende BFF you should know the following things related to licensing:

"Duende BFF will be part of the Duende IdentityServer license. It will be included either in our Business (and up), or Community Edition.

if you as an individual or your company make less than one million USD revenue per year, you can use Duende BFF absolutely free of cost. Since this also includes Duende IdentityServer, you can protect up to five SPAs with the free license.

If you make more than one million USD in the revenue per year - you can get Duende BFF as part of our Business Edition which also includes 15 clients for IdentityServer."

Currently, Duende BFF is in the release candidate 1 phase and the expected release date is in May 2022. Duende BFF utilizes YARP (Yet another reverse proxy) provided by Microsoft which is also in technical review.

Weather Forecast App with Duende BFF

This Weather Forecast App sample has got inspiration from Duende BFF samples. This sample uses the .NET 6 RC1 framework and Minimal API implementation.

Quick overview of the architecture and technology:

Weather Forecast App

The Weather Forecast App project is created with the "Visual Studio ASP.NET Core with React.js" template and it utilizes .NET 6 RC1. Duende BFF is added to the project as a Nuget package. Also, the Duende BFF YARP Nuget package is added to enable reverse proxying.

The following steps are required to enable Duende BFF (program.cs):

These code lines add BFF to the DI pipeline. Remember also to add AddRemoteApis() to get remote api reverse proxying working!

builder.Services.AddBff()
    .AddRemoteApis() //enable remote apis
    .AddServerSideSessions();

Lastly, enable BFF management endpoints and configure reverse proxy rules by passing the bearer (access) token.

app.UseBff();
app.UseEndpoints(endpoints =>
{
    // login, logout, user, backchannel logout...
    endpoints.MapBffManagementEndpoints();
    // reverse proxy configuration
    endpoints.MapRemoteBffApiEndpoint("/weatherforecast", "https://localhost:7291/WeatherForecast").RequireAccessToken(TokenType.User);
});

Full source code of the program.cs:

using Duende.Bff;
using Duende.Bff.Yarp;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddBff()
    .AddRemoteApis() //enable remote apis
    .AddServerSideSessions();

builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = "cookie";
    options.DefaultChallengeScheme = "oidc";
    options.DefaultSignOutScheme = "oidc";
})
.AddCookie("cookie", options =>
{
    options.Cookie.Name = "__Host-bff";
    options.Cookie.SameSite = SameSiteMode.Strict;
})
.AddOpenIdConnect("oidc", options =>
{
    options.Authority = "https://demo.duendesoftware.com";
    options.ClientId = "interactive.confidential";
    options.ClientSecret = "secret";
    options.ResponseType = "code";
    options.ResponseMode = "query";
    options.GetClaimsFromUserInfoEndpoint = true;
    options.MapInboundClaims = false;
    options.SaveTokens = true;
    options.Scope.Clear();
    options.Scope.Add("openid");
    options.Scope.Add("profile");
    options.Scope.Add("api");
    options.Scope.Add("offline_access");
    options.TokenValidationParameters = new()
    {
        NameClaimType = "name",
        RoleClaimType = "role"
    };
});

var app = builder.Build();

// Configure the HTTP request 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();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseBff();

app.UseEndpoints(endpoints =>
{
    // login, logout, user, backchannel logout...
    endpoints.MapBffManagementEndpoints();
    // reverse proxy configuration
    endpoints.MapRemoteBffApiEndpoint("/weatherforecast", "https://localhost:7291/WeatherForecast").RequireAccessToken(TokenType.User);
});

app.Run();

Weather Forecast API with authorization

Weather Forecast API uses the Minimal API Framework which was introduced in .NET 6. This Weather Forecast API endpoint requires authorization (bearer token issued by Demo Duende Server). Minimal API enables to creation of very compact API endpoints. 

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.IdentityModel.Tokens;
using WeatherForecastApi;

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddJsonFile("appsettings.json");

builder.Services.AddAuthentication("token")
    .AddJwtBearer("token", options =>
    {
        options.Authority = builder.Configuration["Authority"];
        options.MapInboundClaims = false;
        options.TokenValidationParameters = new TokenValidationParameters()
        {
            ValidateAudience = false,
            ValidTypes = new[] { "at+jwt" },
            NameClaimType = "name",
            RoleClaimType = "role"
        };
    });

builder.Services.AddAuthorization();

var app = builder.Build();

app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost,
});

app.UseAuthentication();
app.UseAuthorization();
app.UseHttpsRedirection();

app.MapGet("/WeatherForecast", [Authorize]() =>
{
    string[] summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };
    return Enumerable.Range(1, 5).Select(index => new WeatherForecast
    {
        Date = DateTime.Now.AddDays(index),
        TemperatureC = Random.Shared.Next(-20, 55),
        Summary = summaries[Random.Shared.Next(summaries.Length)]
    });
});

app.Run();

The full source code of this sample can be found here.

Utilization and configuration of Duende BFF was very easy and Duende has provided many good BFF samples to GitHub. I also noticed that Curity also provides Node.js-based BFF sample (PoC) in their GitHub repository. It's good that there are ready productized solutions coming for BFF implementation.

If you're using Duende Identity Server (Business or Enterprise edition) and you're considering securing a SPA application with a BFF pattern you should definitely check Duende BFF!

Comments