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