Basic JWT authentication

🟥 Not applicable to Blazor Server

JWT are often used for authentication and authorization purposes in web applications. In this tutorial, you will be guided step-by-step on how to implement authentication in Blazor WebAssembly using JWT and the Identity model. The tutorial covers the 3 main authentication flows: login, revisiting the website, and logout. Additionally, a comprehensive template for building authentication will be provided.

  • Required NuGet libraries.
  • Project setup and basic classes.
  • Handling login flow.
  • Handling users revisit flow.
  • Handling logout flow.
  • Accessing user information.
  • Common mistakes.
You can download the example code used in this topic on GitHub.

Required NuGet libraries

To get started, make sure to install Microsoft.AspNetCore.Components.Authorization. Additionally, depending on your identity platform, you may need to install additional NuGet packages. For example, if you plan on using JWT, be sure to install System.IdentityModel.Tokens.Jwt.


Project setup and basic classes

  1. Install the required NuGet libraries. In this example, we will be using JWT, so you will need to install Microsoft.AspNetCore.Components.Authorization and System.IdentityModel.Tokens.Jwt from the NuGet package manager.
  2. In the _Imports.razor file, add @using Microsoft.AspNetCore.Components.Authorization to enable the use of the Microsoft.AspNetCore.Components.Authorization namespace.
  3. Set CascadingAuthenticationState as the root component in the App.razor file by adding the following code:
<CascadingAuthenticationState>
    <Router AppAssembly="typeof(App).Assembly">
        ...
    </Router>
</CascadingAuthenticationState>
  1. Add a browser storage for authentication. For the purpose of simplicity, we will use memory storage in this case. However, in a real project, you can use local storage and session storage as per your requirements. To do this, create a class named AuthenticationDataMemoryStorage with a Token property that is initialized to an empty string.
public class AuthenticationDataMemoryStorage
{
    public string Token { get; set; } = "";
}
  1. To store information about the users, create a User class. You will later use this class to retrieve user information. For example:
public class User
{
    public string Username { get; set; } = "";
    public string Password { get; set; } = "";
    public int Age { get; set; }
    public List<string> Roles { get; set; } = new();

    public ClaimsPrincipal ToClaimsPrincipal() => new(new ClaimsIdentity(new Claim[]
    {
        new (ClaimTypes.Name, Username),
        new (ClaimTypes.Hash, Password),
        new (nameof(Age), Age.ToString())
    }.Concat(Roles.Select(r => new Claim(ClaimTypes.Role, r)).ToArray()),
    "Blazor School"));

    public static User FromClaimsPrincipal(ClaimsPrincipal principal) => new()
    {
        Username = principal.FindFirst(ClaimTypes.Name)?.Value ?? "",
        Password = principal.FindFirst(ClaimTypes.Hash)?.Value ?? "",
        Age = Convert.ToInt32(principal.FindFirst(nameof(Age))?.Value),
        Roles = principal.FindAll(ClaimTypes.Role).Select(c => c.Value).ToList()
    };
}
When creating a new ClaimsIdentity, it's important to always pass the authenticationType parameter. In the example above, we used "Blazor School" as the authenticationType, but you can use any other string that identifies the authentication scheme being used. By including the authenticationType parameter, you ensure that the ClaimsIdentity is associated with the correct authentication scheme, which is necessary for proper authorization and authentication within your application. So, always make sure to include this parameter when creating a new ClaimsIdentity object.
  1. Create a class that integrate with the API and browser storage. For instance:
public class BlazorSchoolUserService
{
    private readonly HttpClient _httpClient;
    private readonly AuthenticationDataMemoryStorage _authenticationDataMemoryStorage;

    public BlazorSchoolUserService(HttpClient httpClient, AuthenticationDataMemoryStorage authenticationDataMemoryStorage)
    {
        _httpClient = httpClient;
        _authenticationDataMemoryStorage = authenticationDataMemoryStorage;
    }
}
Refer to the API Interaction tutorial for more information.
  1. Create a class that extends the AuthenticationStateProvider. For instance:
public class BlazorSchoolAuthenticationStateProvider : AuthenticationStateProvider
{
    private readonly BlazorSchoolUserService _blazorSchoolUserService;

    public BlazorSchoolAuthenticationStateProvider(BlazorSchoolUserService blazorSchoolUserService)
    {
        _blazorSchoolUserService = blazorSchoolUserService;
    }
}
  1. Register the browser storage accessor and custom AuthenticationStateProvider in the Program.cs file by adding the following code:
builder.Services.AddScoped<AuthenticationDataMemoryStorage>();
builder.Services.AddScoped<BlazorSchoolUserService>();
builder.Services.AddScoped<BlazorSchoolAuthenticationStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider>(sp => sp.GetRequiredService<BlazorSchoolAuthenticationStateProvider>());
builder.Services.AddAuthorizationCore();
For more information on browser storage, please refer to the Browser Storage tutorial.

Handling login flow

When the AuthenticationStateProvider receives the user's credentials, the first step is to validate them by sending a request to the API. Here are the steps to handle the login flow:

  1. In your user service, add a method called SendAuthenticateRequestAsync that sends an authentication request to the API and returns the authenticated user if the credentials are valid:
public class BlazorSchoolUserService
{
    ...
    public async Task<User?> SendAuthenticateRequestAsync(string username, string password)
    {
        var response = await _httpClient.GetAsync($"/example-data/{username}.json");

        if (response.IsSuccessStatusCode)
        {
            string token = await response.Content.ReadAsStringAsync();
            var claimPrincipal = CreateClaimsPrincipalFromToken(token);
            var user = User.FromClaimsPrincipal(claimPrincipal);
            PersistUserToBrowser(token);

            return user;
        }

        return null;
    }

    private ClaimsPrincipal CreateClaimsPrincipalFromToken(string token)
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        var identity = new ClaimsIdentity();

        if (tokenHandler.CanReadToken(token))
        {
            var jwtSecurityToken = tokenHandler.ReadJwtToken(token);
            identity = new(jwtSecurityToken.Claims, "Blazor School");
        }

        return new(identity);
    }

    private void PersistUserToBrowser(string token) => _authenticationDataMemoryStorage.Token = token;
}
  1. In your custom AuthenticationStateProvider, create a LoginAsync method that uses the SendAuthenticateRequestAsync method to authenticate the user's credentials and create a ClaimsPrincipal for the authenticated user. If the credentials are not valid, it creates an empty ClaimsPrincipal:
public class BlazorSchoolAuthenticationStateProvider : AuthenticationStateProvider
{
    ...
    public async Task LoginAsync(string username, string password)
    {
        var principal = new ClaimsPrincipal();
        var user = await _blazorSchoolUserService.SendAuthenticateRequestAsync(username, password);

        if (user is not null)
        {
            principal = user.ToClaimsPrincipal();
        }

        NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(principal)));
    }
}

Handling users revisit flow

When a user returns to your website, it's essential to check the browser storage to see if they have logged in previously. If their JWT is still valid, you can create a ClaimsPrincipal from it. If not, create an empty ClaimsPrincipal.

  1. In your user service, create a method to retrieve the JWT from browser storage:
public class BlazorSchoolUserService
{
    ...
    public User? FetchUserFromBrowser()
    {
        var claimsPrincipal = CreateClaimsPrincipalFromToken(_authenticationDataMemoryStorage.Token);
        var user = User.FromClaimsPrincipal(claimsPrincipal);

        return user;
    }
}
Please note that this code sample does not encrypt the user's password. In a real project, you should consider adding encryption.
  1. In your custom AuthenticationStateProvider, override the GetAuthenticationStateAsync method to create a ClaimsPrincipal if the credentials are valid, or create an empty one if they are invalid:
public class BlazorSchoolAuthenticationStateProvider : AuthenticationStateProvider
{
    ...
    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var principal = new ClaimsPrincipal();
        var user = _blazorSchoolUserService.FetchUserFromBrowser();

        if (user is not null)
        {
            var authenticatedUser = await _blazorSchoolUserService.SendAuthenticateRequestAsync(user.Username, user.Password);

            if (authenticatedUser is not null)
            {
                principal = authenticatedUser.ToClaimsPrincipal();
            }
        }

        return new(principal);
    }
}

Handling logout flow

When a user logs out, you must clear the browser storage and reset their authentication state.

  1. In your user storage, add a method to clear the browser storage:
public class BlazorSchoolUserService
{
    public void ClearBrowserUserData() => _authenticationDataMemoryStorage.Token = "";
}
  1. In your custom AuthenticationStateProvider, add a logout method:
public class BlazorSchoolAuthenticationStateProvider : AuthenticationStateProvider
{
    ...
    public void Logout()
    {
        _blazorSchoolUserService.ClearBrowserUserData();
        NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(new())));
    }
}

This method clears the user's data from the browser storage and creates a new empty ClaimsPrincipal, which effectively logs the user out. Finally, it notifies the authentication state has changed by calling NotifyAuthenticationStateChanged.


Accessing user information

When a user registers on your website, you may ask them to provide personal information such as age and email. Later, you might want to display this information when they log in. Here's how to access user information:

  1. Declare one or more properties in the User class and update the conversion methods as well. For example:
public class User
{
    ...
    public string FullName { get; set; } = "";

    public ClaimsPrincipal ToClaimsPrincipal() => new(new ClaimsIdentity(new Claim[]
    {
        ...
        new (nameof(FullName), FullName),
    }, "BlazorSchool"));

    public static User FromClaimsPrincipal(ClaimsPrincipal principal) => new()
    {
        ...
        FullName = principal.FindFirstValue(nameof(FullName))
    };
}
  1. Declare a property for the current user in the custom AuthenticationStateProvider. For example:
public class BlazorSchoolAuthenticationStateProvider : AuthenticationStateProvider
{
    ...
    public User CurrentUser { get; private set; } = new();
}
  1. Create a method to listen to the AuthenticationStateChanged event. For example:
public class BlazorSchoolAuthenticationStateProvider : AuthenticationStateProvider
{
    ...
    private async void OnAuthenticationStateChangedAsync(Task<AuthenticationState> task)
    {
        var authenticationState = await task;

        if (authenticationState is not null)
        {
            CurrentUser = User.FromClaimsPrincipal(authenticationState.User);
        }
    }
}
  1. Implement the IDisposable interface, subscribe and unsubscribe from the AuthenticationStateChanged event. For example:
public class BlazorSchoolAuthenticationStateProvider : AuthenticationStateProvider, IDisposable
{
    ...
    public BlazorSchoolAuthenticationStateProvider(BlazorSchoolUserService blazorSchoolUserService)
    {
        _blazorSchoolUserService = blazorSchoolUserService;
        AuthenticationStateChanged += OnAuthenticationStateChangedAsync;
    }

    public void Dispose() => AuthenticationStateChanged -= OnAuthenticationStateChangedAsync;
}
  1. Assign the value of CurrentUser in the GetAuthenticationStateAsync and login methods. For example:
public class BlazorSchoolAuthenticationStateProvider : AuthenticationStateProvider, IDisposable
{
    ...
    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        ...
        if (user is not null)
        {
            ...
            CurrentUser = authenticatedUser;
        }
    }

    public async Task LoginAsync(string username, string password)
    {
        ...
        CurrentUser = user;
        ...
    }
}

Now, you can access user information via the custom AuthenticationStateProvider class. For example:

@inject BlazorSchoolAuthenticationStateProvider BlazorSchoolAuthenticationStateProvider

<div>Full name: @BlazorSchoolAuthenticationStateProvider.CurrentUser.FullName</div>

Common mistakes

As a Blazor developer, there are some common mistakes that you should avoid to ensure your application runs smoothly. We have collected some of these mistakes from the Blazor School Discord community:

Mistake #1: Creating ClaimsIdentity without the authenticationType

When converting a user object to a ClaimsPrincipal in the User class, you need to create a ClaimsIdentity first before converting it to a ClaimsPrincipal. Unfortunately, many developers create a ClaimsIdentity without specifying the authenticationType. For example:

public class User
{
    ...
    // Wrong because create ClaimsPrincipal without authentication type.
    public ClaimsPrincipal ToClaimsPrincipal() => new(new ClaimsIdentity(new Claim[]
    {
        new (ClaimTypes.Name, Username),
        new (ClaimTypes.Hash, Password),
    }));
}

What happens if you don't specify the authenticationType?

The ClaimsIdentity will mark the user as unauthenticated, and the ClaimsIdentity.IsAuthenticated property will be false. To avoid this mistake, always specify the authenticationType when creating a ClaimsIdentity. For example:

public class User
{
    ...
    // Correct implementation with an authentication type specified.
    public ClaimsPrincipal ToClaimsPrincipal() => new(new ClaimsIdentity(new Claim[]
    {
        new (ClaimTypes.Name, Username),
        new (ClaimTypes.Hash, Password),
    }, "Blazor School"));
}

Mistake #2: Depend solely on CORS for security

CORS (Cross-Origin Resource Sharing) is a method used to restrict access to an API from unauthorized websites. However, CORS is only enforced by the web browser. Attackers can bypass the browser and send requests from external sources to access your API. Therefore, relying only on CORS for security is not sufficient. Additional measures, such as authentication and authorization, should be implemented to ensure that the API is secure.

Mistake #3: Incorrect usage of GetAuthenticationStateAsync from AuthenticationStateProvider to retrieve user information

GetAuthenticationStateAsync is not intended to be called directly in the component by AuthenticationStateProvider. The following code demonstrates an incorrect implementation:

// Bad code
@inject BlazorSchoolAuthenticationStateProvider BlazorSchoolAuthenticationStateProvider

<div>@Username</div>

@code {
    public string Username { get; set; } = "";

    protected override async Task OnInitializedAsync()
    {
        var authenticationState = await BlazorSchoolAuthenticationStateProvider.GetAuthenticationStateAsync();

        if (authenticationState is not null)
        {
            Username = authenticationState.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "";
        }
    }
}

To avoid making this mistake, it is recommended to follow the Accessing user information section.

What happens when you call GetAuthenticationStateAsync from AuthenticationStateProvider to retrieve user information?

If you call GetAuthenticationStateAsync directly from AuthenticationStateProvider to retrieve user information, it will result in the method being executed each time a component is rendered. This will cause multiple authentication requests to be sent to the API, which can negatively impact performance and result in unnecessary traffic.

Mistake #4: Incorrectly using HttpContextAccessor to retrieve user information

Using HttpContextAccessor to retrieve user information is a common mistake in Blazor. While MVC and Web API use this approach, it is not suitable for Blazor applications. This can lead to confusion and issues when attempting to access user information in Blazor.

Blazor is a Single Page Application framework, which means that only one request is sent and it cannot contain user information. To obtain user information in a Blazor application, it is recommended to follow the guidelines provided in the Accessing user information section of the tutorial.

Therefore, it is important to avoid using HttpContextAccessor in Blazor applications and to follow the recommended approach for obtaining user information to ensure that the application functions correctly and securely.

BLAZOR SCHOOL
Designed and built with care by our dedicated team, with contributions from a supportive community. We strive to provide the best learning experience for our users.
Docs licensed CC-BY-SA-4.0
Copyright © 2021-2024 Blazor School
An unhandled error has occurred. Reload 🗙