Set up a SPA+BFF with ASP.NET Core and Angular in 3 steps (.NET 6/7)

Albert Starreveld
6 min readJun 14, 2023

Moving authentication from the front-end to the back-end seems to have a significant impact. Especially, because there is limited information available on this topic. Fortunately, the process of migrating authentication is relatively straightforward. To learn how to accomplish this with ASP.NET Core and Angular, read how to set up server-side authentication in three steps.

Context

In most cases, Single-Page Applications interact with APIs. In modern times, many of these applications do not just interact with one single API. They interact a full-fledged microservices landscape. In that case, there’s a good chance that your application would look somewhat like this:

Diagram 1.) A microservices architecture

Such an application landscape is commonly seen in many enterprises. These are typically back-office applications or webshops where users and back-office employees log in to handle various processes. So, obviously, a common requirement is that the user must log in.

Currently, the standard practice is for the user to authenticate on the server-side. That’s the safest. This proves challenging, because managing a session on all these front-end agnostic microservice would get messy.

Managing a session on all microservices is not the way to go. Instead, the problem can be easily solved by implementing the BFF Security Pattern.

The resulting situation is that you have one container containing a stateful server-side app hosting the SPA, where the user can log in, with the microservices running behind it.

From an infrastructure perspective, it would look like this:

Diagram 2.) A microservices architecture from a network perspective

This is what we are going to set up in this blogpost.

Step 1: Create the SPA with `dotnet new angular`

Microsoft’s toolkit already includes many of the components you need. For example, there is already a default Single-Page Application (SPA) within a server-side ASP.NET Core app in the default project templates. This is the following template:

dotnet new angular

When you execute this command, the dotnet CLI creates an ASP.NET Core app that hosts an Angular SPA. It looks like this:

In this picture, you see that what’s generated is a plain ASP.NET Core app with controllers and a ClientApp folder in it. Inside that folder, you will find the familiar Angular directory structure.

If you open this project with Visual Studio and click “play” or run the app with `dotnet run`, the SPA will automatically start in the browser.

This is the first step in creating an ASP.NET Core SPA with a BFF.

Step 2: Route API calls to the server-side

By default, any url you type in the address bar is being processed by angular. For a BFF to work, the API requests must be forwarded to the back-end. So, the next step is to route API calls to the back-end.

One notable file in the ClientApp folder is the proxy-conf file, which is included by default. It typically looks like this:

const { env } = require('process');

const target = env.ASPNETCORE_HTTPS_PORT ? `https://localhost:${env.ASPNETCORE_HTTPS_PORT}` :
env.ASPNETCORE_URLS ? env.ASPNETCORE_URLS.split(';')[0] : 'http://localhost:10301';

const PROXY_CONFIG = [
{
context: [
"/weatherforecast",
],
target: target,
secure: false,
headers: {
Connection: 'Keep-Alive'
}
}
]

This code tells the SPA which requests should be handled by the SPA itself (by default, every request) and which ones should be forwarded to the back-end.

As you can see in the diagram (diagram 2), with a BFF Security Pattern, the intention is for the requests not to be handled by the BFF itself but to be forwarded once more, to the downstream microservices.

To make this work, we need to include all API requests in the list of the proxy config. This way, the API requests will reach the server-side and can be forwarded downstream there.

It is also important that authentication takes place on the server-side. Therefore, all account-related requests must also be handled by the back-end.

This results in the following `proxy-conf.ts` file:

const { env } = require('process');

const target = env.ASPNETCORE_HTTPS_PORT ? `https://localhost:${env.ASPNETCORE_HTTPS_PORT}` :
env.ASPNETCORE_URLS ? env.ASPNETCORE_URLS.split(';')[0] : 'http://localhost:10301';

const PROXY_CONFIG = [
{
context: [
"/api",
"/account"
],
target: target,
secure: false,
headers: {
Connection: 'Keep-Alive'
}
}
]

Step 3.) Configuring the BFF

After configuring the front-end, you need to configure the back-end. Fortunately, the Angular template is already quite comprehensive, so you won’t have to do much.

The default Angular template includes back-end functionality, which can be found in the `WeatherForecastController`. However, with a BFF pattern, we want API requests to be handled by the microservices. Therefore, we need to remove this controller.

Additionally, we need to ensure that authentication works on the back-end and that requests are forwarded to the microservices. Although it may sound like many steps, you can quickly achieve this with just one NuGet package and a few lines of code.

In this example, we assume that we are using Auth0 as the identity provider, but you can certainly use something else. For instance, IdentityServer or Azure Active Directory.

The necessary functionality to authenticate and relay requests is provided by the GoCloudNative.Bff packages. For Auth0, we require the following:

dotnet add package GoCloudNative.Bff.Authentication.Auth0

Add the following snippet to your program.cs:

using GoCloudNative.Bff.Authentication.Auth0;
using GoCloudNative.Bff.Authentication.ModuleInitializers;

var builder = WebApplication.CreateBuilder(args);

// -------- <snippet 1> --------
builder.Services.AddSecurityBff(o =>
{
o.ConfigureAuth0(builder.Configuration.GetSection("Auth0"));
o.LoadYarpFromConfig(builder.Configuration.GetSection("ReverseProxy"));
});
// -------- </snippet 1> --------

builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseHsts();
}

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

// -------- <snippet 2> --------
app.UseSecurityBff();
// -------- </snippet 2> --------

// -------- And remove this: --------
// app.MapControllerRoute(
// name: "default",
// pattern: "{controller}/{action=Index}/{id?}");

app.MapFallbackToFile("index.html");

app.Run();

This code will not work without changing the appsettings.json file accordingly. Use the following config file:

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"Auth0": {
"ClientId": "{yourClientId}",
"ClientSecret": "{yourClientSecret}",
"Domain": "{yourDomain}",
"Audience": "{yourAudience}"
},
"AllowedHosts": "*",
"ReverseProxy": {
"Routes": {
"api1": {
"ClusterId": "api1",
"Match": {
"Path": "/api/{*any}"
}
},
"api2": {
"ClusterId": "api2",
"Match": {
"Path": "/api/foo/{*any}"
}
}
},
"Clusters": {
"api1": {
"Destinations": {
"api1": {
"Address": "http://localhost:8081/"
}
}
},
"api2": {
"Destinations": {
"api2": {
"Address": "http://localhost:8082/"
}
}
}
}
}
}

In the Auth0 section, you configure the settings from your Auth0 account. This enables the back-end to redirect users to the Auth0 login page. If you need guidance on how to set this up, please refer to this article.

In the ReverseProxy section, you route traffic to your APIs. In this case, I’m routing all requests to /api/{whatever} to api1 and requests to /api/foo/{whatever} to api2. You can customize this configuration as desired.

By configuring the GoCloudNative.BFF library in this way, all requests to the APIs will be enriched with a Bearer token.

Now, when you run the application and navigate to https://localhost:xxxxx/account/login, you will be redirected to the Auth0 login page and back to your SPA.

After successful login, you can retrieve the user information by making a GET request to https://localhost:xxxxx/account/me.

To log out, navigate to https://localhost:xxxxx/account/end-session.

And that’s all!

3.1 OpenId Connect (IdentityServer/KeyCloak/Etc.)

Alternatively, you have the option to configure the BFF to connect to a plain OpenID Connect Identity Provider such as IdentityServer or KeyCloak. In this scenario, you should not install the Auth0 package, but instead install the OpenID Connect Package:

dotnet add package GoCloudNative.Bff.Authentication.OpenIdConnect

And, in the program.cs, configure OpenIdConnect:

builder.Services.AddSecurityBff(o =>
{
o.ConfigureOpenIdConnect(builder.Configuration.GetSection("OIDC"));
o.LoadYarpFromConfig(builder.Configuration.GetSection("ReverseProxy"));
});

And, you’ll need to add the following to the the appsettings.json too:

{
...
"Oidc": {
"ClientId": "{yourClientId}",
"ClientSecret": "{yourClientSecret}",
"Authority": "https://{yourAuthority}",
"Scopes": [
"openid", "profile", "offline_access"
]
},
...
}

3.2 Azure Active Directory

Alternatively, if you want to use Azure Active Directory for user authentication, employ the Azure AD package:

dotnet add package GoCloudNative.Bff.Authentication.AzureAd

Configure it with the ConfigureAzureAd method:

builder.Services.AddSecurityBff(o =>
{
o.ConfigureAzureAd(builder.Configuration.GetSection("AAD"));
o.LoadYarpFromConfig(builder.Configuration.GetSection("ReverseProxy"));
});

And add the following section to your appsettings.json file:

{
...
"AzureAd": {
"ClientId": "{yourClientId}",
"ClientSecret": "{yourClientSecret}",
"Scopes": [
"openid", "profile", "offline_access", "https://yourDomain.onmicrosoft.com/test/api1"
]
},
...
}

Ok. Awesome, now show me teh codez!!

The code snippets you have seen in this article are a part of the following repository:

Clone this repo, and open the solution to see how everything is set up in detail.

--

--

Albert Starreveld

Passionate about cloud native software development. Only by sharing knowledge and code we can take software development to the next level!