Authentication and Role-based Authorization in Blazor WASM using JWT and AuthenticationStateProvider

Introduction

Authentication and authorization are critical aspects of web application development. In Blazor WebAssembly, you can implement robust authentication and role-based authorization using JSON Web Tokens (JWT) and the Authentication State Provider. In this blog post, we’ll explore how to set up authentication and control access to different parts of your application based on user roles.

GitHub Repository

I’m following the GitHub repository “BlazorAuthenticationTutorial” by Patrick God.

Getting Started

Create a Blazor WASM project named “BlazorAuthentication” using a template or create an empty one.

Install the following NuGet packages:

  • Microsoft.AspNetCore.Components.Authorization
  • Blazored.LocalStorage

Creating Custom Authentication State Provider

To set up a custom authentication state provider, create a file named CustomAuthStateProvider.cs in the project’s root directory. Add the necessary code to manage authentication state using JWT tokens.

using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text.Json;

namespace BlazorAuthenticationTutorial.Client
{
    public class CustomAuthStateProvider : AuthenticationStateProvider
    {
        private readonly ILocalStorageService _localStorage;
        private readonly HttpClient _http;

        public CustomAuthStateProvider(ILocalStorageService localStorage, HttpClient http)
        {
            _localStorage = localStorage;
            _http = http;
        }

        public override async Task<AuthenticationState> GetAuthenticationStateAsync()
        {
            string token = await _localStorage.GetItemAsStringAsync("token");

            var identity = new ClaimsIdentity();
            _http.DefaultRequestHeaders.Authorization = null;

            if (!string.IsNullOrEmpty(token))
            {
                identity = new ClaimsIdentity(ParseClaimsFromJwt(token), "jwt");
                _http.DefaultRequestHeaders.Authorization =
                    new AuthenticationHeaderValue("Bearer", token.Replace("\"", ""));
            }

            var user = new ClaimsPrincipal(identity);
            var state = new AuthenticationState(user);

            NotifyAuthenticationStateChanged(Task.FromResult(state));

            return state;
        }

        public 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);
        }
    }
}

Register the service in Program.cs by adding scoped services for AuthenticationStateProvider and BlazoredLocalStorage.

global using Microsoft.AspNetCore.Components.Authorization;
global using Blazored.LocalStorage;

using BlazorAuthenticationTutorial.Client;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;

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

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

//----- Register Service Here -----
builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();
builder.Services.AddAuthorizationCore();
//---------------------------------

builder.Services.AddBlazoredLocalStorage();

await builder.Build().RunAsync();

Setting Up Login Page

Create a Login.razor component in the Pages folder. Add a form with fields for username and password, and handle the login logic by sending credentials to the server, receiving a JWT, and storing it in local storage.

@page "/login"
@inject HttpClient Http
@inject AuthenticationStateProvider AuthStateProvider
@inject ILocalStorageService LocalStorage
@inject IConfiguration Configuration

<h3>Login</h3>
<EditForm Model="user" OnSubmit="HandleLogin">
    <label for="username">Name</label>
    <InputText id="username" @bind-Value="user.Username" />
    <label for="password">Password</label>
    <InputText id="password" @bind-Value="user.Password" type="password" />
    <button type="submit" class="btn btn-primary">Do it!</button>
</EditForm>

@code {
    UserLoginDto user = new UserLoginDto();
    async Task HandleLogin()
    {
        //var result = await Http.PostAsJsonAsync($"{Configuration["ApiUrl"]}api/auth", user);
        //var token = await result.Content.ReadAsStringAsync();

        //Just hard coding the token for now :)
        var token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlNoYWhyaXlhciIsInJvbGUiOiJBZG1pbiIsImlhdCI6MTUxNjIzOTAyMn0.l9E7Oypb-ozndpFUkeVhOYzhtjGEuFmdYdAxhbpXAFY";
        
        await LocalStorage.SetItemAsync("token", token);
        await AuthStateProvider.GetAuthenticationStateAsync();
    }
}

Customizing App.razor

Replace the code in App.razor to use CascadingAuthenticationState. Set up the router to use AuthorizeRouteView for protected routes and handle unauthorized access with a custom message.

 <CascadingAuthenticationState>
    <Router AppAssembly="@typeof(App).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    <p>Sorry dude, but you're not authorized!</p>
                </NotAuthorized>
            </AuthorizeRouteView>
            <FocusOnNavigate RouteData="@routeData" Selector="h1" />
        </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>

Adding Authorization to a Component

To add authorization to a component, use the Authorize attribute.

@attribute [Authorize]

For role-based authorization, specify roles using the Authorize(Roles = "RoleName") attribute.

@attribute [Authorize(Roles = "Admin")]

Conclusion

Implementing authentication and role-based authorization in Blazor WebAssembly using JWT and the Authentication State Provider is a powerful way to secure your application and control access based on user roles. By following these steps, you can ensure that users see and interact with only the content and features they are authorized to access, enhancing the security and usability of your web application.

0
An error has occurred. This application may no longer respond until reloaded. Reload x