Basic authentication

🟥 Not applicable to Blazor WebAssembly

In this tutorial, you'll learn how to implement authentication from scratch and display authenticated users' information. We'll cover the three main authentication flows: login, revisiting the website, and logout.

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

Recommended NuGet libraries

For converting a C# object to JSON, we recommend using the Newtonsoft.Json library. In this tutorial, we'll be using this library. In addition to Newtonsoft.Json, there are several other libraries available for data serialization. You may choose to use a different library based on your project's specific needs and requirements.


Project setup and basic classes

  1. Install the required NuGet libraries.
  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. To enable authentication in your application, you'll need to add a browser storage. In this tutorial, we'll be using ProtectedLocalStorage, which provides access to encrypted local storage. However, you may choose to use a different browser storage type, such as vanilla local storage or session storage, depending on your specific needs and requirements.
  2. 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()),
    "BlazorSchool"));

    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 database and browser storage. For instance:
public class BlazorSchoolUserService
{
    private readonly ProtectedLocalStorage _protectedLocalStorage;
    private readonly string _blazorSchoolStorageKey = "blazorSchoolIdentity";

    public BlazorSchoolUserService(ProtectedLocalStorage protectedLocalStorage)
    {
        _protectedLocalStorage = protectedLocalStorage;
    }
}
  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 (if any) and custom AuthenticationStateProvider in the Program.cs file by adding the following code:
builder.Services.AddScoped<BlazorSchoolUserService>();
builder.Services.AddScoped<BlazorSchoolAuthenticationStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider>(sp 
    => sp.GetRequiredService<BlazorSchoolAuthenticationStateProvider>());
builder.Services.AddAuthorizationCore();

Handling login flow

To handle the login flow, the AuthenticationStateProvider first validates the user's credentials by querying the database. Here are the steps involved in this process:

  1. Add a method called FindUserFromDatabaseAsync to your user service. This method finds a user from the database and stores the user's information to the browser storage if a valid credential is provided. Here's an example implementation:
public class BlazorSchoolUserService
{
    ...
    public async Task<User?> FindUserFromDatabaseAsync(string username, string password)
    {
        var userInDatabase = ...;

        if (userInDatabase is not null)
        {
            await PersistUserToBrowserAsync(userInDatabase);
        }

        return userInDatabase;
    }

    public async Task PersistUserToBrowserAsync(User user)
    {
        string userJson = JsonConvert.SerializeObject(user);
        await _protectedLocalStorage.SetAsync(_blazorSchoolStorageKey, userJson);
    }
}

This method validates the user's credentials by checking if the provided username and password match the user data in the database. If a matching user is found, their information is stored in the browser storage using the PersistUserToBrowserAsync method.

Note: The example code assumes that you have a User class with properties for Username, Password, Roles, and Age. You may need to adjust this code to match your specific database schema and user model.

  1. In your custom AuthenticationStateProvider, create a LoginAsync method that uses the FindUserFromDatabaseAsync 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.FindUserFromDatabaseAsync(username, password);
        CurrentUser = user;

        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 important to check the browser storage to see if they have previously logged in. If their credentials are still valid, you can create a ClaimsPrincipal from the stored user data. If the user is not logged in, you can create an empty ClaimsPrincipal:

  1. In your user service, create a method to retrieve the credentials from browser storage:
public class BlazorSchoolUserService
{
    ...
    public async Task<User?> FetchUserFromBrowserAsync()
    {
        try // When Blazor Server is rendering at server side, there is no local storage. Therefore, put an empty try catch to avoid error
        {
            var fetchedUserResult = await _protectedLocalStorage.GetAsync<string>(_blazorSchoolStorageKey);

            if (fetchedUserResult.Success && !string.IsNullOrEmpty(fetchedUserResult.Value))
            {
                var user = JsonConvert.DeserializeObject<User>(fetchedUserResult.Value);

                return user;
            }
        }
        catch
        {
        }

        return null;
    }
}
Please note that in this code sample, we are storing the entire User object as the user's credentials. While this may be suitable for small projects or proof of concept applications, it's not recommended for production applications. In a real project, you should consider using a more secure approach, such as storing a token that can be used to authenticate the user's requests to the server.
  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 = await _blazorSchoolUserService.FetchUserFromBrowserAsync();

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

            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 async Task ClearBrowserUserDataAsync() => await _protectedLocalStorage.DeleteAsync(_blazorSchoolStorageKey);
}
  1. In your custom AuthenticationStateProvider, add a logout method:
public class BlazorSchoolAuthenticationStateProvider : AuthenticationStateProvider
{
    ...
    public async Task LogoutAsync()
    {
        await _blazorSchoolUserService.ClearBrowserUserDataAsync();
        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: Using HttpContextAccessor to get user information

While MVC and Web API commonly use HttpContextAccessor to get the user information, it's not recommended to do the same in Blazor. Blazor is a single-page application (SPA) framework, and since there is only one request, the request cannot contain the user information. To access the user information in Blazor, please follow the steps outlined in the Accessing User Information section of this tutorial.

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 requests to be sent to the database, which can negatively impact performance and result in unnecessary traffic.

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 🗙