This tutorial will walk you through how to implement authentication from scratch and display the user's information once authenticated. In this tutorial:
You can download the example code used in this topic on GitHub.
Before you begin, you need to choose a library to convert a C# object to JSON. We recommend Newtonsoft.Json
.
From the previous tutorial, you have learned to implement authentication, you need:
AuthenticationStateProvider
.CascadingAuthenticationState
.When you implement authentication from scratch, you are going to need some extra building blocks, they are:
The user class represents a user, this class contains information to identify the user (For example, the username and password) and other information to do the authorization (For example, the user age, payment status). The user class is responsible for converting itself into a ClaimsPrincipal
and vice versa.
The user service interacts with the data source (commonly a database), verify if the user provided credentials (commonly username and password) valid, and manage browser data storage.
You also need to have a custom AuthenticationStateProvider
, your AuthenticationStateProvider
will handle login, logout and revisit flow, it interacts with the user service and notify the user authentication state to other components to render the UI respectively.
Process steps of login flow:
ClaimsPrincipal
object.ClaimsPrincipal
to call NotifyAuthenticationStateChanged
.Process steps of logout flow:
NotifyAuthenticationStateChanged
with an empty ClaimsPrincipal
.Process steps of revisit flow:
ClaimsPrincipal
and then return the AuthenticationState
with the created ClaimsPrincipal
.AuthenticationState
with the empty ClaimsPrincipal
.This section will help you implement the user class, the user service, and a custom AuthenticationStateProvider
.
public class User { public string Username { get; set; } = ""; public string Password { get; set; } = ""; }
User
object to a ClaimsPrincipal
. For example:public class User { .... public ClaimsPrincipal ToClaimsPrincipal() => new(new ClaimsIdentity(new Claim[] { new (ClaimTypes.Name, Username), new (ClaimTypes.Hash, Password), }, "BlazorSchool")); }
TheauthenticationType
is an important parameter and you must specify it. In the example, we specified theauthenticationType
asBlazorSchool
.
ClaimsPrincipal
to an User
object. For example:public class User { .... public static User FromClaimsPrincipal(ClaimsPrincipal principal) => new() { Username = principal.FindFirstValue(ClaimTypes.Name), Password = principal.FindFirstValue(ClaimTypes.Hash) }; }
public class BlazorSchoolUserService { private readonly ProtectedLocalStorage _protectedLocalStorage; public BlazorSchoolUserService(ProtectedLocalStorage protectedLocalStorage) { _protectedLocalStorage = protectedLocalStorage; } }
public class BlazorSchoolUserService { ... public User? LookupUserInDatabase(string username, string password) { var usersFromDatabase = new List<User>() { new() { Username = "blazorschool", Password = "blazorschool" } }; var foundUser = usersFromDatabase.SingleOrDefault(u => u.Username == username && u.Password == password); return foundUser; } }
public class BlazorSchoolUserService { ... private readonly string _blazorSchoolStorageKey = "blazorSchoolIdentity"; public async Task PersistUserToBrowserAsync(User user) { string userJson = JsonConvert.SerializeObject(user); await _protectedLocalStorage.SetAsync(_blazorSchoolStorageKey, userJson); } }
public class BlazorSchoolUserService { ... public async Task<User?> FetchUserFromBrowserAsync() { try { var storedUserResult = await _protectedLocalStorage.GetAsync<string>(_blazorSchoolStorageKey); if (storedUserResult.Success && !string.IsNullOrEmpty(storedUserResult.Value)) { var user = JsonConvert.DeserializeObject<User>(storedUserResult.Value); return user; } } catch (InvalidOperationException) { } return null; } }
public class BlazorSchoolUserService { ... public async Task ClearBrowserUserDataAsync() => await _protectedLocalStorage.DeleteAsync(_blazorSchoolStorageKey); }
AuthenticationStateProvider
AuthenticationStateProvider
and inject the user service. For example:public class BlazorSchoolAuthenticationStateProvider : AuthenticationStateProvider { private readonly BlazorSchoolUserService _blazorSchoolUserService; public BlazorSchoolAuthenticationStateProvider(BlazorSchoolUserService blazorSchoolUserService) { _blazorSchoolUserService = blazorSchoolUserService; } }
public class BlazorSchoolAuthenticationStateProvider : AuthenticationStateProvider { .... public async Task LoginAsync(string username, string password) { var principal = new ClaimsPrincipal(); var user = _blazorSchoolUserService.LookupUserInDatabase(username, password); if (user is not null) { await _blazorSchoolUserService.PersistUserToBrowserAsync(user); principal = user.ToClaimsPrincipal(); } NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(principal))); } }
public class BlazorSchoolAuthenticationStateProvider : AuthenticationStateProvider { .... public async Task LogoutAsync() { await _blazorSchoolUserService.ClearBrowserUserDataAsync(); NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(new()))); } }
GetAuthenticationStateAsync
to handle revisit flow. For example: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 userInDatabase = _blazorSchoolUserService.LookupUserInDatabase(user.Username, user.Password); if (userInDatabase is not null) { principal = userInDatabase.ToClaimsPrincipal(); CurrentUser = userInDatabase; } } return new(principal); } }
CascadingAuthenticationState
in the App.razor
, the CascadingAuthenticationState
needs to be the root component.<CascadingAuthenticationState> <Router AppAssembly="typeof(App).Assembly"> ... </Router> </CascadingAuthenticationState>
Program.cs
. If you use ProtectedLocalStorage
then you don't need to do this.Program.cs
.builder.Services.AddScoped<BlazorSchoolUserService>();
AuthenticationStateProvider
in Program.cs
.builder.Services.AddScoped<BlazorSchoolAuthenticationStateProvider>(); builder.Services.AddScoped<AuthenticationStateProvider>(sp => sp.GetRequiredService<BlazorSchoolAuthenticationStateProvider>());
After you have done those steps, you can inject the custom AuthenticationStateProvider
to perform the authentication. This is an example login page:
@inject BlazorSchoolAuthenticationStateProvider BlazorSchoolAuthenticationStateProvider <h3>Login</h3> <button type="button" class="btn btn-success" @onclick="LoginAsync">Login</button> @code { public async Task LoginAsync() { await BlazorSchoolAuthenticationStateProvider.LoginAsync("blazorschool", "blazorschool"); } }
When a user register to your website, you might ask them to provide some personal information like age, email, then later on, you want to display their information once they logged in. To access the user information:
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)) }; }
AuthenticationStateProvider
. For example:public class BlazorSchoolAuthenticationStateProvider : AuthenticationStateProvider { ... public User CurrentUser { get; private set; } = new(); }
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); } } }
IDiposable
interface, subscribe and unsubscribe the AuthenticationStateChanged
event. For example:public class BlazorSchoolAuthenticationStateProvider : AuthenticationStateProvider, IDisposable { ... public BlazorSchoolAuthenticationStateProvider(BlazorSchoolUserService blazorSchoolUserService) { _blazorSchoolUserService = blazorSchoolUserService; AuthenticationStateChanged += OnAuthenticationStateChangedAsync; } public void Dispose() => AuthenticationStateChanged -= OnAuthenticationStateChangedAsync; }
GetAuthenticationStateAsync
method. For example:public class BlazorSchoolAuthenticationStateProvider : AuthenticationStateProvider, IDisposable { ... public override async Task<AuthenticationState> GetAuthenticationStateAsync() { var principal = new ClaimsPrincipal(); var user = await _blazorSchoolUserService.FetchUserFromBrowserAsync(); if (user is not null) { var userInDatabase = _blazorSchoolUserService.LookupUserInDatabase(user.Username, user.Password); if (userInDatabase is not null) { principal = userInDatabase.ToClaimsPrincipal(); CurrentUser = userInDatabase; } } return new(principal); } }
Now you can access the user information via the custom AuthenticationStateProvider
class. For example:
@inject BlazorSchoolAuthenticationStateProvider BlazorSchoolAuthenticationStateProvider <div>Full name: @BlazorSchoolAuthenticationStateProvider.CurrentUser.FullName</div>
These are some common mistakes that we have collected from the Blazor School Discord Community.
ClaimsIdentity
without the authenticationType
In the User
class, when converting the user object to a ClaimsPrincipal
, but before you can convert to a ClaimsPrincipal
, you need a ClaimsIdentity
first. However, most people created a ClaimsIdentity
without 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), }));
authenticationType
?The ClaimsIdentity
will mark the user as unauthenticated, the property ClaimsIdentity.IsAuthenticated
will be false
.
GetAuthenticationStateAsync
from AuthenticationStateProvider
to get the user informationThe GetAuthenticationStateAsync
is not intended to be called in the component by AuthenticationStateProvider
. For example, the following code is bad:
@inject BlazorSchoolAuthenticationStateProvider BlazorSchoolAuthenticationStateProvider <h3>BadImplementation</h3> <div>Username: @Username</div> @code { public string Username { get; set; } = ""; protected override async Task OnInitializedAsync() { var authState = await BlazorSchoolAuthenticationStateProvider.GetAuthenticationStateAsync(); if(authState is not null) { Username = authState.User.FindFirstValue(ClaimTypes.Name); } } }
GetAuthenticationStateAsync
from AuthenticationStateProvider
to get the user's information?The GetAuthenticationStateAsync
will be executed whenever a component is rendered. The following video demonstrates this mistake:
HttpContextAccessor
to get the user informationThe MVC and the Web API use HttpContextAccessor
to get the user information. However, Blazor is an SPA framework, there will be only 1 request and that request cannot contain the user information. To get the information, follow the Access the user information in this tutorial.