Blazor WebAssembly applications at Elia

Vincent Van Den Berghe
Elia Group Engineering Blog
9 min readJan 18, 2021

Introduction

Blazor is a technology to build front-end apps that run in a browser using C#. Interested readers can refer to https://docs.microsoft.com/en-us/aspnet/core/blazor/?view=aspnetcore-3.1 for more information. There are two hosting models available, and here we only consider Blazor WebAssembly, the hosting model that allows us to develop a standalone application that is deployable via simple content delivery, and optionally installable on any client (desktop or mobile).

At Elia, applications are developed using an enterprise framework called “Arc4u”, in combination with a Visual Studio plugin which generates a predictable architecture. This is called the “Guidance”.

The whole setup is flexible enough to adopt new front-end technology without requiring extensive changes. Blazor/WebAssembly is no exception. There is some integration required, but it’s manageable.

This document describes how to add and integrate a Blazor front-end to a project generated by the Guidance, and how to add security to it. Although adding security to a Blazor App is easy and well documented, there are two unique requirements at Elia that makes this a bit more difficult:

  1. Blazor security only seamlessly integrates with the MSAL authentication library by default, which is incompatible with the ADFS 2016 servers used at Elia, since they only support v1.0 protocol. Until they will be upgraded, we need a workable solution.
  2. The Arc4u framework implements its own security attributes based on additional scopes and operations. They need to be handled seamlessly in Blazor somehow.

The solution described here handles these requirements, while allowing the developer to use all documented built-in Blazor security components. In addition, when MSAL will become usable, the changes to migrate to it will be trivial.

The links and the code provided in this article are specific for Elia, but illustrate the flexibility of our infrastructure to adopt new technologies.

The next section describes the way to generate a Blazor front-end in an existing Guidance project. The last section details how to implement Elia-compatible security in your Blazor app.

The proof of concept, including the two assemblies needed for supporting Blazor in Arc4u can be found at https://tfs.belgrid.net/TrashCollection/_git/IoTApp. It is assumed that the Guidance will integrate these assemblies in the future.

All back-end information applies to .NET Core 3.1+ Guidance projects only!

Any comments and improvements to this document are welcome and can be sent to Vincent.VanDenBerghe@elia.be.

Adding a Blazor front-end to an existing Guidance project

(until the Guidance has integrated this)

Adding a Blazor project

Adding a Blazor front-end is easy. Just choose “Blazor App” as a project type:

On the next screen, make sure that you choose “Blazor WebAssembly App” and that the “Configure for HTTPS” checkbox is turned off (since on localhost, we do not use HTTPS anymore). Since we want to generate a Blazor App and take care of our own hosting, the “ASP.NET Core hosted” option should be unchecked as well.

Optional Back-end integration

A Blazor application can be deployed serverless, e.g. via a content delivery network, since it is just a bunch of files delivered to a browser. If you run the project you have generated, a browser will start with a port on “localhost” determined by a “launchSettings.json” file, which is part of the default project artifacts:

The mechanism is similar to the one used in .NET Core Web API’s.

In almost all cases, your Blazor App will communicate to at least one back-end. Just like with other front-ends, you will need to make sure that this back-end is started before starting your Blazor app. You can do this manually, or by configuring multiple startup projects in your solution.

But if you have only one back-end and your Blazor App will be hosted on it (which means that they both will share the same http endpoint), you can integrate the Blazor project in your IIS project. If you then start your IIS project, the Blazor App will be shown instead of the default .NET Core welcome page.

If you want to do this, apply these steps on your “IISHost” project:

  • Add the NuGet package Microsoft.AspNetCore.Components.WebAssembly.Server
  • Add a reference to your BlazorApp project
  • Make sure that the launchSettings.json for both your back-end and your Blazor front-end refer to the same endpoint.
  • Apply the following changes in the Configure method in Startup.cs. For clarity, these changes are inside the conditional BLAZOR_HOST section, which is assumed to be defined:
if (env.IsDevelopment()){app.UseDeveloperExceptionPage();#if BLAZOR_HOSTapp.UseWebAssemblyDebugging();#endif}app.AddMonitoringTimeElapsed();var container = app.ApplicationServices.GetService<IContainerResolve>();Logger.Initialize(container.Resolve<Config>(), container.Resolve<Microsoft.Extensions.Logging.ILogger>());#if BLAZOR_HOST#region Blazor WebAssembly hosting// This methods serves the WebAssembly framework files when a request is made to root path.// This method also take path parameter that can be used if the WebAssembly project is only served from part of the project, giving options to combine web assembly project with a web applicationapp.UseBlazorFrameworkFiles();// This configuration helps in serving the static files like Javascript and CSS that is part of the Blazor WebAssemblyapp.UseStaticFiles();#endregion#endifapp.UseEndpoints(endpoints =>{endpoints.MapControllers();#if BLAZOR_HOST// Instead of the default "app.UseWelcomePage();", serve the index.html file from the WebAssembly when the WebAPI route does not find a match in the routing tableendpoints.MapFallbackToFile("index.html");#endif});#if !BLAZOR_HOSTapp.UseWelcomePage();#endifvar logger = container.Resolve<ILogger<Startup>>();

See https://tfs.belgrid.net/TrashCollection/_git/IoTApp?path=%2FBE%2FIotApp.IISHost%2FStartup.cs&version=GBmaster for a reference.

The nice thing about a BLAZOR_HOST definition is that you can define it when you want to make your debugging life easier, and remove it when it is not needed anymore (because the Blazor App will be hosted on a different endpoint).

Adding security to a Blazor App

The problem

The fundamental idea to implement “Elia-compliant” security is for the Blazor app to obtain an access (“bearer”) token to call back-end methods. This token contains all the claims for a user, including the special claim containing the custom security attributes defined by Arc4u.

Two problems are solved in MSAL but required tricky code for ADFS 2016:

  • The token is very long, and passing it in an URL is problematic because many hosts and proxies limit the maximum URL query length.
  • The token has a limited lifetime, and therefore expires at some point. Because we are using implicit flow with an untrusted client, we do not have refresh tokens to obtain a new one and are therefore required to implement a “silent refresh” in the background.

The way this was solved is to add a well-known endpoint in an already registered back-end, and have the Blazor app call this endpoint to obtain the token. The only knowledge needed by the Blazor app is the address of this back-end.

If you Blazor app requires communication with more than one back-end, these back-ends need to accept the same bearer token. This means you need to add an “AddRedirectUriToBE” for each additional back-end. As long as the back-ends are part of the same security group in ADFS, you’re golden.

The procedure to modify the back-end and the Blazor app is described below. All the referenced Arc4u.* assemblies are those in the proof of concept project. You are encouraged to browse the proof of concept project and see the actual implementation.

Modifying the back-end

You need to add the assembly Arc4u.BlazorHost to your IISHost project, and add the following entry in the RegisterTypes section of your appsettings.json:

“RegisterTypes”: [“Arc4u.BlazorHost.BlazorAuthentication, Arc4u.BlazorHost”]

Don’t forget to add this for all environments (Dev, Test, Prod, Acc).

Modifying the front-end

(Not everything will be detailed here. Please refer to the https://tfs.belgrid.net/TrashCollection/_git/IoTApp?path=%2FFE%2FBlazorApp&version=GBmaster project for reference)

Adding references

You need to add the following to your Blazor project:

  • The NuGet package Microsoft.Authentication.WebAssembly.Msal
  • The assembly Arc4u.Standard.OAuth.Blazor

The latter assembly replaces and extends some of the services provided by the MSAL package, while keeping the existing Blazor components intact.

Settings

You need to add appsettings.json in wwwroot (or extend it) to contain the reference to your back-end to be used for authentication. For example, if the back-end you will be using is at http://localhost:19932, the appsettings.json of your Blazor app will contain:

{“Oidc”: {“Authority”: “http://localhost:19932" }}

If you do not specify anything, the default will be the same http address as your Blazor app.

This will work only if the back-end is the one serving the Blazor app (see the “Optional back-end integration” above).

If you create an appsettings.json file in wwwroot, you can add your own stuff in it, and inject the configuration interface in the pages you need it:

@inject IConfiguration Configuration

To have multiple appsettings.json depending on your environment, you can use the same mechanism as in the Guidance back-ends, or use standard “appsettings.[environment].json mechanism as described in https://docs.microsoft.com/en-us/aspnet/core/blazor/fundamentals/environments?view=aspnetcore-3.1.

Namespaces

Make sure the following namespaces are in the _Imports.razor file:

@using Microsoft.AspNetCore.Authorization@using Microsoft.AspNetCore.Components.Authorization@using Arc4u.Standard.OAuth.Blazor

This will make sure all security components will be accessible.

Program startup

The Program.cs file is the one that is the most different: in there, the http client factory is adapted to server http clients containing the bearer token by default, and the authentication service is initialized:

public static async Task Main(string[] args){var builder = WebAssemblyHostBuilder.CreateDefault(args);builder.RootComponents.Add<App>(“app”);// Instead of the default “builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });” we need Http client factory that integrates authentication/// Note that because the <see cref=”HttpClient.BaseAddress”/> is initialized, any NSwag-generated facade proxy can set its BaseUrl to nullconst string HttpClientName = “IoTApp.ServerAPI”;builder.Services.AddHttpClient(HttpClientName, client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)).AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();// Supply HttpClient instances that include access tokens when making requests to the server projectbuilder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient(HttpClientName));builder.Services.AddArc4uOidcAuthentication(options =>{builder.Configuration.Bind(“Oidc”, options.ProviderOptions);});await builder.Build().RunAsync();}

Security pages and components

You need to add a reference to the javascript support in the index.html page on wwwroot:

<script src=”_framework/blazor.webassembly.js”></script><script src=”_content/Arc4u.Standard.OAuth.Blazor/AuthenticationService.js”></script>

To make sure the authentication state is properly propagated to the pages, the App.razor page needs to be modified as follows:

<CascadingAuthenticationState><Router AppAssembly=”@typeof(Program).Assembly”><Found Context=”routeData”><AuthorizeRouteView RouteData=”@routeData” DefaultLayout=”@typeof(MainLayout)”><NotAuthorized>@if (!context.User.Identity.IsAuthenticated){<RedirectToLogin />}else{<p>You are not authorized to access this resource.</p>}</NotAuthorized></AuthorizeRouteView></Found><NotFound><LayoutView Layout=”@typeof(MainLayout)”><p>Sorry, there’s nothing at this address.</p></LayoutView></NotFound></Router></CascadingAuthenticationState>

Various components need to be manually added: Authentication.razor, LoginDisplay.razor, and RedirectToLogin.razor.

Please refer to the proof of concept, and read https://docs.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/standalone-with-authentication-library?view=aspnetcore-3.1&tabs=visual-studio to understand what they do. This is basic boilerplate code, but there is no mechanism to automate this at this time.

Because an effort was made to respect the existing authentication model, you can use the standard Microsoft documentation to understand what is going on.

Using security in Blazor

Once properly configured, you can use the security features in Blazor. By default, nothing will change and the app will be accessible without logging in.

Please refer to https://tfs.belgrid.net/TrashCollection/_git/IoTApp?path=%2FFE%2FBlazorApp%2FPages%2FAbout.razor&version=GBmaster for a concrete example of an “About” page making back-end façade calls.

If you want to force the user to log in, add the following :

@attribute [Authorize]

If you add this at the top of a .razor page, navigating to the page will force the login.

If you add this at the end of _Imports.razor, the user must log in before being able to use the Blazor app. By doing this, you obtain the same behavior as in other UWP/WPF/Web backends

You can use the standard security components to define sections of a page only accessible for specific users:

<AuthorizeView><Authorized>The user is authorized</Authorized><NotAuthorized>The user is not authorized</NotAuthorized></AuthorizeView>

If you want certain sections to be available to users with certain roles, you can specify the role as an attribute:

<AuthorizeView Roles=”Admin”><Authorized>The user is authorized and has Admin role</Authorized></AuthorizeView>

If you want certain sections to be available to users having certain operations, you can specify them by including them between ‘[‘ and ‘]’. This is an Arc4u-specific extension to the existing component:

<AuthorizeView Roles=”[CanManageServerLoggingLevel]”><Authorized>The user is authorized to manage the server logging level</Authorized></AuthorizeView>

Note that the syntax is actually “Scope[operation1 [, operation2,…]” but in the above example the scope is “empty”, which is the default.

Calling a façade proxy is very simple: The injected http client is already initialized to carry the bearer token. The only thing you need to specify is the base address of the back-end if it is not the same as the Blazor app.

@inject HttpClient httpClient/// note that we can set <see cref=”IotAppFacadeClient.BaseUrl”/> to null since <see cref=”HttpClient”/> already contains the proper address in our casehttpClient.BaseAddress = “< the address of the service>”;var facade = new IotAppFacadeClient(httpClient) { BaseUrl = null };EnvironmentInfo = await facade.Environment_GetAsync();

Finally, you can get at the claims principal by referencing a cascading parameter, just as you would do with an MSAL-secured app:

[CascadingParameter]private Task<AuthenticationState> authenticationStateTask { get; set; }

Extension methods are provided to check for Arc4u-operations, just like in a regular back-end:

protected override async Task OnInitializedAsync(){var auth = await authenticationStateTask;if (auth.User.Identity.IsAuthenticated){if (auth.User.IsAuthorized(Access.CanManageServerLoggingLevel)){// …}}await base.OnInitializedAsync();}

--

--