Quantcast
Channel: damienbod – Software Engineering
Viewing all articles
Browse latest Browse all 358

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

$
0
0

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/


Viewing all articles
Browse latest Browse all 358

Trending Articles