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

Albert Starreveld
4 min readJan 24, 2024

--

Last year I wrote an article about how to set up a SPA + BFF with .NET 7. In short, to scaffold an ASP.NET Angular project in .NET 7 and earlier, all you had to do is type dotnet new angular, add some NuGet packages, configure them, and Bob’s your uncle. Unfortunately, .NET 8 does not have that template anymore.

The BFF in the article was based on a NuGet package called GoCloudNative.Bff. This package has been rebranded and is now called OidcProxy.Net.

With OidcProxy.Net, setting up an Angular BFF + SPA is a lot easier. OidcProxy comes with a Template-Pack which you can use to scaffold a project for you.

Execute the following commands to build an Angular App with a BFF:

# Step 1:
# Download and install the template-pack
dotnet new install OidcProxy.Net.Templates

# Step 2:
# Scaffold an Angular App + ASP.NET Core Identity-Aware BFF
dotnet new OidcProxy.Net.Angular --backend "https://api.myapp.com"
--idp "https://idp.myapp.com"
--clientId xyz
--clientSecret abc

# Step 3:
# Run it
dotnet run

And that’s really all you need to do…

What do you get when you run these commands?

If you run the generated code, ASP.NET Core starts a webserver. The webserver serves a SPA and some additional endpoints:

  • /.auth/login
  • /.auth/login/callback (important: whitelist this in your IDP settings)
  • /.auth/end-session
  • /.auth/me

When you invoke the /.auth/login endpoint, the browser will redirect the user to the Identity Provider (--idp) you’ve configured in step 2. The user will sign in there and be returned to the the webserver.

Now, the user has a session on the webserver. When the user invokes the /.auth/me endpoint, it will show his user-info.

Also, what you’ve done by executing these commands, is that any request to /api/*, invoked by the SPA, will be forwarded the API specified in step 2, with the --backend flag. The OidcProxy.Net module will add a authentication: Bearer [ACCESS_TOKEN] header to every forwarded request.

What you’d need to do if you want to accomplish the same, manually

Under the bonnet, executing these commands does two things:

  • It creates a new dotnet project and installs the OidcProxy.Net.OpenIdConnect package.
  • It creates a ClientApp folder with a blank Angular App. This app will be compiled. The distfolder is copied in the wwwroot folder of the project.

Installing and configuring the OidcProxy.Net.OpenIdConnect package

In essence, the solution is a black dotnet web app. To scaffold one, type the following command:

dotnet new web

To enable authentication, include the OidcProxy.Net.OpenIdConnect package:

dotnet add package OidcProxy.Net.OpenIdConnect

Bootstrap the OidcProxy in the Program.cs:

using OidcProxy.Net.ModuleInitializers;
using OidcProxy.Net.OpenIdConnect;

var builder = WebApplication.CreateBuilder(args);

var config = builder.Configuration
.GetSection("OidcProxy")
.Get<OidcProxyConfig>();

builder.Services.AddOidcProxy(config);

var app = builder.Build();

app.UseOidcProxy();

app.Run();

Obviously, appsettings.json does not contain a section called OidcProxy. So, add it:

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"OidcProxy": {
"Oidc": {
"ClientId": "client",
"ClientSecret": "secret",
"Authority": "https://idp/"
},
"ReverseProxy": {
"Routes": {
"api": {
"ClusterId": "api",
"Match": {
"Path": "/api/{*any}"
}
}
},
"Clusters": {
"api": {
"Destinations": {
"api/node1": {
"Address": "https://localhost:8123"
}
}
}
}
}
}
}

The OidcProxy is basically a Yarp plugin. So, the appsettings.jon contains a section called ReverseProxy . You can configure any number of backends/APIs here.

Configuring and installing the OidcProxy.Net packages will introduce new endpoints in your webapp:

  • /.auth/login
  • /.auth/end-session
  • /.auth/me (returns either the userinfo of the signed-in user, or a 404 if not signed in)

Wiring the Angular App

To be able to develop an Angular app, you need npm and the angular-cli. Download npm here and install it. Install the Angular CLI by executing the following command:

npm install -g @angular/cli

Now, create a folder called ‘ClientApp’ and type the following command:


mkdir ClientApp
cd ClientApp
ng new

This will scaffold an empty Angular project for you. When you’re done, this is what your project structure should now look like:

figure 1.) A ASP.NET Core app with an Angular App in it.

You might feel urged to hit ‘play’ in visual studio or type .net run, but when you do, you will not see a working Angular app.

That’s because MSBuild does not compile Angular Apps out of the box. To make that work, you’ll need to add build steps to the .csproj file. In abstract, you’ll need to instruct MSBuild to:

  • Create a wwwroot folder and make sure it’s empty
  • Compile the Angular App
  • Copy it to a folder called wwwroot

This is accomplished by adding a target element in the .csproj file. This is what the .csproj will look like:

<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>

<Target Name="CopyScriptsToProject" BeforeTargets="Build">
<Message Text="Building ClientApp. This may take a while." />
<!-- Compile The angular App //-->
<Exec Command="npm install" Condition="'$(RestorePackagesWithLockFile)' != 'true'" WorkingDirectory="$(MSBuildProjectDirectory)\ClientApp" />
<Exec Command="npm run build" Condition="'$(RestorePackagesWithLockFile)' != 'true'" WorkingDirectory="$(MSBuildProjectDirectory)\ClientApp" />

<!-- Copy it to the wwwroot folder //-->
<Message Text="Copying ClientApp/dist folder to wwwroot" />
<RemoveDir Directories="$(MSBuildProjectDirectory)\WwwRoot" />
<ItemGroup>
<SourceScripts Include="$(MSBuildThisFileDirectory)\ClientApp\dist\client-app\**\*.*" />
</ItemGroup>
<Copy SourceFiles="@(SourceScripts)" DestinationFiles="@(SourceScripts -> '$(MSBuildProjectDirectory)\WwwRoot\%(RecursiveDir)%(FileName)%(Extension)')" Condition="!Exists('$(MSBuildProjectDirectory)\WwwRoot\%(RecursiveDir)%(FileName)%(Extension)')" />
</Target>

<ItemGroup>
<PackageReference Include="OidcProxy.Net.OpenIdConnect" Version="1.0.0" />
</ItemGroup>

<ItemGroup>
<Folder Include="ClientApp\dist\" />
<Folder Include="wwwroot\" />
</ItemGroup>

</Project>

Still, you will notice that you will not see a working Angular app when you build and run the project. You need to do one more thing: enable static files for ASP.NET Core. Do so by adding two lines of code to the program.cs:

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

Rendering your Program.cs to look like this:

using OidcProxy.Net.ModuleInitializers;
using OidcProxy.Net.OpenIdConnect;

var builder = WebApplication.CreateBuilder(args);

var config = builder.Configuration
.GetSection("OidcProxy")
.Get<OidcProxyConfig>();

builder.Services.AddOidcProxy(config);

var app = builder.Build();

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

app.UseOidcProxy();

app.Run();

And that’s it. Type dotnet run or hit “play” in Visual Studio, and you should see:

Figure 2.) An Angular App with a BFF

--

--

Albert Starreveld

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