Introducing the next era of Duende IdentityServer.

Read our CEO’s announcement

Secure a Vue app with OpenID Connect and the BFF pattern

A profile photo of Khalid Abuhakmeh in black and white
Khalid Abuhakmeh
Two blue circles

When building web applications, single-page framework applications have become one of the dominant forms of user
experience creation. Developers have many choices, including React, Angular, Vue, and many more. While plenty of
frameworks exist, only a few options exist for securing these applications.

At Duende, we recommend that developers adopt the Backend for Frontend (BFF) pattern to maintain a high-security posture
and protect their users and data from malicious attacks.

In this post, we’ll look at the basic architecture of a BFF solution, the responsibilities of each component, and how it
all fits together.

The Goal

The main objective of any security solution is to keep communication between components trusted and secured. In the case
of a modern solution, two logical elements will communicate: the front end and the back end. We want to ensure that
authenticated and authorized users can perform said actions when they act. Without the establishment of trust, sensitive
information may be accessible to unintended users.

The good news is that OAuth 2.x and OpenID Connect are standards that go hand-in-hand with this architecture. A user
will access an application, request an access token from an identity provider, and use that token to access a backing
API. The bad news is that single-page and client-side applications require a secure place to store the access token. The
browser's local storage, used by the oidc-client-js and oidc-client-ts JavaScript libraries, can be accessed by the
user using the Developer Tools (F12), making it easy to make requests to the API bypassing the intended application.

Even worse, any malicious JavaScript that makes its way into the SPA will have access to the access token too and can
make API calls impersonating the authenticated user, or just exfiltrate it altogether.

In the following sections, we’ll introduce a BFF proxy, a trusted intermediary between the front and back end. As you’ll
see, there are many advantages to using a BFF, and ultimately, it leads to successful and secure solution deployments.

The Backend For Frontend

What is a BFF? You can think of BFF in two crucial but related ways. First, BFF is an architectural pattern stating that
every browser-based application should have a complimentary server-side application that handles
all authentication requirements for the IETF definition of BFF.
Secondly, the BFF solution provided by Duende is an intermediary between the front and back end. It is a trusted party
in solution architecture and has several essential responsibilities:

  • Server-side session management
  • Server-side end-user authentication using OpenID Connect
  • Requesting and managing access tokens to access APIs
  • Cross-site Request Forgery attack protection
  • Securely calling APIs

How does the BFF carry out these responsibilities? Using production-tested security patterns and practices, of course.

When communicating with the frontend, the elements communicate using HTTPS and cookie-based sessions. First-party
cookies are the most trusted method of security in web development. A mix of HTTP-only cookies, expiration, CORS
protections, and Same-Site policies makes attacks by malicious parties very challenging.

Interactions happen within a trusted context on the server, so developers can apply a mix of OAuth 2.x, OpenID Connect,
and a library of security practices to ensure only trusted parties can communicate. Additionally, logging and auditing
techniques can be applied here to quickly track and shut down any compromised sessions before they are used to
exfiltrate user data. In general, you have much more control over a server-side environment than you can on a user’s
machine, where you generally have little to no influence.

Let’s take a look at the architecture of a BFF solution.

BFF solution architecture

In an ASP.NET Core host, adding a BFF is as straightforward as installing two NuGet packages.

Markup


<PackageReference Include="Duende.BFF" Version="3.0.0"/>
<PackageReference Include="Duende.BFF.Yarp" Version="3.0.0"/>

And then registering some ASP.NET Core infrastructure.

Csharp

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddBff().AddRemoteApis();

// Add Identity Provider
// ...

var app = builder.Build();

app.UseDefaultFiles();
app.UseStaticFiles();

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

app.MapRemoteBffApiEndpoint("/todos", "https://localhost:7001/todos")
    .RequireAccessToken(Duende.Bff.TokenType.User);

In the next section, we’ll examine a Vue example and learn how to create a component that interacts with Duende’s BFF
session management APIs.

The Frontend

Thanks to community member Marco Cabrera for providing the Vue sample code used in this
post.

The frontend SPA framework is up to you, but we’ll use Vue in this post. From the previous section, you may have noticed
a call to MapBffManagementEndponts. These endpoints allow any frontend code to log in or out or check on a user’s
active session.

Let’s start with the most straightforward action a frontend may want to initiate: a login.

Javascript

export function login() {
    window.location.href = '/bff/login';
}

That’s it! By redirecting a user to the login endpoint provided by our BFF, we will be redirected to the identity
provider login page, where a typical OpenID Connect login will occur and ultimately set a cookie.

Markup

<div v-else>
    <p>Please <a href="#" @click.prevent="login">Login</a> to view ToDos.</p>
</div>

Next, look at retrieving the user session and any associated claims.

Javascript

export const isAuthenticated = ref(false);
export const user = ref(null);

export async function checkAuthStatus() {
    try {
        const res = await fetch('/bff/user', {
            credentials: 'include',
            headers: {
                'X-CSRF': '1'
            }
        });

        if (res.ok) {
            isAuthenticated.value = true;
            user.value = await res.json();
        } else if (res.status === 401) {
            isAuthenticated.value = false;
            user.value = null;
            console.log('User is not authenticated (401 from /bff/user).');
        } else {
            console.error('Error checking auth status:', res.status, res.statusText);
            isAuthenticated.value = false;
            user.value = null;
        }
    } catch (error) {
        console.error('Network error checking auth status:', error);
        isAuthenticated.value = false;
        user.value = null;
    }
}

Again, we use the BFF management endpoints to ask about the user’s current status, check the HTTP status codes, and
handle each appropriately. Binding our user to the UI is as straightforward as you’d imagine.

Markup


<table class="table table-striped table-bordered">
    <thead class="thead-dark">
    <tr>
        <th>Claim Type</th>
        <th>Claim Value</th>
    </tr>
    </thead>
    <tbody>
    <tr v-for="(claim, index) in user" :key="index">
        <td>{{ claim.type }}</td>
        <td>{{ claim.value }}</td>
    </tr>
    </tbody>
</table>

So far, we’ve only used BFF management endpoints, but what about custom APIs? Well, we’ve already registered one at
/todos. Securely calling APIs is as straightforward as calling the management APIs.

Javascript

const todos = ref([]);

// Fetch ToDos from the backend (no changes needed here)
async function fetchTodos() {
    loading.value = true;
    error.value = null;
    try {
        todos.value = await fetchApi('/todos'); // GET request
    } catch (err) {
        error.value = err.message || 'Failed to fetch ToDos.';
        todos.value = []; // Clear potentially stale data
    } finally {
        loading.value = false;
    }
}

The fetchApi call handles adding the X-CSRF security header and handling JSON responses. Retrieving JSON from APIs
is as simple as using fetch on BFF-secured endpoints and relying on cookie-based authentication.

The following section will examine what it takes to set up the BFF so that our front end and API can communicate
securely.

The Backend

ASP.NET Core is a best-in-breed technology stack for building web APIs. Let’s examine a few of the endpoints powering
our front-end experience.

Csharp

group.MapGet("/", () => data);
group.MapGet("/{id}", (int id) =>
{
    var item = data.FirstOrDefault(x => x.Id == id);
}).WithName("todo#show");

// POST
group.MapPost("/", (ToDo model, ClaimsPrincipal user, LinkGenerator links) =>
{
    model.Id = ToDo.NewId();
    model.User = $"{user.FindFirst("sub")?.Value} ({user.FindFirst("name")?.Value})";

    data.Add(model);

    var url = links.GetPathByName("todo#show", new { id = model.Id });
    return Results.Created(url, model);
});

Now, where does authentication fit into this API definition? We must add JSON Web Token (JWT) authentication and
authorization policies for each endpoint.

Csharp

using Vue.Api;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication("token")
    .AddJwtBearer("token", options =>
    {
        options.Authority = "https://demo.duendesoftware.com";
        options.Audience = "api";
        options.MapInboundClaims = false;
    });

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("ApiCaller", policy =>
    {
        policy.RequireClaim("scope", "api");
    });

    options.AddPolicy("InteractiveUser", policy =>
    {
        policy.RequireClaim("sub");
    });
});

var app = builder.Build();

app.UseHttpsRedirection();

app.MapGroup("/todos")
    .ToDoGroup()
    .RequireAuthorization("ApiCaller", "InteractiveUser")
    .WithOpenApi();

app.Run();

As you can see in the code, we are using the constructs provided by ASP.NET Core to secure our APIs. While we
implemented this API definition within our BFF architecture, it doesn’t rely on any BFF-specific code.

Conclusion

In this quick overview of BFF, we defined the architectural pattern, the critical elements of a BFF solution, and how
they all relate. We also showed how secure communication between the front and back end can be as seamless as calling
fetch on exposed endpoints. By adopting a BFF, you can reduce boilerplate code in your UI while improving the overall
security posture of your solutions.

To learn more about BFF, check our official BFF documentation for more
insights and samples.