🟥 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.
You can download the example code used in this topic on GitHub.
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.
@using Microsoft.AspNetCore.Components.Authorization
to enable the use of the Microsoft.AspNetCore.Components.Authorization
namespace.CascadingAuthenticationState
as the root component in the App.razor file by adding the following code:<CascadingAuthenticationState> <Router AppAssembly="typeof(App).Assembly"> ... </Router> </CascadingAuthenticationState>
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.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 newClaimsIdentity
, it's important to always pass theauthenticationType
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 theauthenticationType
parameter, you ensure that theClaimsIdentity
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 newClaimsIdentity
object.
public class BlazorSchoolUserService { private readonly ProtectedLocalStorage _protectedLocalStorage; private readonly string _blazorSchoolStorageKey = "blazorSchoolIdentity"; public BlazorSchoolUserService(ProtectedLocalStorage protectedLocalStorage) { _protectedLocalStorage = protectedLocalStorage; } }
AuthenticationStateProvider
. For instance:public class BlazorSchoolAuthenticationStateProvider : AuthenticationStateProvider { private readonly BlazorSchoolUserService _blazorSchoolUserService; public BlazorSchoolAuthenticationStateProvider(BlazorSchoolUserService blazorSchoolUserService) { _blazorSchoolUserService = blazorSchoolUserService; } }
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();
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:
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.
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))); } }
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
:
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.
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); } }
When a user logs out, you must clear the browser storage and reset their authentication state.
public class BlazorSchoolUserService { ... public async Task ClearBrowserUserDataAsync() => await _protectedLocalStorage.DeleteAsync(_blazorSchoolStorageKey); }
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
.
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:
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)) }; }
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); } } }
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; }
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>
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:
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")); }
HttpContextAccessor
to get user informationWhile 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.
GetAuthenticationStateAsync
from AuthenticationStateProvider
to retrieve user informationGetAuthenticationStateAsync
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.