Search code examples
authenticationcontrollerblazorwebassembly

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...:-)


Solution

  • 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.