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"));
}
TheauthenticationTypeis an important parameter and you must specify it. In the example, we specified theauthenticationTypeasBlazorSchool.
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);
}
AuthenticationStateProviderAuthenticationStateProvider 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 authenticationTypeIn 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.