Blazor Wasm – custom authentication – Part 2

After having done the view parts (Blazor Wasm – custom authentication – Part 1) now it’s time for the model.

Starting with reading the JWT into a List of Claims, so that we can pass it later into a own implementation of AuthenticationStateProvider.

using System.Security.Claims;
using System.Text.Json;

namespace tryout_blazor_api.Client.Util;

public static class JwtParser
{
    public static IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
    {
        Console.WriteLine("Parsing claims from JWT");
        var claims = new List<Claim>();
        var payload = jwt.Split('.')[1];
        
        var jsonBytes = ParseBase64WithoutPadding(payload);
        
        var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(jsonBytes);
        foreach(var kvp in keyValuePairs)
        {
            Console.WriteLine($"{kvp.Key}: ({kvp.Value.GetType()})");
          if(kvp.Value.ToString().Contains("["))
          {
            foreach(var role in kvp.Value.EnumerateArray()) {
              Console.WriteLine($"{kvp.Key}: {role}");
              claims.Add(new Claim(kvp.Key, role.ToString()));
            }
          }else{
            claims.Add(new Claim(kvp.Key, kvp.Value.ToString()));
          }
        }
        return claims;
    }
    private static byte[] ParseBase64WithoutPadding(string base64)
    {
        switch (base64.Length % 4)
        {
            case 2: base64 += "=="; break;
            case 3: base64 += "="; break;
        }
        return Convert.FromBase64String(base64);
    }
}

For internal authentication handling 2 classes are used. AuthenticationService will wrap authentication handling for Registration, Login and Logout. While AuthStateProvider will be the interface for Components like AuthorizeView getting logged in user data.

using System.Net.Http.Headers;
using System.Security.Claims;
using Blazored.LocalStorage;
using Microsoft.AspNetCore.Components.Authorization;
using tryout_blazor_api.Client.Util;

public class AuthStateProvider : AuthenticationStateProvider
{
    private readonly HttpClient _httpClient;
    private readonly ILocalStorageService _localStorage;
    private readonly AuthenticationState _anonymous;

    public AuthStateProvider(HttpClient httpClient, ILocalStorageService localStorage)
    {
        _httpClient = httpClient;
        _localStorage = localStorage;
        _anonymous = new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
    }
    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var token = await _localStorage.GetItemAsync<string>("authToken");
        if (string.IsNullOrWhiteSpace(token))
            return _anonymous;
        _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", token);
        return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(JwtParser.ParseClaimsFromJwt(token), "jwtAuthType")));
    }
    public void NotifyUserAuthentication(string token)
    {
        var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(JwtParser.ParseClaimsFromJwt(token), "jwtAuthType"));
        var authState = Task.FromResult(new AuthenticationState(authenticatedUser));
        NotifyAuthenticationStateChanged(authState);
    }
    public void NotifyUserLogout()
    {
        var authState = Task.FromResult(_anonymous);
        NotifyAuthenticationStateChanged(authState);
    }
}
using tryout_blazor_api.Shared.Auth;

namespace tryout_blazor_api.Client.Services
{
	public interface IAuthenticationService
	{
		Task Initialize();
		Task Register(RegisterModel regData);
		Task Login(LoginModel loginData);
		Task Logout();
	}
}
using Blazored.LocalStorage;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using tryout_blazor_api.Shared;
using tryout_blazor_api.Shared.Auth;

namespace tryout_blazor_api.Client.Services
{
    public class AuthenticationService : IAuthenticationService
    {
        private HttpClient _httpclient;
        private NavigationManager _navigationManager;
        private ILocalStorageService _localStorageService;
        private readonly AuthenticationStateProvider _authStateProvider;


        public AuthenticationService(
            HttpClient httpclient,
            NavigationManager navigationManager,
            ILocalStorageService localStorageService,
            AuthenticationStateProvider authStateProvider
        ) {
            _httpclient = httpclient;
            _navigationManager = navigationManager;
            _localStorageService = localStorageService;
            _authStateProvider = authStateProvider;
        }

        public async Task Initialize()
        {
            if(_httpclient.DefaultRequestHeaders.Authorization is null) {
                var token = await _localStorageService.GetItemAsStringAsync("authToken");
                if(token is not null) {
                    _httpclient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
                }
            }
        }

        public async Task Register(RegisterModel regData) {
            var response = await _httpclient.PostAsJsonAsync("Auth/register", regData);
            try
            {
                response.EnsureSuccessStatusCode();
            }catch(HttpRequestException ex)
            {
                throw new Exception(await response.Content.ReadAsStringAsync());
            }
            var responseString = await response.Content.ReadAsStringAsync();
            var resultRegister = JsonSerializer.Deserialize<Response>(responseString, new JsonSerializerOptions()
            {
                PropertyNameCaseInsensitive = true
            });
            _navigationManager.NavigateTo("/");
        }

        public async Task Login(LoginModel loginData)
        {
            var responseLogin = await _httpclient.PostAsJsonAsync("Auth/login", loginData);
            responseLogin.EnsureSuccessStatusCode();
            var responseString = await responseLogin.Content.ReadAsStringAsync();
            var resultLogin = JsonSerializer.Deserialize<LoginToken>(responseString, new JsonSerializerOptions()
            {
                PropertyNameCaseInsensitive = true
            });
            _httpclient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", resultLogin!.Token);

            await _localStorageService.SetItemAsStringAsync("authToken", resultLogin!.Token);
            ((AuthStateProvider)_authStateProvider).NotifyUserAuthentication(resultLogin!.Token);

            _navigationManager.NavigateTo("/");
        }

        public async Task Logout()
        {
            _httpclient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "INVALID");
            await _localStorageService.RemoveItemAsync("authToken");
            ((AuthStateProvider)_authStateProvider).NotifyUserLogout();
            _navigationManager.NavigateTo("/");
        }
    }
}

Finally make these classes known.

using Blazored.LocalStorage;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using tryout_blazor_api.Client;
using tryout_blazor_api.Client.Services;

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

builder.Services.AddOptions();
builder.Services.AddBlazoredLocalStorage();
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<IAuthenticationService, AuthenticationService>();
builder.Services.AddScoped<AuthenticationStateProvider, AuthStateProvider>();

await builder.Build().RunAsync();

Code can be found here: https://github.com/sukapx/tryout_blazor_api/tree/8351b7f0e025f883c075367cb56471a0fbb03727

Leave a Reply

Your email address will not be published. Required fields are marked *