Securing a MudBlazor UI web application using security headers and Microsoft Entra ID

This article shows how a Blazor application can be implemented in a secure way using MudBlazor UI components and Microsoft Entra ID as an identity provider. The MudBlazor UI components adds some inline styles and requires a specific CSP setup due to this and the Blazor WASM script requirements.

Code: https://github.com/damienbod/MicrosoftEntraIDMudBlazor

Setup

The application is setup using a Blazor WASM UI hosted in an ASP.NET Core application. The MudBlazor Nuget package was added to client project. Some MudBlazor components were added to the UI using MudBlazor documentation.

Security Headers

The security headers need to be added to protect the session of the web application. I use NetEscapades.AspNetCore.SecurityHeaders to implement the headers. We can protect the UI using CSP nonces and so the NetEscapades.AspNetCore.SecurityHeaders.TagHelpers Nuget package is also used. The following packages are added to the server project.

  • NetEscapades.AspNetCore.SecurityHeaders
  • NetEscapades.AspNetCore.SecurityHeaders.TagHelpers

The SecurityHeadersDefinitions class adds the security headers as best possible for this technical setup. A nonce is used in the CSP for the scripts tag. The ‘unsafe-eval’ value is added to the script CSP definition due to the Blazor WASM technical setup. This reduces the security protections. The unsafe inline is added as a fallback for older browsers. The style CSP definition allows unsafe inline due to the MudBlazor UI components.

namespace MicrosoftEntraIdMudBlazor.Server;

public static class SecurityHeadersDefinitions
{
    public static HeaderPolicyCollection GetHeaderPolicyCollection(bool isDev, string? idpHost)
    {
        if(idpHost == null)
        {
            throw new ArgumentNullException(nameof(idpHost));
        }

        var policy = new HeaderPolicyCollection()
            .AddFrameOptionsDeny()
            .AddContentTypeOptionsNoSniff()
            .AddReferrerPolicyStrictOriginWhenCrossOrigin()
            .AddCrossOriginOpenerPolicy(builder => builder.SameOrigin())
            .AddCrossOriginResourcePolicy(builder => builder.SameOrigin())
            .AddCrossOriginEmbedderPolicy(builder => builder.RequireCorp()) // remove for dev if using hot reload
            .AddContentSecurityPolicy(builder =>
            {
                builder.AddObjectSrc().None();
                builder.AddBlockAllMixedContent();
                builder.AddImgSrc().Self().From("data:");
                builder.AddFormAction().Self().From(idpHost);
                builder.AddFontSrc().Self();        
                builder.AddBaseUri().Self();
                builder.AddFrameAncestors().None();

                builder.AddStyleSrc()
                    .UnsafeInline() // due to Mudblazor
                    .Self();

                builder.AddScriptSrc()
                    .WithNonce()
                    .UnsafeEval() // due to Blazor WASM
                    .UnsafeInline();

                // disable script and style CSP protection if using Blazor hot reload
                // if using hot reload, DO NOT deploy with an insecure CSP
            })
            .RemoveServerHeader()
            .AddPermissionsPolicy(builder =>
            {
                builder.AddAccelerometer().None();
                builder.AddAutoplay().None();
                builder.AddCamera().None();
                builder.AddEncryptedMedia().None();
                builder.AddFullscreen().All();
                builder.AddGeolocation().None();
                builder.AddGyroscope().None();
                builder.AddMagnetometer().None();
                builder.AddMicrophone().None();
                builder.AddMidi().None();
                builder.AddPayment().None();
                builder.AddPictureInPicture().None();
                builder.AddSyncXHR().None();
                builder.AddUsb().None();
            });

        if (!isDev)
        {
            // maxage = one year in seconds
            policy.AddStrictTransportSecurityMaxAgeIncludeSubDomains(
        maxAgeInSeconds: 60 * 60 * 24 * 365);
        }

        policy.ApplyDocumentHeadersToAllResponses();

        return policy;
    }
}

The UseSecurityHeaders adds the security headers middleware.

app.UseSecurityHeaders(SecurityHeadersDefinitions
    .GetHeaderPolicyCollection(env.IsDevelopment(), 
         configuration["AzureAd:Instance"]));

A nonce is used to protect the UI application and the tag helpers are used for this.

@addTagHelper *, NetEscapades.AspNetCore.SecurityHeaders.TagHelpers

The asp-add-nonce adds the nonce to the scripts for all the HTTP responses.

    <script asp-add-nonce src="_framework/blazor.webassembly.js"></script>
    <script asp-add-nonce src="_content/MudBlazor/MudBlazor.min.js"></script>
    <script asp-add-nonce src="antiForgeryToken.js"></script>

Microsoft Entra ID

Microsoft Entra ID is used to protect the Blazor application. The Microsoft.Identity.Web packages are used to implement the OpenID Connect client. The application authentication security is implemented using backend for frontend (BFF) security architecture. The UI part, is a view belonging to the server backend. All security is implemented using the trusted backend and the session is persisted using a secure HTTP only cookie. The WASM uses this cookie for the secure data requests.

  • Microsoft.Identity.Web
  • Microsoft.Identity.Web.UI
  • Microsoft.Identity.Web.GraphServiceClient

The AddMicrosoftIdentityWebAppAuthentication implements the UI OpenID Connect client.

var scopes = configuration.GetValue<string>("DownstreamApi:Scopes");
string[] initialScopes = scopes!.Split(' ');

services.AddMicrosoftIdentityWebAppAuthentication(configuration)
    .EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
    .AddMicrosoftGraph("https://graph.microsoft.com/v1.0", initialScopes)
    .AddInMemoryTokenCaches();

Note: if using in-memory cache, the cache gets reset after every application restart, but not the cookie. You need to use a persistent cache or reset the cookie when the tokens are missing.

Links

https://mudblazor.com/

https://github.com/MudBlazor/MudBlazor/

https://github.com/damienbod/Blazor.BFF.AzureAD.Template

https://me-id-mudblazor.azurewebsites.net/

3 comments

  1. […] Securing a MudBlazor UI web application using security headers and Microsoft Entra ID (Damien Bowden) […]

  2. […] Securing a MudBlazor UI web application using security headers and Microsoft Entra ID – Damien Bowden […]

  3. Marshall Penn · · Reply

    Hi Damien i am a bit confused – i might be a bit dim!In the HostAuthenticationStateProvider on line 75 you call to get the UseInfo like so: user = await _client.GetFromJsonAsync(“api/User”);

    But in the UserController there is no matching route – am i missing something?Regards,

    Marshall

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.