Menu

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 "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 BFF pattern and shows how to create a SPA application which follows "no tokens in the browser" policy. BFF implementation is built on .NET 6 RC1 application with Duende BFF.

Why we need BFF?

Currently most of the SPA applications are built on 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 recommends BFF implementation for all SPA applications.

What is BFF in shortly?

BFF is a intermediate layer (reverse proxy) between your SPA frontend and API services. BFF enables that handling of the tokens and communication to the API services are handled in 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

undefined

SPA architecture with BFF

undefined

Read also these articles

The BFF Pattern (Backend for Frontend): An Introduction | by Viduni Wickramarachchi | Bits and Pieces (bitsrc.io)
Backends for Frontends pattern - Cloud Design Patterns | Microsoft Docs

What is Duende BFF?

Duende BFF is a BFF (backend-for-frontend) library which 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 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 makes 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 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 release candidate 1 phase and 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 .NET 6 RC1 framework and Minimal API implementation.

Quick overview to the architecture and technology:

undefined

Weather Forecast App

Weather Forecast App project is created with "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 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 with 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 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 create 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();

Full source code of this sample can be found from 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 to secure SPA application with BFF pattern you should definitely check Duende BFF! 

Comments