Software engineers looking to deploy highly secure web applications increasingly leverage the backend for frontend pattern. It provides the means for performing the OpenID Connect flow on the server, and not exposing security-critical access tokens in an insecure web browser.
For example, Duende’s Backend for Frontend security framework ensures the tokens used for secure access are managed on the server. The web browser only uses an HTTP-only signed cookie for session management. Simply put, this approach makes stealing access tokens nearly impossible.
Here’s a quick tutorial for implementing our BFF framework on a JavaScript web app using ASP.NET Core. This example also uses Duende IdentityServer for OpenID Connect and OAuth 2.0 functionality. Check out our robust collection of IdentityServer Quickstarts for hands-on programmatic walkthroughs of our other products.
Duende Tutorial Prerequisites
We generally recommend that our users follow the tutorial Quickstarts in order. However, if you want to dive right into the BFF Quickstart, start with a copy of Quickstart 3. That tutorial focuses on connecting to secure APIs from an ASP.NET Core app using OpenID Connect and OAuth 2.0. This is important functionality for understanding the BFF tutorial.
In this Quickstart, note that the paths are written relative to the base Quickstart directory created earlier, serving as the root directory for the reference implementation. Installing the IdentityServer templates is another requirement.
Building an ASP.NET Core JavaScript Web Application with Duende BFF
The goal of this tutorial involves building a simple JavaScript web application with a backend providing the security functionality. The app’s server-side code includes the token management logic. For authentication purposes, the client-side JavaScript only needs to handle an encrypted cookie.
This simple app performs four basic functions:
- Login using IdentityServer (part of Quickstart 3)
- Interaction with a local API on the server host
- Interaction with an external API on another host
- Logging out of IdentityServer
This tutorial provides a basic use case illustrating the ease of implementing the frontend for backend pattern with Duende. Our product line offers a state-of-the-art and secure approach for web-based identity management and authentication.
Create a New ASP.NET Core Web Application Project
In the terminal of your IDE, create a new ASP.NET Core web application project in the src
directory with the following commands:
dotnet new web -n JavaScriptClient
cd ..
dotnet sln add ./src/JavaScriptClient
You now need to install the NuGet packages for the Duende BFF framework and the OpenID Connect functionality. Run these commands from the /src/JavaScriptClient
directory.
dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect
dotnet add package Duende.BFF
dotnet add package Duende.BFF.Yarp
Once the project is created, verify the launchsettings.json
file is configured to use the application URL https://localhost:5003
. For example:
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"JavaScriptClient": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:5003",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
Implementing the BFF Pattern in the Web Application Project
Now it’s time to modify the code in the web app’s Program.cs
file to use Duende’s BFF framework. Remember, the backend for frontend pattern includes interaction with OpenID Connect functionality. Once again, this approach provides a stronger cybersecurity footprint, preventing cybercriminals from stealing access tokens since they aren’t actually stored within the browser.
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Duende.Bff.Yarp;
using Microsoft.AspNetCore.Authorization;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization();
builder.Services
.AddBff()
.AddRemoteApis();
builder.Services
.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
options.DefaultSignOutScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "https://localhost:5001";
options.ClientId = "bff";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.Scope.Add("api1");
options.Scope.Add("offline_access");
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.MapInboundClaims = false;
});
var app = builder.Build();
This code snippet includes important functionality for the Duende BFF framework. It indicates the use of cookie authentication between the browser and the backend, along with configuring OpenID Connect (“oidc”) as the default login. This ensures all tokens stay on the server. Requesting the offline_access
scope also allows the framework to automatically refresh the access token for remote APIs.
Now, add the code for the app’s middleware to Program.cs:
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseBff();
app.UseAuthorization();
app.MapBffManagementEndpoints();
app.Run();
This middleware code looks similar to the logic within WebClient. However, it also calls functions (UseBFF, MapBFFManagementEndpoints)
adding the BFF middleware and endpoints.
Adding the Client-Side HTML and JavaScript
With the BFF framework complete, it’s time to create the HTML and JavaScript files used for the app’s client-side. First, create a wwwroot
directory within the src/JavaScriptClient
path. Within that directory, create two files: index.html
and app.js
.
As the main page for the web app, index.html
includes buttons for logging in and out of the app. Additionally, two other buttons are used to trigger API calls, one local and one remote. A <pre>
container provides a window for displaying messages to the user. Finally, the <script>
tag references the app’s JavaScript file.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<button id="login">Login</button>
<button id="local">Call Local API</button>
<button id="remote">Call Remote API</button>
<button id="logout">Logout</button>
<pre id="results"></pre>
<script src="app.js"></script>
</body>
</html>
The JavaScript file handles client-side logic, like a helper function to display user messages:
function log() {
document.getElementById("results").innerText = "";
Array.prototype.forEach.call(arguments, function (msg) {
if (typeof msg !== "undefined") {
if (msg instanceof Error) {
msg = "Error: " + msg.message;
} else if (typeof msg !== "string") {
msg = JSON.stringify(msg, null, 2);
}
document.getElementById("results").innerText += msg + "\r\n";
}
});
}
Additionally, the Duende BFF framework includes a user management endpoint to verify if the user is logged in. The userClaims
variable is defined as a global, as it’s accessed elsewhere in the app.
let userClaims = null;
(async function () {
var req = new Request("/bff/user", {
headers: new Headers({
"X-CSRF": "1",
}),
});
try {
var resp = await fetch(req);
if (resp.ok) {
userClaims = await resp.json();
log("user logged in", userClaims);
} else if (resp.status === 401) {
log("user not logged in");
}
} catch (e) {
log("error checking user status");
}
})();
The event handlers for the UI’s buttons also need to be registered.
document.getElementById("login").addEventListener("click", login, false);
document.getElementById("local").addEventListener("click", localApi, false);
document.getElementById("remote").addEventListener("click", remoteApi, false);
document.getElementById("logout").addEventListener("click", logout, false);
The logic for implementing the login functionality is relatively straightforward, simply calling the BFF endpoint.
function login() {
window.location = "/bff/login";
}
The logout functionality is more complex, as the BFF logout endpoint includes cross-site request forgery (CSRF) protection logic. This anti-forgery token uses the global userClaims
variable mentioned earlier. That variable contains the token along with the full URL.
function logout() {
if (userClaims) {
var logoutUrl = userClaims.find(
(claim) => claim.type === "bff:logout_url"
).value;
window.location = logoutUrl;
} else {
window.location = "/bff/logout";
}
}
As an additional step, create empty stubs for the event handlers of the API calls, which get implemented later.
async function localApi() {
}
async function remoteApi() {
}
Register the BFF Host and JavaScript Client with IdentityServer
With the web client app ready to go, you now need to register it and the BFF host with IdentityServer. This registration happens in the configuration file used by IdentityServer. This configuration closely matches the one from the web client. Note that requesting the offline_access
scope must be allowed.
In the IdentityServer project, update the src/IdentityServer/Config.cs
file with the following code:
// JavaScript BFF client
new Client
{
ClientId = "bff",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.Code,
// where to redirect to after login
RedirectUris = { "https://localhost:5003/signin-oidc" },
// where to redirect to after logout
PostLogoutRedirectUris = { "https://localhost:5003/signout-callback-oidc" },
AllowOfflineAccess = true,
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1"
}
}
Initial Testing of the Web Client and Duende BFF Middleware
Run the web client application and test the functionality of the login and logout buttons. Clicking on the Login button redirects the browser to IdentityServer for logging in. It then redirects back to the web app with the validated session cookie from the BFF middleware. Note that the page now displays the key-value pairs from the user’s claims.
Clicking on the Logout button works as expected, notifying the user of their logged-out status.
Implementing the API Support
With the login/logout functionality operational, it’s time to implement the API calls and event handler stubs. This includes the local API residing in the same backend host as the rest of the JavaScript web client app. In the BFF pattern, the user’s session cookie authenticates any local API used.
Any remote APIs – hosted on another server – are authenticated using an access token. Our application has that token stored in the user’s session in the backend server. The Duende BFF uses a proxy to accept a call from the browser with the authenticated user’s session cookie. It then calls the remote API using the access token for authentication.
To define a local API for this example app, we simply add a quick handler to the Program.cs
file.
[Authorize]
static IResult LocalIdentityHandler(ClaimsPrincipal user)
{
var name = user.FindFirst("name")?.Value ?? user.FindFirst("sub")?.Value;
return Results.Json(new { message = "Local API Success!", user = name });
}
Now, update the endpoint configuration code in src/JavaScriptClient/Program.cs
to register both APIs. This includes the local API and the proxy used by the Duende BFF for the remote API.
app.MapBffManagementEndpoints();
// Uncomment this for Controller support
// app.MapControllers()
// .AsBffApiEndpoint();
app.MapGet("/local/identity", LocalIdentityHandler)
.AsBffApiEndpoint();
app.MapRemoteBffApiEndpoint("/remote", "https://localhost:6001")
.RequireAccessToken(Duende.Bff.TokenType.User);
AsBffApiEndpoint()
serves as a fluent helper method, providing Duende BFF support to local APIs. It includes anti-forgery protection as well as the suppression of failed login attempts. In the latter scenario, a 401 or 403 status code is returned. MapRemoteBffApiEndpoint()
registers the BFF proxy, configuring it to use the BFF access token.
Return to the app.js
file and fully implement the event handlers for the API calls as follows:
async function localApi() {
var req = new Request("/local/identity", {
headers: new Headers({
"X-CSRF": "1",
}),
});
try {
var resp = await fetch(req);
let data;
if (resp.ok) {
data = await resp.json();
}
log("Local API Result: " + resp.status, data);
} catch (e) {
log("error calling local API");
}
}
async function remoteApi() {
var req = new Request("/remote/identity", {
headers: new Headers({
"X-CSRF": "1",
}),
});
try {
var resp = await fetch(req);
let data;
if (resp.ok) {
data = await resp.json();
}
log("Remote API Result: " + resp.status, data);
} catch (e) {
log("error calling remote API");
}
}
Note that the path for the remote API adds a /remote
prefix to identify the need to use the BFF proxy. Both API calls require the X-CSRF: 1
header as an anti-forgery token.
Run the JavaScriptClient application again to test the API calls. The local API returns a simple status message, while the remote displays the user token data as a list of key-value pairs.
Duende provides Software Engineers with Top-Shelf Resources
For more information on this tutorial, check out our Quickstart detailing how browser-based JavaScript apps use the BFF pattern. Explore other Quickstarts providing practical information on implementing IdentityServer and Backend for Frontend on your next project.