.NET 6.0 Blazor WebAssembly JWT Token Authentication From Scratch C# Tutorial

In the previous tutorial, we built an API that creates and issues JWT tokens to authenticated users. If you haven’t already completed this tutorial, it is recommended to do so as this API will be the server project in our solution and will be used to issue JWT tokens to our client.

In this tutorial, we will be building a client in Blazor WebAssembly .NET 6.0 that consumes this API and implements JWT token authentication.

Our Blazor WebAssembly application will allow the user to register and log in. Upon successful authentication of the user, a JWT token will be obtained and stored. This token will then be used to determine whether or not a user is authorized to view a resource.

By the end of this tutorial, we will have built a Blazor WebAssembly application that implements JWT token authentication from scratch. To start this project, I’ve created a blank Blazor WebAssembly application in the same solution as the API and specified no authentication (as we are building this from scratch).

Registering a Http Client & User Registration

Ensure that the UserRegisterDTO and UserRegisterResultDTO that are in the shared library look like the following:

public class UserRegisterDTO
    {
        [Required]
        [EmailAddress]
        public string? Email { get; set; }

        [Required]
        [DataType(DataType.Password)]
        public string? Password { get; set; }

        [Required]
        [DataType(DataType.Password)]
        [Compare(nameof(Password), ErrorMessage = "The passwords do not match.")]
        public string? ConfirmPassword { get; set; }
    }
public class UserRegisterResultDTO
    {
        public bool Succeeded { get; set; }

        public IEnumerable<string> Errors { get; set; }
    }

Next, we will create a typed HTTP client to send and retrieve data to and from our API. Create a Clients folder and inside it add the following AuthenticationHttpClient:

public class AuthenticationHttpClient
    {
        private readonly ILogger<AuthenticationHttpClient> logger;
        private readonly HttpClient http;

        public AuthenticationHttpClient(ILogger<AuthenticationHttpClient> logger,
            HttpClient http)
        {
            this.logger = logger;
            this.http = http;
        }

        public async Task<UserRegisterResultDTO> RegisterUser(UserRegisterDTO userRegisterDTO)
        {
            try
            {
                var response = await http.PostAsJsonAsync("user/register", userRegisterDTO);
                var result = await response.Content.ReadFromJsonAsync<UserRegisterResultDTO>();
                return result;
            }
            catch (Exception ex)
            {
                logger.LogError(ex.Message);

                return new UserRegisterResultDTO
                {
                    Succeeded = false,
                    Errors = new List<string>()
                    {
                        "Sorry, we were unable to register you at this time. Please try again shortly."
                    }
                };
            }
        }
    }

Our HTTP client needs registering, so first create an appsettings and place inside it the URL to our JWT API:

{
  "APIBaseAddress": "https://localhost:7143"
}

Now our HTTP client can be registered in Program.cs:

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

string apiBaseAddress = builder.Configuration["APIBaseAddress"];

if (string.IsNullOrWhiteSpace(apiBaseAddress))
    throw new InvalidOperationException("APIBaseAddress missing from appsettings file.");

builder.Services.AddHttpClient<AuthenticationHttpClient>(client =>
    client.BaseAddress = new Uri(apiBaseAddress));

await builder.Build().RunAsync();

Now we can bring together all the code we have written into a page. Create a Classes folder and add the following CustomValidation:

public class CustomValidation : ComponentBase
    {
        private ValidationMessageStore? messageStore;

        [CascadingParameter]
        private EditContext? CurrentEditContext { get; set; }

        protected override void OnInitialized()
        {
            if (CurrentEditContext is null)
            {
                throw new InvalidOperationException(
                    $"{nameof(CustomValidation)} requires a cascading " +
                    $"parameter of type {nameof(EditContext)}. " +
                    $"For example, you can use {nameof(CustomValidation)} " +
                    $"inside an {nameof(EditForm)}.");
            }

            messageStore = new(CurrentEditContext);

            CurrentEditContext.OnValidationRequested += (s, e) =>
                messageStore?.Clear();
            CurrentEditContext.OnFieldChanged += (s, e) =>
                messageStore?.Clear(e.FieldIdentifier);
        }

        public void DisplayErrors(Dictionary<string, List<string>> errors)
        {
            if (CurrentEditContext is not null)
            {
                foreach (var err in errors)
                {
                    messageStore?.Add(CurrentEditContext.Field(err.Key), err.Value);
                }

                CurrentEditContext.NotifyValidationStateChanged();
            }
        }

        public void ClearErrors()
        {
            messageStore?.Clear();
            CurrentEditContext?.NotifyValidationStateChanged();
        }
    }

Create a new Blazor component titled Register.razor and add the following code:

@page "/register"
@inject AuthenticationHttpClient Http

<PageTitle>Register</PageTitle>

<h1>Register</h1>

@if (!succeeded)
{
    <EditForm Model="@userRegisterDTO" OnValidSubmit="@HandleValidSubmit">
        <CustomValidation @ref="customValidation" />
        <DataAnnotationsValidator />
        <ValidationSummary />

        <div class="mb-3">
            <InputText class="form-control" id="Email" @bind-Value="userRegisterDTO.Email" placeholder="Email" />
        </div>

        <div class="mb-3">
            <InputText class="form-control" id="Password" @bind-Value="userRegisterDTO.Password" placeholder="Password" />
        </div>

        <div class="mb-3">
            <InputText class="form-control" id="ConfirmPassword" @bind-Value="userRegisterDTO.ConfirmPassword" placeholder="Confirm password" />
        </div>

        @if (!registering)
        {
            <button class="btn btn-primary" type="submit">Submit</button>
        }
        else
        {
            <p>
                Registering...
            </p>
        }
    </EditForm>
}
else
{
    <p>
        Registration successful! <a href="login">Click here to login</a>.
    </p>
}

@code {
    private UserRegisterDTO userRegisterDTO = new();
    private CustomValidation? customValidation;
    private bool registering;
    private bool succeeded;

    private async Task HandleValidSubmit()
    {
        registering = true;

        var result = await Http.RegisterUser(userRegisterDTO);

        if (result.Succeeded)
        {
            succeeded = true;
        }
        else
        {
            customValidation?.ClearErrors();
            var errors = new Dictionary<string, List<string>>();
            errors.Add("", result.Errors.ToList());
            customValidation?.DisplayErrors(errors);
        }

        registering = false;
    }
}

User Login

Now that we can register users, we’ll next add the ability to log in. Ensure the following UserLoginDTO, UserLoginResultDTO and TokenDTO exist in the shared class library:

public class UserLoginDTO
    {
        [Required]
        [EmailAddress]
        public string? Email { get; set; }

        [Required]
        [DataType(DataType.Password)]
        public string? Password { get; set; }
    }
public class UserLoginResultDTO
    {
        public bool Succeeded { get; set; }

        public string Message { get; set; }

        public TokenDTO Token { get; set; }
    }
public class TokenDTO
    {
        public string Token { get; set; }

        public DateTime Expiration { get; set; }
    }

Add the following LoginUser method to the AuthenticationHttpClient:

public async Task<UserLoginResultDTO> LoginUser(UserLoginDTO userLoginDTO)
        {
            try
            {
                var response = await http.PostAsJsonAsync("user/login", userLoginDTO);
                var result = await response.Content.ReadFromJsonAsync<UserLoginResultDTO>();
                return result;
            }
            catch (Exception ex)
            {
                logger.LogError(ex.Message);

                return new UserLoginResultDTO
                {
                    Succeeded = false,
                    Message = "Sorry, we were unable to log you in at this time. Please try again shortly."
                };
            }
        }

Now create the following Login.razor page:

@page "/login"
@inject AuthenticationHttpClient Http
@inject NavigationManager NavigationManager

<PageTitle>Login</PageTitle>

<h1>Login</h1>

<EditForm Model="@userLoginDTO" OnValidSubmit="@HandleValidSubmit">
    <CustomValidation @ref="customValidation" />
    <DataAnnotationsValidator />
    <ValidationSummary />

    <div class="mb-3">
        <InputText class="form-control" id="Email" @bind-Value="userLoginDTO.Email" placeholder="Email" />
    </div>

    <div class="mb-3">
        <InputText class="form-control" id="Password" @bind-Value="userLoginDTO.Password" placeholder="Password" />
    </div>

    @if (!loggingIn)
    {
        <button class="btn btn-primary" type="submit">Submit</button>
    }
    else
    {
        <p>
            Logging in...
        </p>
    }

</EditForm>

@code {
    private UserLoginDTO userLoginDTO = new();
    private CustomValidation? customValidation;
    private bool loggingIn;

    private async Task HandleValidSubmit()
    {
        loggingIn = true;

        var result = await Http.LoginUser(userLoginDTO);

        if (result.Succeeded)
        {
            NavigationManager.NavigateTo("dashboard");
        }
        else
        {
            customValidation?.ClearErrors();
            var errors = new Dictionary<string, List<string>>();
            errors.Add("", new List<string> { result.Message });
            customValidation?.DisplayErrors(errors);
        }

        loggingIn = false;
    }
}

Upon successful login, the user will be redirected to a dashboard page that implements authorization. We will create this page in a later section of this tutorial.

Storing JWT Token in Local Storage

Now that we can obtain our JWT token from the API via our Login method, we need a way of storing the JWT token to our client as this will determine the user’s authentication status.

First, install the Blazored.Localstorage NuGet package to the client project. Then create a folder named Services and create the following TokenService:

public interface ITokenService
    {
        Task<TokenDTO> GetToken();
        Task RemoveToken();
        Task SetToken(TokenDTO tokenDTO);
    }

    public class TokenService : ITokenService
    {
        private readonly ILocalStorageService localStorageService;

        public TokenService(ILocalStorageService localStorageService)
        {
            this.localStorageService = localStorageService;
        }

        public async Task SetToken(TokenDTO tokenDTO)
        {
            await localStorageService.SetItemAsync("token", tokenDTO);
        }

        public async Task<TokenDTO> GetToken()
        {
            return await localStorageService.GetItemAsync<TokenDTO>("token");
        }

        public async Task RemoveToken()
        {
            await localStorageService.RemoveItemAsync("token");
        }
    }

Be sure to reigster the service in Program.cs:

builder.Services.AddScoped<ITokenService, TokenService>();

We can then inject our ITokenService into our AuthenticationHttpClient and store the JWT token obtained from our API upon successful log in:

private readonly ILogger<AuthenticationHttpClient> logger;
        private readonly HttpClient http;
        private readonly ITokenService tokenService;

        public AuthenticationHttpClient(ILogger<AuthenticationHttpClient> logger,
            HttpClient http,
            ITokenService tokenService)
        {
            this.logger = logger;
            this.http = http;
            this.tokenService = tokenService;
        }

        public async Task<UserLoginResultDTO> LoginUser(UserLoginDTO userLoginDTO)
        {
            try
            {
                var response = await http.PostAsJsonAsync("user/login", userLoginDTO);
                var result = await response.Content.ReadFromJsonAsync<UserLoginResultDTO>();
                await tokenService.SetToken(result.Token);
                return result;
            }
            catch (Exception ex)
            {
                logger.LogError(ex.Message);

                return new UserLoginResultDTO
                {
                    Succeeded = false,
                    Message = "Sorry, we were unable to log you in at this time. Please try again shortly."
                };
            }
        }

Adding Authentication via Custom AuthenticationStateProvider

It is important to note that, althought we have a JWT token, Blazor WebAssembly runs on the client, meaning that authentication checks can be modified or bypassed. Authentication is used only to determine which UI options to show. It is important to perform authentication checks when calling the API. Authenticating calls to the server will be covered later in this tutorial.

In the Classes folder, add the following CustomAuthenticationStateProvider. This will be called when AuthorizeView or CascadingAuthenticationState is called to determine whether the user is authenticated or not:

public class CustomAuthenticationStateProvider : AuthenticationStateProvider
    {
        private readonly ITokenService tokenService;

        public CustomAuthenticationStateProvider(ITokenService tokenService)
        {
            this.tokenService = tokenService;
        }

        public void StateChanged()
        {
            NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
        }

        public override async Task<AuthenticationState> GetAuthenticationStateAsync()
        {
            var tokenDTO = await tokenService.GetToken();
            var identity = string.IsNullOrEmpty(tokenDTO?.Token) || tokenDTO?.Expiration < DateTime.Now
                ? new ClaimsIdentity()
                : new ClaimsIdentity(ParseClaimsFromJwt(tokenDTO.Token), "jwt");
            return new AuthenticationState(new ClaimsPrincipal(identity));
        }

        private static IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
        {
            var payload = jwt.Split('.')[1];
            var jsonBytes = ParseBase64WithoutPadding(payload);
            var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);
            return keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString()));
        }

        private static byte[] ParseBase64WithoutPadding(string base64)
        {
            switch (base64.Length % 4)
            {
                case 2: base64 += "=="; break;
                case 3: base64 += "="; break;
            }
            return Convert.FromBase64String(base64);
        }
    }

Next, install the Microsoft.AspNetCore.Components.Authorization NuGet package. Next, modify App.razor to wrap the app in CascadingAuthenticationState and to include AuthorizeRouteView:

<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(App).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                <Authorizing>
                    <text>Authorizing user, please wait...</text>
                </Authorizing>
                <NotAuthorized>
                    <text>Sorry, you are not authorized to access this page.</text>
                </NotAuthorized>
            </AuthorizeRouteView>
        </Found>
        <NotFound>
            <PageTitle>Not found</PageTitle>
            <LayoutView Layout="@typeof(MainLayout)">
                <p role="alert">Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

Now modify MainLayout.razor to show links to log in and reigster if the user is not authenticated, and show a link to log out if the user is authenticated:

@inherits LayoutComponentBase

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

    <main>
        <div class="top-row px-4">
            <AuthorizeView>
                <Authorized>
                    <text>Hello, @context.User.Identity.Name</text>
                    <a href="/logout">Logout</a>
                </Authorized>
                <NotAuthorized>
                    <a href="/login">Login</a>
                    <a href="/register">Register</a>
                </NotAuthorized>
            </AuthorizeView>
        </div>

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

Our CustomAuthenticationStateProvider and AuthenticationStateProvider must be registered in Program.cs:

builder.Services.AddAuthorizationCore();

builder.Services.AddScoped<CustomAuthenticationStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider>(provider => provider.GetRequiredService<CustomAuthenticationStateProvider>());

The main drawback to using AuthenticationStateProvider directly is that the component isn’t notified automatically if the underlying authentication state data changes. Upon a successful log in, we must notify the component that the authentication state has changed so that the UI knows to update and show the username instead of links to log in and register:

private readonly ILogger<AuthenticationHttpClient> logger;
        private readonly HttpClient http;
        private readonly ITokenService tokenService;
        private readonly CustomAuthenticationStateProvider myAuthenticationStateProvider;

        public AuthenticationHttpClient(ILogger<AuthenticationHttpClient> logger,
            HttpClient http,
            ITokenService tokenService,
            CustomAuthenticationStateProvider myAuthenticationStateProvider)
        {
            this.logger = logger;
            this.http = http;
            this.tokenService = tokenService;
            this.myAuthenticationStateProvider = myAuthenticationStateProvider;
        }

        public async Task<UserLoginResultDTO> LoginUser(UserLoginDTO userLoginDTO)
        {
            try
            {
                var response = await http.PostAsJsonAsync("user/login", userLoginDTO);
                var result = await response.Content.ReadFromJsonAsync<UserLoginResultDTO>();
                await tokenService.SetToken(result.Token);
                myAuthenticationStateProvider.StateChanged();
                return result;
            }
            catch (Exception ex)
            {
                logger.LogError(ex.Message);

                return new UserLoginResultDTO
                {
                    Succeeded = false,
                    Message = "Sorry, we were unable to log you in at this time. Please try again shortly."
                };
            }
        }

User Logout

CustomAuthenticationStateProvider uses the JWT token stored in local storage to determine whether the user is authenticated or not. So when the user logs out, we must remove the token from local storage.

Add the following LogoutUser method to AuthenticationHttpClient. You will notice that we are again notifying the component that the authentication state has changed. This is so that we can display the log in and reigster links once again:

public async Task LogoutUser()
        {
            await tokenService.RemoveToken();
            myAuthenticationStateProvider.StateChanged();
        }

Now add the following Logout.razor page:

@page "/logout"
@inject AuthenticationHttpClient Http
@inject NavigationManager NavigationManager

<text>Logging out...</text>

@code {
    protected override async Task OnInitializedAsync()
    {
        await Http.LogoutUser();
        NavigationManager.NavigateTo("login");
    }
}

Preventing Access to a Page Using the [Authorize] Attribute

We can prevent access to a page if the user is not logged in by using the [Authorize] attribute:

@page "/dashboard"
@attribute [Authorize]

<PageTitle>Dashboard</PageTitle>

<h1>Dashboard</h1>

<p>Welcome to the dashboard!</p>

Our router defined in App.razor will determine whether the user is authenticated or not and display an appropriate message:

Authenticating an API Call From Blazor WebAssembly Using JWT Token Authentication

Firstly, ensure the following WeatherForecastDTO and GetWeatherForecast API endpoint exist in the shared library and API respectively:

public class WeatherForecastDTO
    {
        public DateTime Date { get; set; }

        public int TemperatureC { get; set; }

        public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);

        public string? Summary { get; set; }
    }
[Authorize]
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        [HttpGet(Name = "GetWeatherForecast")]
        public IEnumerable<WeatherForecastDTO> Get()
        {
            return Enumerable.Range(1, 5).Select(index => new WeatherForecastDTO
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = Summaries[Random.Shared.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }

In the Clients folder, create the following WeatherForecastHttpClient:

public class WeatherForecastHttpClient
    {
        private readonly HttpClient http;
        private readonly ITokenService tokenService;

        public WeatherForecastHttpClient(HttpClient http, ITokenService tokenService)
        {
            this.http = http;
            this.tokenService = tokenService;
        }

        public async Task<WeatherForecastDTO[]> GetForecastAsync()
        {
            try
            {
                var token = await tokenService.GetToken();

                if (token != null && token.Expiration > DateTime.Now)
                {
                    http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue($"Bearer", $"{token.Token}");
                }

                return await http.GetFromJsonAsync<WeatherForecastDTO[]>("WeatherForecast");
            }
            catch
            {
                return new WeatherForecastDTO[0];
            }
        }
    }

The JWT is obtained from local storage and added to the HTTP client headers. Don’t forget to register this HTTP client in Program.cs:

builder.Services.AddHttpClient<WeatherForecastHttpClient>(client =>
    client.BaseAddress = new Uri(apiBaseAddress));

Finally, we can add our page to bring it all together:

@page "/fetchdata"
@attribute [Authorize]
@inject WeatherForecastHttpClient Http

<PageTitle>Weather forecast</PageTitle>

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from the server.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private WeatherForecastDTO[]? forecasts;

    protected override async Task OnInitializedAsync()
    {
        forecasts = await Http.GetForecastAsync();
    }
}
Scroll to Top