Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Customizing the AuthenticationStateProvider in Blazor Server App with Jwt Token Authentication

I've noticed that many developers subclass the AuthenticationStateProvider both in Blazor Server App and Blazor WebAssembly App wrongly, and more imprtantly for the wrong reasons.

How to do it correctly and when ?

like image 862
enet Avatar asked Jun 23 '20 06:06

enet


People also ask

How do I add authentication to Blazor server app?

To enable authentication for the Blazor server-side app, select the Configure for HTTPS check box in the Advanced section. Then, click the Change link in the Authentication section. Syncfusion's Blazor components suite is the expert's choice for building modern web apps.

How do I use Blazor authorize attribute?

Authorize attribute in Blazor and AuthorizeRouteView component. In the following example, [Authorize] attribute is used in it's simplest form, without any parameters (i.e roles or policies), so, it only checks if the user is authenticated. If the user is authenticated, then the authorization is granted otherwise not.

How do you secure Blazor WebAssembly?

Blazor WebAssembly apps are secured in the same manner as single-page applications (SPAs). There are several approaches for authenticating users to SPAs, but the most common and comprehensive approach is to use an implementation based on the OAuth 2.0 protocol, such as OpenID Connect (OIDC).


1 Answers

First off, you do not subclass the AuthenticationStateProvider for the sole purpose of adding claims to the ClaimPrincipal object. Generally speaking, claims are added after a user has been authenticated, and if you need to inspect those claims and tranform them, it should be done somewhere else, not in the AuthenticationStateProvider object. Incidentally, in Asp.Net Core there are two ways how you can do that, but this merits a question of its own.

I guess that this code sample led many to believe that this is the place to add claims to the ClaimsPrincipal object.

In the current context, implementing Jwt Token Authentication, claims should be added to the Jwt Token when it is created on the server, and extracted on the client when required, as for instance, you need the name of the current user. I've noticed that developers save the name of the user in the local storage, and retrieved it when needed. This is wrong. You should extract the name of the user from the Jwt Token.

The following code sample describes how to create a custom AuthenticationStateProvider object whose objective is to retrieve from the local storage a Jwt Token string that has newly added, parse its content, and create a ClaimsPrincipal object that is served to interested parties (subscribers to the AuthenticationStateProvider.AuthenticationStateChanged event) , such as the CascadingAuthenticationState object.

The following code sample demonstrates how you can implement a custom authenticationstateprovider properly, and for good reason.

public class TokenServerAuthenticationStateProvider : 
                                AuthenticationStateProvider
    {
        private readonly IJSRuntime _jsRuntime;
       
        public TokenServerAuthenticationStateProvider(IJSRuntime jsRuntime)
        {
            _jsRuntime = jsRuntime;
           
           
        }

       public async Task<string> GetTokenAsync()
            => await _jsRuntime.InvokeAsync<string>("localStorage.getItem", "authToken");

        public async Task SetTokenAsync(string token)
        {
            if (token == null)
            {
                await _jsRuntime.InvokeAsync<object>("localStorage.removeItem", "authToken");
            }
            else
            {
                await _jsRuntime.InvokeAsync<object>("localStorage.setItem", "authToken", token);
            }
            
            NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
        }

        public override async Task<AuthenticationState> GetAuthenticationStateAsync()
        {
            var token = await GetTokenAsync();
            var identity = string.IsNullOrEmpty(token)
                ? new ClaimsIdentity()
                : new ClaimsIdentity(ServiceExtensions.ParseClaimsFromJwt(token), "jwt");
            return new AuthenticationState(new ClaimsPrincipal(identity));
        }
    }

And here's a code sample residing in the submit button of a Login page that calls a Web Api endpoint where the user credentials are validated, after which a Jwt Token is created and passed back to the calling code:

async Task SubmitCredentials()
{

    bool lastLoginFailed;

    var httpClient = clientFactory.CreateClient();
    httpClient.BaseAddress = new Uri("https://localhost:44371/");

    var requestJson = JsonSerializer.Serialize(credentials, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });


    var response = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Post, "api/user/login")
    {
        Content = new StringContent(requestJson, Encoding.UTF8, "application/json")
    });

    var stringContent = await response.Content.ReadAsStringAsync();

    var result = JsonSerializer.Deserialize<LoginResult>(stringContent, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });

    lastLoginFailed = result.Token == null;
    if (!lastLoginFailed)
    {
        // Success! Store token in underlying auth state service
        await TokenProvider.SetTokenAsync(result.Token);
        NavigationManager.NavigateTo(ReturnUrl);
        
    }
}

Point to note: TokenProvider is an instance of TokenServerAuthenticationStateProvider. 
Its name reflects its functionality: handling the recieved Jwt Token, and providing 
the Access Token when requested.

This line of code: TokenProvider.SetTokenAsync(result.Token); passes the Jwt Token 
to TokenServerAuthenticationStateProvider.SetTokenAsync in which the token is sored 
in the local storage, and then raises AuthenticationStateProvider.AuthenticationStateChanged
event by calling NotifyAuthenticationStateChanged, passing an AuthenticationState object
built from the data contained in the stored Jwt Token.


Note that the GetAuthenticationStateAsync method creates a new ClaimsIdentity object from 
the parsed Jwt Token. All the claims added to the newly created ClaimsIdentity object 
are retrieved from the Jwt Token. I cannot think of a use case where you have to create
a new claim object and add it to the ClaimsPrincipal object.

The following code is executed when an authenticated user is attempting to access
the FecthData page

@code 
{
   private WeatherForecast[] forecasts;


protected override async Task OnInitializedAsync()
{
    var token = await TokenProvider.GetTokenAsync();
   
    var httpClient = clientFactory.CreateClient();
    httpClient.BaseAddress = new Uri("https://localhost:44371/");
    httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);

    var response = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, $"api/WeatherForecast?startDate={DateTime.Now}"));
    
    var stringContent = await response.Content.ReadAsStringAsync();

    forecasts = JsonSerializer.Deserialize<WeatherForecast[]>(stringContent, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
   
}

}

Note that the first line of code: var token = await TokenProvider.GetTokenAsync(); retrieves the Jwt Token stored in the local storage, and add it to the Authorization header of the request.

Hope this helps...

Edit

Note: ServiceExtensions.ParseClaimsFromJwt is a method that gets the Jwt token extracted from the local storage, and parse it into a collection of claims.

Your Startup class should be like this:

public void ConfigureServices(IServiceCollection services)
   {
      // Code omitted...

      services.AddScoped<TokenServerAuthenticationStateProvider>();
      services.AddScoped<AuthenticationStateProvider>(provider =>  provider.GetRequiredService<TokenServerAuthenticationStateProvider>());

  }
like image 75
enet Avatar answered Oct 09 '22 21:10

enet