close icon
.NET

Securing Blazor WebAssembly Apps

Learn how to secure Blazor WebAssembly applications with Auth0.

July 21, 2020

The final release of Blazor WebAssembly has been released, so you can finally build your WebAssembly (also known as WASM) applications by leveraging the Blazor framework and .NET runtime. Thanks to Auth0, you can also easily secure them by adding support for authentication and authorization, as this article will show.

The Blazor WebAssembly Application

This article assumes that you have a basic knowledge of building applications with Blazor. If you don't, you can read our tutorial that will guide you in exploring the different Blazor hosting models and building Single Page Applications (SPA) without using JavaScript. The application that you are going to secure throughout this article is the result of that tutorial. In particular, it is the Blazor WebAssembly app hosted in an ASP.NET Core application. To start, clone the Blazor WebAssembly version of that application by running the following command in a terminal window:

git clone -b starting-point --single-branch https://github.com/auth0-blog/secure-blazor-wasm-quiz-manager.git

The command above gets the content of the starting-point branch of the GitHub repository containing the full code of the project you are going to secure.

After downloading the Blazor project, move to its root folder (secure-blazor-wasm-quiz-manager) and run the following command to check if everything is working as expected:

dotnet run --project Server

If this is the first time you run an ASP.NET Core project on your machine, you may need to generate and trust the .NET Core developer certificate to enable HTTPS support locally. If you don't trust the development certificate, your browser will warn you about security risks.

Check out this document for detailed instructions about trusting the development certificate.

Now, point your browser to https://localhost:5001. You should see the following page:

Blazor App Home page

Clicking the Quiz item on the navigation bar, you should be able to take a simple quiz as shown in the following screenshot:

Blazor Quiz Page

If you take a look at the content of the project folder, you will find three subfolders, as shown below:

secure-blazor-wasm-quiz-manager
ā”‚   .gitignore
ā”‚   QuizManagerClientHosted.sln    
ā”œā”€ā”€ Client
ā”œā”€ā”€ Server
ā””ā”€ā”€ Shared

The Client folder contains the code of the Blazor WebAssembly application. The Server folder contains the API providing the questions of the quiz. The Shared folder hosts the definition of the QuizItem class shared among the client and the server. Again, to have more details about the Blazor app implementation, check out this tutorial.

In this tutorial, you will secure an existing Blazor WASM application. If you want to create a new Blazor WASM application from scratch that supports authentication, you can run the following command:

dotnet new blazorwasm -au Individual -o NewBlazorApplication

Here, NewBlazorApplication is the name of the application you will create. Of course, if you create your application from scratch, you will find a few things we are going to explain already applied, and you will not be able to follow this tutorial step by step.

Registering the Blazor WASM App with Auth0

As said, the main goal of this article is to show how to secure the Blazor application described in the previous section. You will use Auth0 since it provides an easy way to integrate authentication and authorization without having to deal with the complexity of the underlying technology. To use Auth0, you need to provide some information and configure your application to make the two parties communicate with each other. If you don't have an Auth0 account yet, you can sign up for a free one right now.

After accessing the Auth0 Dashboard, move to the Applications section, and follow these steps:

  1. Click the Create Application button.
  2. Provide a friendly name for your application (for example, Quiz Blazor WASM Client) and select Single Page Web Applications as the application type.
  3. Finally, click the Create button.

After you create the application, move to the Settings tab and take note of your Auth0 Domain and your Client ID. Then, assign the value https://localhost:5001/authentication/login-callback to the Allowed Callback URLs field and the value https://localhost:5001 to the Allowed Logout URLs field.

The first value tells Auth0 which URL to call back after users authenticate. The second value tells Auth0 which URL users should be redirected to after they logout.

Finally, click the Save Changes button to apply them.

Adding Support for Authentication

Now, you need to configure your Blazor project by applying some changes to make it aware of Auth0.

Configure your Blazor app

So, move to the Client/wwwroot folder and create an appsettings.json file with the following content:

{
  "Auth0": {
    "Authority": "https://<YOUR_AUTH0_DOMAIN>",
    "ClientId": "<YOUR_CLIENT_ID>"
  }
}

Replace the placeholders <YOUR_AUTH0_DOMAIN> and <YOUR_CLIENT_ID> with the respective values taken from the Auth0 dashboard.

Add support for authentication

Now, add the authentication package to the Blazor client project by running the following command in the Client folder:

dotnet add package Microsoft.AspNetCore.Components.WebAssembly.Authentication

After adding the package, still in the Client folder, edit the Program.cs file by replacing its content with the following C# code:

// Client/Program.cs

using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace QuizManagerClientHosted.Client
{
  public class Program
  {
    public static async Task Main(string[] args)
    {
      var builder = WebAssemblyHostBuilder.CreateDefault(args);
      builder.RootComponents.Add<App>("app");

      builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

      builder.Services.AddOidcAuthentication(options =>
      {
        builder.Configuration.Bind("Auth0", options.ProviderOptions);
        options.ProviderOptions.ResponseType = "code";
      });

      await builder.Build().RunAsync();
    }
  }
}

You added the call to AddOidcAuthentication() with specific options. In particular, you specified to use the parameters from the Auth0 section of the appsettings.json configuration file and provided some further information:

To complete the implementation of authentication support in your application, open the index.html file under the Client/wwwroot folder and add the reference to the AuthenticationService.js script as shown below:

<!-- Client/wwwroot/index.html -->
<!DOCTYPE html>
<html>
  <!-- existing markup -->
  <body>
    <!-- existing markup -->
    <script src="\_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>
    <!--šŸ‘† new addition -->
    <script src="\_framework/blazor.webassembly.js"></script>
  </body>
</html>

This script is responsible for performing the authentication operations on the WebAssembly client side.

"Blazor WebAssembly has now integrated support for authentication."

Tweet

Tweet This

Adjust the UI of your Blazor app

At this point, you prepared the infrastructure for your Blazor app to support authentication. Now you need to make some changes to the UI.

The first step is to enable support for the authorization Razor components. So, open the _Imports.razor file in the Client folder and add a reference to the Microsoft.AspNetCore.Components.Authorization and Microsoft.AspNetCore.Authorization namespaces. The content of that file will look as follows:

@* Client/_Imports.razor *@

@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Authorization  //šŸ‘ˆ new addition
@using Microsoft.AspNetCore.Authorization             //šŸ‘ˆ new addition
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using QuizManagerClientHosted.Client
@using QuizManagerClientHosted.Client.Shared

Then, open the App.razor file in the same folder and replace its content with the following:

<!-- Client/App.razor -->
<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
            <Authorizing>
                <p>Determining session state, please wait...</p>
            </Authorizing>
            <NotAuthorized>
                <h1>Sorry</h1>
                <p>You're not authorized to reach this page. You need to log in.</p>
            </NotAuthorized>
        </AuthorizeRouteView>
    </Found>
    <NotFound>
        <p>Sorry, there's nothing at this address.</p>        
    </NotFound>
</Router>

You used the AuthorizeRouteView Blazor component to customize the content according to the user's authentication status.

The next step is to create a new Razor component that allows the user to log in and to see their name when authenticated. So, create a new file named AccessControl.razor in the Client/Shared folder with the following content:

@* Client/Shared/AccessControl.razor *@

@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

@inject NavigationManager Navigation
@inject SignOutSessionStateManager SignOutManager

<AuthorizeView>
    <Authorized>
        Hello, @context.User.Identity.Name!
        <a href="#" @onclick="BeginSignOut">Log out</a>
    </Authorized>
    <NotAuthorized>
        <a href="authentication/login">Log in</a>
    </NotAuthorized>
</AuthorizeView>

@code{
    private async Task BeginSignOut(MouseEventArgs args)
    {
        await SignOutManager.SetSignOutState();
        Navigation.NavigateTo("authentication/logout");
    }
}

The component uses the AuthorizeView component to show different content according to the user's authentication status. Basically, it shows the Log in link when the user is not authenticated. It shows the name of the user and the Log out link when the user is authenticated.

Note the URL the user is redirected to when they click the Log out link (authentication/logout). You will learn about that URL in a moment.

Now, open the MainLayout.razor file in the Shared folder and add the AccessControl component just before the About link. The final code should look like the following:

@* Client/Shared/MainLayout.razor *@

@inherits LayoutComponentBase

 <div class="sidebar">
    <NavMenu />
 </div>

 <div class="main">
    <div class="top-row px-4">
        <AccessControl />
        <a href="https://docs.microsoft.com/en-us/aspnet/" target="_blank">About</a>
    </div>

    <div class="content px-4">
        @Body
    </div>
 </div>

When you registered your Blazor app with Auth0, you specified a few URLs in the form https://localhost:5001/authentication/* as allowed URLs for login callback and logout. You also used the logout URL in the AccessControl component.

To manage these URLs, you need to implement a page responsible for handling different authentication stages. For this purpose, create a new Authentication.razor file in the Pages folder with the following code:

@* Client/Pages/Authentication.razor *@

@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using Microsoft.Extensions.Configuration

@inject NavigationManager Navigation
@inject IConfiguration Configuration

<RemoteAuthenticatorView Action="@Action">
    <LogOut>
        @{
            var authority = (string)Configuration["Auth0:Authority"];
            var clientId = (string)Configuration["Auth0:ClientId"];

             Navigation.NavigateTo($"{authority}/v2/logout?client_id={clientId}");
        }
    </LogOut>
</RemoteAuthenticatorView>

@code{
    [Parameter] public string Action { get; set; }
}

As you can see, this component implements a page containing the RemoteAuthenticatorView component. This component manages the users' authentication status and interacts with the authorization server on the Auth0 side. While the login interaction doesn't require any specific code, you need to manage the logout transaction. In fact, by design Blazor clears your authentication state on the client side but doesn't disconnect you from Auth0. To close your session on the Auth0 side, you need to explicitly call the logout endpoint, as shown in the code above.

Disclaimer: At the time of writing, the logout function seems not to be stable due to an apparently Blazor issue. In fact, in some random circumstances, it doesn't actually call the Auth0 logout endpoint.

Finally, you need to add the Authorize attribute to the QuizViewer.razor page to protect it from unauthorized accesses. Open the QuizViewer.razor file in the Pages folder and add the attribute as shown below:

@* Client/Pages/QuizViewer.razor *@

@page "/quizViewer"
@attribute [Authorize]            //šŸ‘ˆ new addition

@using QuizManagerClientHosted.Shared
@using System.Net.Http.Json
@inject HttpClient Http

// ... exisiting code ...

Note that the presence of the Authorize attribute on the page doesn't prevent the client from calling the API on the server. You need to protect the API on the server side as well.

At this point, you can stop your Blazor app, if it is still running, and restart it to test the authentication integration. Once the app is running, by clicking the Quiz menu item, you should see the following screen:

Blazor app and the unauthenticated user

Note the Log In in the upper right corner. By clicking on it, the Auth0 Universal Login page is shown, and the authentication process takes place. After authentication, you will be able to access the QuizViewer page.

Securing the API with Auth0

The data shown in the QuizViewer page are loaded from the /quiz API implemented in the server project. This API is not protected, so any client could access it. In fact, the Blazor WASM client is able to access it without any problem. However, in a production-ready scenario, you need to protect the API to prevent unauthorized access. Although the API security implementation is out of the scope of this tutorial, you need to perform a few minimal changes to the existing API in the server project to secure it.

If you want to learn more about protecting Web APIs in .NET Core, please check out this article.

Register the API

Similarly to what you did with the Blazor WASM application, you need to register the API with Auth0. So, head your browser to the Auth0 Dashboard, move to the API section, and follow these steps:

  1. Click the Create API button.
  2. Provide a friendly name for your API (for example, Quiz API) and a unique identifier (also known as audience) in the URL format (for example, https://quizapi.com).
  3. Leave the signing algorithm to RS256 and click the Create button.

This way, Auth0 is aware of your Web API and will allow you to control access.

Protecting the API

In the server project under the Server folder, open the appsettings.json. Its content look like the following:

{
  "Logging": {
      "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
      }
    },
  "AllowedHosts": "*",
  "Auth0": {
    "Authority": "https://<YOUR_AUTH0_DOMAIN>",
    "ApiIdentifier": "<YOUR_API_IDENTIFIER>"
  }
}

Replace the <YOUR_AUTH0_DOMAIN> placeholder with the Auth0 domain value you used for the Blazor WASM client. Also, replace the <YOUR_API_IDENTIFIER> placeholder with the unique identifier you defined for your API in the Auth0 Dashboard: it should be https://quizapi.com, if you kept the suggested value.

Now, open the QuizController.cs file in the Controllers folder and uncomment the Authorize attribute attached to the QuizController class. The resulting code should look like the following:

// Server/Controllers/QuizController.cs

using QuizManagerClientHosted.Shared;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;

namespace QuizManagerClientHosted.Server.Controllers
{
  [ApiController]
  [Route("[controller]")]
  [Authorize]            //šŸ‘ˆ uncommented code
  public class QuizController : ControllerBase
  {
    // ... existing code ...
  }
}

Remember that the API of this project is prepared to be secure. If you want to learn in depth how to protect your API, read this article.

Now your API is protected. To check if everything is working as expected, move to the root of the project and restart it. Then, log in to the application and click the Quiz menu item. This time you shouldn't be able to display the quiz data. Your screen should be like the following:

Blazor app unauthorized to access the API

If you take a look at the network section of your browser's developer tool, you will find that the call to the /quiz endpoint gets an HTTP 401 status code, as in the following example:

Unauthorized error when calling an API

This confirms that the server prevents unauthorized access to the API.

"Learn how to call a protected API with Blazor WebAssembly."

Tweet

Tweet This

Calling the Protected API

To enable your Blazor WASM application to access the protected API, you need to get an access token from Auth0 and provide it along with your API call. So, open the QuizViewer.razor file in the Client\Pages folder and change its content as follows:

@* Client/Pages/QuizViewer.razor *@

@page "/quizViewer"
@attribute [Authorize]

@using QuizManagerClientHosted.Shared
@using System.Net.Http.Json
//šŸ‘‡ new addition
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using System.Net.Http.Headers
//šŸ‘† new addition

@inject HttpClient Http
@inject IAccessTokenProvider TokenProvider //šŸ‘ˆ new addition

// ... existing code ...
  
@code {
  
    // ... existing code ...
  
    protected override async Task OnInitializedAsync()
    {
      using (var requestMessage = new HttpRequestMessage(HttpMethod.Get, "quiz")) {
        var tokenResult = await TokenProvider.RequestAccessToken();

        if (tokenResult.TryGetToken(out var token)) {
          requestMessage.Headers.Authorization = 
            new AuthenticationHeaderValue("Bearer", token.Value);
          var response = await Http.SendAsync(requestMessage);
          quiz = await response.Content.ReadFromJsonAsync<List<QuizItem>>();
        }
      }
    }
  
  // ... existing code ...
}

You imported the Microsoft.AspNetCore.Components.WebAssembly.Authentication and System.Net.Http.Headers namespaces and injected the TokenProvider dependency. Then, you arranged the OnInitializedAsync() method to get an access token and insert it as an HTTP authorization header in the API request.

This example shows an approach where you need to explicitly retrieve and attach an access token to each API request. If your Blazor WASM client performs many API requests, you'd like to centralize this operation to automatically add access tokens to every API call. See the official documentation for more information on how to customize your HttpClient instance for this purpose.

After applying these changes, restart your application, log in, and try to move to the Quiz page. Unfortunately, you still will get an HTTP 401 status code, but with a different message, as you can see from the following picture:

Blazor error with invalid token message

Why does this happen? Yet we are passing the access token, as you can see from the picture above. However, the error message is saying that the access token is not valid.

Unfortunately, at the time of writing, Blazor WASM has a limitation, as documented by this issue. In a nutshell, in a standard authentication and authorization flow, you should be able to send the unique identifier of your API (the ApiIdentifier parameter, also known as the audience parameter) when you authenticate. This way, the Auth0 authorization server will give you an access token that grants you specific access to that API. Due to that limitation, your Blazor client is not explicitly requesting access to your API.

When Auth0 is not receiving the audience parameter, it emits an opaque and generic access token, as per specs. This access token is not in JWT format and doesn't contain any information about the resource the token authorizes to access.

Defining a default audience

To overcome the current Blazor WASM limitation, you can define a default audience for your Auth0 tenant.

So, access your Auth0 Dashboard and navigate to your tenant settings. Now, scroll down to the API Authorization Settings section and add your ApiIdentifier as you default API audience, as you can see in the following picture:

Configuring the default audience in Auth0

Click the Save button to confirm your changes.

From now on, any client requiring an access token without specifying any audience parameter while authenticating will receive an access token related to your default audience.

Since, in general, the default audience is not bound to a specific API, you should use a generic name.

Anyway, consider that, whenever possible, you should use specific audiences for your APIs.

Now, log out your Blazor app and login again to get a new access token. This time you should be able to access your protected API and show the Quiz page.

Recap

This tutorial guided you in securing a Blazor WebAssembly application and integrating it with Auth0. You learned how to enable your application to support authentication and call a protected API passing the access token. You also came across the Blazor issue that currently prevents your application to request an access token for a specific API. Anyway, you learned how to work around this limitation by configuring a default audience in your Auth0 Dashboard.

The full source code of the application secured in this tutorial can be downloaded from this GitHub repository.

  • Twitter icon
  • LinkedIn icon
  • Faceboook icon