TL;DR: This tutorial introduces the Blazor framework by guiding you in building a simple Web application with C#. It will also show you how to integrate your Blazor application with Auth0 in order to secure it. Following the steps described in this tutorial, you will end up building two versions of a simple Web application. You can find the full code in this GitHub repository.

"Learn how to use Blazor to build your Web application with C#."

What is Blazor?

Blazor has been gaining in popularity lately, especially after the release of .NET Core 3.0, which enriched it with many interesting features. There is great interest around it and Microsoft is betting a lot on its future. But what is Blazor exactly?

Blazor is a programming framework to build client-side Web applications with .NET. It allows .NET developers to use their C# and Razor knowledge to build interactive UIs running in the browser. Developing client-side applications with Blazor brings a few benefits to .NET developers:

  • They use C# and Razor instead of JavaScript and HTML
  • They can leverage the whole .NET functionalities
  • They can share code across server and client
  • They can use the .NET development tools they are used to

In a nutshell, Blazor promises .NET developers to let them build client Web applications with the development platform they are comfortable with.

The Hosting Models

Blazor provides you with two ways to run your Web client application: Blazor Server and Blazor WebAssembly. These are called hosting models.

The Blazor Server hosting model runs your application on the server, within an ASP.NET Core application. The UI is sent to the browser, but UI updates and event handling are performed on the server side. This is similar to traditional Web applications, but the communication between the client side and the server side happens over a SignalR connection. The following picture gives you an idea of the overall architecture of the Blazor Server hosting model:

Blazor Server architecture diagram

The Blazor Server hosting model provides a few benefits, such as a smaller download size of the client app and the compatibility with not recent browsers. However, it has some drawbacks, like a higher latency due to the roundtrip between the client and the server for most user interactions and the challenging scalability in high traffic scenarios.

The Blazor WebAssembly hosting model lets your application run entirely on the user's browser. The full code of the application, including its dependencies and the .NET runtime, is compiled into WebAssembly, downloaded by the user's browser and locally executed. The following picture describes the hosting model of Blazor WebAssembly:

Blazor WebAssembly architecture diagram

The benefits provided by the Blazor WebAssembly hosting model are similar to those provided by Single Page Applications. After the download, the application is independent of the server, apart from the needed interactions. Also, you don't need an ASP.NET Core Web server to host your application. You can use any Web server, since the result of the WebAssembly compilation is just a set of static files.

On the other side, you should be aware of the drawbacks of this hosting model. The Blazor WebAssembly hosting model requires that the browser supports WebAssembly. In addition, the initial download of the application may take some time.

"You don't need an ASP.NET Core Web server to host Blazor WebAssembly applications."

Blazor Roadmap

Blazor is promising a great opportunity for .NET developers. However, take into account that the whole project is still a work in progress. In fact, at the time of writing, only the Blazor Server hosting model is officially supported in .NET Core 3.0. The Blazor WebAssembly hosting model is available in .NET Core 3.1 preview just for testing purposes, but its general availability is scheduled for May 2020.

Microsoft's goals on the Blazor project are very ambitious, especially for Blazor WebAssembly. In their vision, not only Blazor WebAssembly will become the main hosting model, but also it will drive a great revolution in the development of clients.

The Blazor WebAssembly hosting model will include Single Page Applications compiled into WebAssembly, Progressive Web Apps, hybrid mobile applications, Electron-based desktop applications, and native applications.

Prerequisites

Before starting to build your Blazor application, you need to ensure you have installed the right tools on your machine. In particular, you need to check if you have installed the .NET Core 3.0 SDK by typing the following command in a terminal window:

dotnet --version

You should get as a result the value 3.0.100 or above. If you don't, you should download the .NET Core 3.0 SDK and install it on your machine.

If you are going to use Visual Studio, be aware that you need to use Visual Studio 2019 16.3 or Visual Studio for Mac 8.3 or above.

Note: If you update Visual Studio to the latest version, you will get .NET Core 3.0 SDK bundled.

Building a Blazor Server Application

To getting started with Blazor, you are going to build a simple quiz application that shows a list of questions with multiple answers and assigns you a score based on the correct answers you provide. You will build this application by using the Blazor Server hosting model. You will see how to build the same application by using the Blazor WebAssembly model later on.

So, create a basic Blazor Server project by typing the following command in a terminal window:

dotnet new blazorserver -o QuizManager

This command uses the blazorserver template to generate the project for your application in the QuizManager folder. This newly created folder has a lot of content but, apart from the root folder, the relevant folders that you are going to touch are:

  • The Data folder: it contains the models and the services implementing the business logic.
  • The Pages folder: this contains the Razor components that generate the HTML views. In particular, this folder contains the _Host.cshtml Razor page which acts as the starting point of the Web UI.
  • The Shared folder: it contains Razor components and other elements shared among pages

Creating the Model and the Service

So, as a first step, delete the files inside the Data folder. Next, add a QuizItem.cs file into this folder and paste in the following code:

// Data/QuizItem.cs

using System;
using System.Collections.Generic;

namespace QuizManager.Data
{
    public class QuizItem
    {
        public string Question { get; set; }
        public List<string> Choices { get; set; }
        public int AnswerIndex { get; set; }
        public int Score { get; set; }

        public QuizItem()
        {
            Choices = new List<string>();
        }
    }
}

This class implements the model for each item of the quiz. It provides a question, a list of possible answers, the zero-based index of the correct answer, and the score assigned when the user gives the correct answer.

In the same Data folder, add a second file named QuizService.cs with the following content:

// Data/QuizService.cs

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace QuizManager.Data
{
    public class QuizService
    {
        private static readonly List<QuizItem> Quiz;

        static QuizService()
        {
            Quiz = new List<QuizItem> {
                new QuizItem
                {
                    Question = "Which of the following is the name of a Leonardo da Vinci's masterpiece?",
                    Choices = new List<string> {"Sunflowers", "Mona Lisa", "The Kiss"},
                    AnswerIndex = 1,
                    Score = 3
                },
                new QuizItem
                {
                    Question = "Which of the following novels was written by Miguel de Cervantes?",
                    Choices = new List<string> {"The Ingenious Gentleman Don Quixote of La Mancia", "The Life of Gargantua and of Pantagruel", "One Hundred Years of Solitude"},
                    AnswerIndex = 0,
                    Score = 5
                }
            };
        }

        public Task<List<QuizItem>> GetQuizAsync()
        {
            return Task.FromResult(Quiz);
        }
    }
}

This class defines a quiz as a list of QuizItem instances initialized by the QuizService() constructor. For simplicity, the list is implemented with a static variable, but in a real-world scenario, it should be persisted into a database. The GetQuizAsync() method simply returns the value of the Quiz variable.

Now, move into the root of your project and edit the Startup.cs file by replacing the definition of the ConfigureServices() method with the following code:

// Startup.cs

using System;
// Other using clauses
namespace QuizManager
{
    public class Startup
    {
        //leave the rest untouched

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();
            services.AddServerSideBlazor();
            services.AddSingleton<QuizService>();
        }

      //leave the rest untouched
    }
}

With this change, you registered the QuizService service you defined above instead of the service of the sample application coming with the default Blazor project template.

Creating Razor Components

Now that you've created the model and the service of the application, it's time to implement the UI. Blazor leverages Razor as a template processor to produce dynamic HTML. In particular, Blazor uses Razor components to build up the application UI. Razor components are self-contained units of markup and code that can be nested and reused even in other projects. They are implemented in file with the .razor extension.

"Razor components are the basic UI elements in a Blazor application."

To show the quiz to the user and let them interact with it, you need to implement a specific view as a Razor component. So, move into the Pages folder and remove the Counter.razor andFetchData.razor files. These files belonged to the default sample project. Then, add in the same folder the QuizViewer.razor file with the following content:

// Pages/QuizViewer.razor

 @page "/quizViewer"

 @using QuizManager.Data
 @inject QuizService QuizRepository

 <h1>Take your quiz!</h1>
 <p>Your current score is @currentScore</p>

@if (quiz == null)
{
    <p><em>Loading...</em></p>
}
else
{
    int quizIndex = 0;
    @foreach (var quizItem in quiz)
    {
        <section>
            <h3>@quizItem.Question</h3>
            <div class="form-check">
            @{
                int choiceIndex = 0;
                quizScores.Add(0);
            }
            @foreach (var choice in quizItem.Choices)
            {
                int currentQuizIndex = quizIndex;
                <input class="form-check-input" type="radio" name="@quizIndex" value="@choiceIndex" @onchange="@((eventArgs) => UpdateScore(Convert.ToInt32(eventArgs.Value), currentQuizIndex))"/>@choice<br>

                choiceIndex++;
            }
            </div>
        </section>

        quizIndex++;
    }
}

@code {
    List<QuizItem> quiz;
    List<int> quizScores = new List<int>();
    int currentScore = 0;

    protected override async Task OnInitializedAsync()
    {
        quiz = await QuizRepository.GetQuizAsync();
    }

    void UpdateScore(int chosenAnswerIndex, int quizIndex)
    {
        var quizItem = quiz[quizIndex];

        if (chosenAnswerIndex == quizItem.AnswerIndex)
        {
            quizScores[quizIndex] = quizItem.Score;
        } else
        {
            quizScores[quizIndex] = 0;
        }
        currentScore = quizScores.Sum();
    }
}

Take a look at the code of this component. Its first line uses the @page directive to define this component as a page, which is a UI element that is directly reachable through an address (/quizViewer in this case) in the Blazor's routing system. Then, you have the @using directive, which provides access to the QuizManager.Data namespace where you defined the QuizItem model and the QuizService service. The @inject directive asks the dependency injection system to get an instance of the QuizService class mapped to the QuizRepository variable.

After these initializations, you find the markup defining the UI. As you can see, this part is a mix of HTML and C# code whose purpose is to build the list of questions with the respective possible answers represented as radio buttons.

The final block of the component is enclosed in the @code directive. This is where you put the logic of the component. In the case of the QuizViewer component, you have the OnInitializedAsync() and the UpdateScore() methods. The first method is called when the component is initialized, and it basically gets the quiz data by invoking the GetQuizAsync() method of the QuizRepository service instance. The UpdateScore() method is called when the user clicks one of the proposed answers and it updates the list of the assigned scores according to the answer chosen by the user. In the same method, the value of the current score is computed and assigned to the currentScore variable. The value of this variable is shown above the list of questions, as you can see in the markup.

Now, go to apply the final touch by moving in the Shared folder and replacing the content of the NavMenu.razor file with the following code:

// Shared/NavMenu.razor

 <div class="top-row pl-4 navbar navbar-dark">
   <a class="navbar-brand" href="">QuizManager</a>
   <button class="navbar-toggler" @onclick="ToggleNavMenu">
       <span class="navbar-toggler-icon"></span>
   </button>
 </div>

 <div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
   <ul class="nav flex-column">
       <li class="nav-item px-3">
           <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
               <span class="oi oi-home" aria-hidden="true"></span> Home
           </NavLink>
       </li>
       <li class="nav-item px-3">
           <NavLink class="nav-link" href="quizViewer">
              <span class="oi oi-list-rich" aria-hidden="true"></span> Quiz
           </NavLink>
       </li>
   </ul>
 </div>

@code {
   bool collapseNavMenu = true;
   string NavMenuCssClass => collapseNavMenu ? "collapse" : null;

   void ToggleNavMenu()
   {
       collapseNavMenu = !collapseNavMenu;
   }
}

The NavMenu.razor file contains the definition of the navigation bar component of the application. The code you put its this file defines a navigation menu of two items: one pointing to the home page and the other to the QuizViewer component.

You're not going to change the App component implemented by the App.razor file in the project root folder yet, but it's still worth taking a look at anyway.

// App.razor

<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="@typeof(MainLayout)">
            <p>Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

This component attaches the Blazor routing system to your application by using the built-in Router component. It enables navigation among the pages of your application, distinguishing when the page is found from when it does not exist. For more information about the Blazor routing system, check the official documentation.

Running your Blazor Server Application

Now you have your quiz application, so launch it by typing the following command in a terminal window:

dotnet run

After a few seconds, you should get your application available at the address https://localhost:5001. So, if you open your browser at that address, you should get access to the home page, as shown by the following picture:

Blazor application default home page

Selecting the Quiz item in the navigation menu, you should get the interactive quiz you built so far. It should look like the following picture:

Quiz view in the Blazor application

If you open the developer tools of your browser, click on the Network tab, and refresh, you will discover that the communication between the client side and the server side of your application doesn't use HTTP, but it is a bi-directional binary communication managed by SignalR. The following picture shows the WebSocket channel in Chrome developer tools:

Blazor server communication based on SignalR

Building a Blazor WebAssembly Application

Now you are going to implement the same application by using the WebAssembly hosting model. As you know, this hosting model makes your application be compiled in WebAssembly and run in your browser. However, depending on the structure of your project, you have two options to create your application:

  • You may have just the client-side application that will call an existing Web API
  • You may have both the client-side application and the Web API application. In this case, the Web API application also serves the Blazor WebAssembly app to the browsers. This option is called ASP.NET Core hosted.

For this project, you will choose the second option. In fact, you will have the client-side application, that will be responsible for showing the UI and managing the user interaction, and the Web API application, that will provide the quiz to the client.

At the time of writing, the Blazor WebAssembly hosting model is not available for production, but it is supported in the .NET Core SDK 3.1 preview. So, in order to use this hosting model, you need to install this SDK preview on your machine.

Once you're finished installing the SDK from the link above, you have to manually install the Blazor WebAssembly project template by typing the following command in a terminal window:

dotnet new --install Microsoft.AspNetCore.Blazor.Templates::3.1.0-preview1.19508.20

Finally, to create this new project, type the following command:

dotnet new blazorwasm -o QuizManagerClientHosted --hosted

Note: If you want to create only the client-side application, you have to omit the --hosted flag in the previous command.

If you take a look at the QuizManagerClientHosted folder, you will find three folders: the Client folder, the Server folder, and the Shared folder. Each of these folders contains a .NET project. While you can figure out what the Client and the Server folder contain, you may wonder what the Shared folder contains. It contains a class library project with the code shared by the client-side and the server-side applications. In the case of the application you are going to re-implement, it will contain the data model.

So, move in the Shared folder, remove the WeatherForecasts.cs file and put into it the same QuizItem.cs file you created for the Blazor server application. Then, open the QuizItem.cs file and change the namespace into QuizManagerClientHosted.Shared. The content of the file will look like the following:

// Shared/QuizItem.cs

using System;
using System.Collections.Generic;

namespace QuizManagerClientHosted.Shared
{
    public class QuizItem
    {
        public string Question { get; set; }
        public List<string> Choices { get; set; }
        public int AnswerIndex { get; set; }
        public int Score { get; set; }

        public QuizItem()
        {
            Choices = new List<string>();
        }
    }
}

Creating the Server

Now, move in the Server/Controllers folder and remove the WeatherForecastController.cs file. Then, add in the same folder a new file named QuizController.cs and put the following code inside it:

// Server/Controllers/QuizController.cs

using QuizManagerClientHosted.Shared;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace QuizManagerClientHosted.Server.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class QuizController : ControllerBase
    {
        private static readonly List<QuizItem> Quiz = new List<QuizItem> {
            new QuizItem
                {
                    Question = "Which of the following is the name of a Leonardo da Vinci's masterpiece?",
                    Choices = new List<string> {"Sunflowers", "Mona Lisa", "The Kiss"},
                    AnswerIndex = 1,
                    Score = 3
                },
                new QuizItem
                {
                    Question = "Which of the following novels was written by Miguel de Cervantes?",
                    Choices = new List<string> {"The Ingenious Gentleman Don Quixote of La Mancia", "The Life of Gargantua and of Pantagruel", "One Hundred Years of Solitude"},
                    AnswerIndex = 0,
                    Score = 5
                }
            };

        [HttpGet]
        public List<QuizItem> Get()
        {
            return Quiz;
        }
    }
}

As you can see, this is the Web API version of the QuizService class you created in the Blazor server application. You notice the initialization of the Quiz static variable with a few QuizItem instances and the definition of the Get() action returning that variable.

For more information on how to create a Web API in .NET Core 3.0, see this tutorial.

Creating the Client

In order to create the Blazor client application, move into the Client/Pages folder and remove the Counter.razor and the FetchData.razor files. Then, add to this folder a file named QuizViewer.razor with the following content:

// Client/Pages/QuizViewer.cs

@page "/quizViewer"
@using QuizManagerClientHosted.Shared
@inject HttpClient Http

 <h1>Take your quiz!</h1>
 <p>Your current score is @currentScore</p>

@if (quiz == null)
{
    <p><em>Loading...</em></p>
}
else
{
    int quizIndex = 0;
    @foreach (var quizItem in quiz)
    {
        <section>
            <h3>@quizItem.Question</h3>
            <div class="form-check">
            @{
                int choiceIndex = 0;
                quizScores.Add(0);
            }
            @foreach (var choice in quizItem.Choices)
            {
                int currentQuizIndex = quizIndex;
                <input class="form-check-input" type="radio" name="@quizIndex" value="@choiceIndex" @onchange="@((eventArgs) => UpdateScore(Convert.ToInt32(eventArgs.Value), currentQuizIndex))"/>@choice<br>

                choiceIndex++;
            }
            </div>
        </section>

        quizIndex++;
    }
}

@code {
    List<QuizItem> quiz;
    List<int> quizScores = new List<int>();
    int currentScore = 0;

    protected override async Task OnInitializedAsync()
    {
        quiz = await Http.GetJsonAsync<List<QuizItem>>("Quiz");
    }

    void UpdateScore(int chosenAnswerIndex, int quizIndex)
    {
        var quizItem = quiz[quizIndex];

        if (chosenAnswerIndex == quizItem.AnswerIndex)
        {
            quizScores[quizIndex] = quizItem.Score;
        } else
        {
            quizScores[quizIndex] = 0;
        }
        currentScore = quizScores.Sum();
    }
}

If you compare this code with the QuizViewer Razor component you created before, you may notice that the only differences are the injection of the HttpClient instance and the body of the OnInitializedAsync() method. Regarding the latter, you are making an HTTP GET call to the Quiz endpoint of the Web API implemented before.

To complete your application, replace the content of the NavMenu.razor file in the Client/Shared folder with the following code:

// Shared/NavMenu.razor

 <div class="top-row pl-4 navbar navbar-dark">
   <a class="navbar-brand" href="">QuizManager</a>
   <button class="navbar-toggler" @onclick="ToggleNavMenu">
       <span class="navbar-toggler-icon"></span>
   </button>
 </div>

 <div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
   <ul class="nav flex-column">
       <li class="nav-item px-3">
           <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
               <span class="oi oi-home" aria-hidden="true"></span> Home
           </NavLink>
       </li>
       <li class="nav-item px-3">
           <NavLink class="nav-link" href="quizViewer">
              <span class="oi oi-list-rich" aria-hidden="true"></span> Quiz
           </NavLink>
       </li>
   </ul>
 </div>

@code {
   bool collapseNavMenu = true;
   string NavMenuCssClass => collapseNavMenu ? "collapse" : null;

   void ToggleNavMenu()
   {
       collapseNavMenu = !collapseNavMenu;
   }
}

As you may have noticed, this is the same NavMenu.razor file you created in the Blazor Server application. As you remember, it redefines the navigation menu, including an item to reach the QuizViewer component.

Running your Blazor WebAssembly Application

Your Blazor WebAssembly application is complete. Just move into the Server folder and type the following command:

dotnet run

Pointing your browser to the https://localhost:5001 address, you should see the same Web UI as in the Blazor server implementation case.

Even if the look and feel are basically the same, the application architecture is quite different. In this case, you have the client side compiled into WebAssembly and running in your browser, while the server side is running in the built-in Web server. In addition, with this architecture, the client and the server interact with classic HTTP requests. You can check this by analyzing the network traffic with the developer tools of your browser.

Securing the Application with Auth0

Now you have two versions of the same quiz Web application: one implementing the Blazor Server hosting model and one implementing the Blazor WebAssembly one. In order to secure this application, you will learn how to integrate it with Auth0 services. The Blazor WebAssembly hosting model is not yet officially released, so this tutorial will only cover how to integrate Auth0 with the Blazor Server hosting model.

Creating the Auth0 application

The first step to secure your Blazor Server application is to access the Auth0 Dashboard in order to create your Auth0 application. If you haven't created an Auth0 account, you can sign up for a free one now.

Once in the dashboard, move to the Applications section and follow these steps:

  1. Click on Create Application
  2. Provide a friendly name for your application (for example, Quiz Blazor Server App) and choose Regular Web Applications as an application type
  3. Finally, click the Create button

These steps make Auth0 aware of your Blazor application and will allow you to control access.

After the application has been created, move in the Settings tab and take note of your Auth0 domain, your client id, and your client secret. Then, in the same form, assign the value https://localhost:5001/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 the user authentication. The second value tells Auth0 which URL a user should be redirected to after their logout.

Click the Save Changes button to apply them.

Configuring your Blazor application

Open the appsettings.json file in the root folder of your Blazor Server project and replace its content with the following:

// appsettings.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "Auth0": {
    "Domain": "YOUR_AUTH0_DOMAIN",
    "ClientId": "YOUR_CLIENT_ID",
    "ClientSecret": "YOUR_CLIENT_SECRET"
  }
}

Replace the placeholders YOUR_AUTH0_DOMAIN, YOUR_CLIENT_ID, and YOUR_CLIENT_SECRET with the respective values taken from the Auth0 dashboard.

Integrating with Auth0

Now, install the Microsoft.AspNetCore.Authentication.OpenIdConnect library by typing the following command in a terminal window:

dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect

After the installation is complete, open the Startup.cs file and replace the current using section with the following code:

// Startup.cs

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using QuizManager.Data;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authentication.Cookies;

//leave the rest untouched

Now, replace the definition of the ConfigureServices() method with the following code:

// Startup.cs

public void ConfigureServices(IServiceCollection services)
{
  services.AddRazorPages();
  services.AddServerSideBlazor();

  services.Configure<CookiePolicyOptions>(options =>
  {
    // This lambda determines whether user consent for non-essential cookies is needed for a given request.
    options.CheckConsentNeeded = context => true;
    options.MinimumSameSitePolicy = SameSiteMode.None;
  });

  // Add authentication services
  services.AddAuthentication(options =>
  {
    options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
  })
  .AddCookie()
  .AddOpenIdConnect("Auth0", options =>
  {
    // Set the authority to your Auth0 domain
    options.Authority = $"https://{Configuration["Auth0:Domain"]}";

    // Configure the Auth0 Client ID and Client Secret
    options.ClientId = Configuration["Auth0:ClientId"];
    options.ClientSecret = Configuration["Auth0:ClientSecret"];

    // Set response type to code
    options.ResponseType = "code";

    // Configure the scope
    options.Scope.Clear();
    options.Scope.Add("openid");

    // Set the callback path, so Auth0 will call back to http://localhost:3000/callback
    // Also ensure that you have added the URL as an Allowed Callback URL in your Auth0 dashboard
    options.CallbackPath = new PathString("/callback");

    // Configure the Claims Issuer to be Auth0
    options.ClaimsIssuer = "Auth0";

    options.Events = new OpenIdConnectEvents
    {
      // handle the logout redirection
      OnRedirectToIdentityProviderForSignOut = (context) =>
        {
          var logoutUri = $"https://{Configuration["Auth0:Domain"]}/v2/logout?client_id={Configuration["Auth0:ClientId"]}";

          var postLogoutUri = context.Properties.RedirectUri;
          if (!string.IsNullOrEmpty(postLogoutUri))
          {
            if (postLogoutUri.StartsWith("/"))
            {
              // transform to absolute
              var request = context.Request;
              postLogoutUri = request.Scheme + "://" + request.Host + request.PathBase + postLogoutUri;
            }
            logoutUri += $"&returnTo={ Uri.EscapeDataString(postLogoutUri)}";
          }

          context.Response.Redirect(logoutUri);
          context.HandleResponse();

          return Task.CompletedTask;
        }
    };
  });

  services.AddHttpContextAccessor();
  services.AddSingleton<QuizService>();
}

Here you are configuring the Blazor application in order to support authentication via OpenID Connect. The Auth0 configuration parameters are taken from the appsetting.json configuration file.

Also, replace the Configure() method in the Startup.cs file with the following definition:

// Startup.cs

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
  if (env.IsDevelopment())
  {
    app.UseDeveloperExceptionPage();
  }
  else
  {
    app.UseExceptionHandler("/Error");
    app.UseHsts();
  }

  app.UseHttpsRedirection();
  app.UseStaticFiles();

  app.UseRouting();

  app.UseCookiePolicy();
  app.UseAuthentication();
  app.UseAuthorization();

  app.UseEndpoints(endpoints =>
  {
    endpoints.MapBlazorHub();
    endpoints.MapFallbackToPage("/_Host");
  });
}

You simply added the middleware to manage the cookie policy, the authentication, and the authorization processes. Now your application has the infrastructure to support authentication.

Securing the server side

In order to prevent unauthorized users from accessing the server-side functionalities of your application, you need to protect them. So, open the Index.razor file in the Pages folder and add the Authorize attribute as shown in the following:

// Pages/Index.razor

@page "/"
@attribute [Authorize]

 <h1>Hello, world!</h1>

Welcome to your new app.

Also add the same attribute to the QuizViewer.razor component:

// Pages/QuizViewer.razor

@page "/quizViewer"
@attribute [Authorize]

@using QuizManager.Data
@inject QuizService QuizRepository

//leave the rest untouched

This ensures that the server-side rendering of your pages is triggered only by authorized users.

Creating login and logout endpoints

As said before, in the Blazor Server hosting model, the communication between the client side and the server side does not occur over HTTP, but through SignalR. Since Auth0 uses standard protocols like OpenID and OAuth that rely on HTTP, you need to provide a way to bring those protocols on Blazor.

To solve this issue, you are going to create two endpoints, /login and /logout, that redirect requests for login and for logout to Auth0. Two standard Razor pages respond behind these endpoints.

So, add the Login razor page to the project by typing the following command in a terminal window:

dotnet new page --name Login --namespace QuizManager.Pages --output Pages

This command creates two files in the Pages folder: Login.cshtml and Login.cshtml.cs.

Open the Login.cshtml.cs file in the Pages folder and replace its content with the following:

// Pages/Login.cshtml.cs

using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace QuizManager.Pages
{
    public class LoginModel : PageModel
    {
        public async Task OnGet(string redirectUri)
        {
            await HttpContext.ChallengeAsync("Auth0", new AuthenticationProperties
            {
                RedirectUri = redirectUri
            });
        }
    }
}

This code starts the challenge for the Auth0 authentication scheme you defined in the Startup class.

Then, add the Logout page by typing the following command:

dotnet new page --name Logout --namespace QuizManager.Pages --output Pages

Similarly to the previous case, you will get two new files in the Pages folder: Logout.cshtml and Logout.cshtml.cs.

Replace the content of the Logout.cshtml.cs file with the following code:

// Pages/Logout.cshtml.cs

using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace QuizManager.Pages
{
    public class LogoutModel : PageModel
    {
       public async Task<IActionResult> OnGet()
        {
            await HttpContext.SignOutAsync();
            return Redirect("/");
        }
    }
}

This code closes the user's session on Auth0.

Securing the client side

Now, you have to secure the client side of your Blazor application, so that the users see different content when they are logged in or not.

Open the App.razor file in the root folder of the project and replace its content with the following markup:

// 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>

Here you are using the AuthorizeRouteView component, which displays the associated component only if the user is authorized. In practice, the content of the MainLayout component will be shown only to authorized users. If the user is not authorized, they will see the content wrapped by the NotAuthorized component. If the authorization is in progress, the user will see the content inside the Authorizing component.

"The AuthorizeRouteView component allows you to control access to the UI parts of your Blazor application."

At this point, make sure you're in the QuizManager folder and run dotnet run in your terminal. When a user tries to access your application, they will see just the not authorized message, as shown in the following picture:

Unauthorized view in Blazor application

So, you need a way to let users authenticate. Create a razor component for this purpose by adding an AccessControl.razor file in the Shared folder with the following content:

// Shared/AccessControl.razor

<AuthorizeView>
    <Authorized>        
        <a href="logout">Log out</a>
    </Authorized>
    <NotAuthorized>
        <a href="login?redirectUri=/">Log in</a>
    </NotAuthorized>
</AuthorizeView>

This component uses the Authorized component to let the authorized users see the Log out link and the NotAuthorized component to let the unauthorized users access the Log in link. Both links point to the endpoints you created before. In particular, the Log in link specifies the home page as the URI where to redirect users after authentication.

The final step is to put this component in the top bar of your Blazor application. So, replace the content of the MainLayout.razor file with the following content:

// 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>

As you can see, the only difference is the addition of the AccessControl component just before the About link.

Now, your Blazor application is accessible just to authorized users. When users click the Log in link, they will be redirected to the Auth0 Universal Login page for authentication. After authentication completes, they will be back to the home page of your application and will be able to take their quiz.

Recap

This article introduced the basics of Blazor, the programming framework that allows you to build Web client applications by using C# and the .NET Core platform. You learned about the two hosting models, Blazor Server and Blazor WebAssembly, and built a quiz manager application step by step in both hosting models.

Finally, you secured the Blazor Server version of the application by integrating it with Auth0.

The full source code of both applications can be downloaded from this GitHub repository.