Mario Duzioni
Mario Duzioni

Reputation: 43

Get User Identity in Blazor WebAssembly API Controller server side with custom JWT AuthenticationStateProvider

I'm using a custom JWT AuthenticationStateProvider from this sample both for Blazor Server and Blazor WebAssembly.

   public class JwtAuthenticationStateProvider : AuthenticationStateProvider
    {
        private readonly ISessionStorageService sessionStorageService;
        private ClaimsPrincipal anonymous = new ClaimsPrincipal(new ClaimsIdentity());

        public JwtAuthenticationStateProvider(ISessionStorageService sessionStorageService)
        {
            this.sessionStorageService = sessionStorageService;
        }

        public override async Task<AuthenticationState> GetAuthenticationStateAsync()
        {
            try
            {
                var userSession = await sessionStorageService.ReadEncryptedItemAsync<UserSession>("UserSession");
                if (userSession == null) return await Task.FromResult(new AuthenticationState(anonymous));
                var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>
                {
                    new Claim(ClaimTypes.Name, userSession.UserName),
                    new Claim(ClaimTypes.Role, userSession.Role)
                }, "JwtAuth"));
                return await Task.FromResult(new AuthenticationState(claimsPrincipal));
            }
            catch (Exception ex)
            {
                //You can log exception
                return await Task.FromResult(new AuthenticationState(anonymous));
            }
        }

        public async Task UpdateAuthenticationState(UserSession? userSession)
        {
            ClaimsPrincipal claimsPrincipal;
            if (userSession != null)
            {
                claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>
                {
                    new Claim(ClaimTypes.Name, userSession.UserName),
                    new Claim(ClaimTypes.Role, userSession.Role)
                }));
                userSession.ExpiryTimeStamp = DateTime.Now.AddSeconds(userSession.ExpiresIn);
                await sessionStorageService.SaveItemEncryptedAsync("UserSession", userSession);
            }
            else
            {
                claimsPrincipal = anonymous;
                await sessionStorageService.RemoveItemAsync("UserSession");
            }
            NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(claimsPrincipal)));
        }

        public async Task<string> GetToken()
        {
            var result = string.Empty;
            try
            {
                var userSession = await sessionStorageService.ReadEncryptedItemAsync<UserSession>("UserSession");
                if (userSession != null && DateTime.Now < userSession.ExpiryTimeStamp) result = userSession.Token;
            }
            catch (Exception ex)
            {
                //You can log exception
            }
            return result;
        }
    }

In Blazor Server I'm able to get user identity name and roles via AuthenticationState, but in Blazor WebAssembly when I call API methods of server-side Controller via HttpClient, I can't get the caller username.

    [Route("api/[controller]")]
    [ApiController]
    public class DevController : ControllerBase
    {
        [HttpGet, Route("GetUserNameByUser")]
        public string GetUserNameByUser()
        {
            return User?.Identity?.Name ?? String.Empty;  //<<-- User?.Identity?.Name == null
        }


        [Microsoft.AspNetCore.Components.CascadingParameter] private Task<Microsoft.AspNetCore.Components.Authorization.AuthenticationState> AuthenticationState { get; set; }

        [HttpGet, Route("GetUserNameByAuthenticationState")]
        public async Task<ActionResult<string>> GetUserNameByAuthenticationState()
        {
            var currentUserName = (await AuthenticationState).User?.Identity?.Name;  //<<-- AuthenticationState == null
            return Ok(currentUserName);
        }
    }

Here is the AddAuthentication call in [myProject].Server.Program.cs:

builder.Services.AddAuthentication(o =>
{
    o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(o =>
{
    o.RequireHttpsMetadata = true;
    o.SaveToken = true;
    o.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(JwtAuthenticationManager.JwtSecurityKey)),
        ValidateIssuer = false,
        ValidateAudience = false
    };
});

Is there a "standard" way to add current user authentication information in HttpClient call header or similar?

P.S. There is a similar unsolved old question here, but I hope that in two years something has changed or clarified...:-)

Upvotes: 2

Views: 537

Answers (1)

Dimitris Maragkos
Dimitris Maragkos

Reputation: 11382

In your custom AuthenticationStateProvider inject the HttpClient and use this code to attach the bearer token in all requests:

_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);

Example implementation:

public class TokenAuthenticationStateProvider : AuthenticationStateProvider
{
    private readonly HttpClient _httpClient;
    private readonly ILocalStorageService _localStorage;

    public TokenAuthenticationStateProvider(HttpClient httpClient, ILocalStorageService localStorage)
    {
        _httpClient = httpClient;
        _localStorage = localStorage;
    }

    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var token = await GetTokenAsync();
        var anonymousState = new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));

        if (string.IsNullOrWhiteSpace(token))
        {
            return anonymousState;
        }

        var claims = ParseClaimsFromJwt(token);
        var expiry = claims.FirstOrDefault(c => c.Type == "exp");

        if (expiry == null)
        {
            return anonymousState;
        }

        var datetime = DateTimeOffset.FromUnixTimeSeconds(long.Parse(expiry.Value));

        if (datetime.UtcDateTime <= DateTime.UtcNow)
        {
            return anonymousState;
        }

        _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);

        var identity = new ClaimsIdentity(claims, "jwt");

        return new AuthenticationState(new ClaimsPrincipal(identity));
    }

    public async Task<string> GetTokenAsync()
        => await _localStorage.GetItemAsync<string>("authToken");

    public async Task SetTokenAsync(string token)
    {
        if (token == null)
        {
            await _localStorage.RemoveItemAsync("authToken");
        }
        else
        {
            await _localStorage.SetItemAsync("authToken", token);
        }

        NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
    }

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

Modify yours accordingly.

Upvotes: 2

Related Questions