Set up a SPA+BFF with ASP.NET Core and Angular in 3 steps (.NET 8)
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
dist
folder is copied in thewwwroot
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:
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: