🟥 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.
You can download the example code used in this topic on GitHub.
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
.
Microsoft.AspNetCore.Components.Authorization
and System.IdentityModel.Tokens.Jwt
from the NuGet package manager.@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>
AuthenticationDataMemoryStorage
with a Token
property that is initialized to an empty string
.public class AuthenticationDataMemoryStorage { public string Token { get; set; } = ""; }
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 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 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.
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<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.
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:
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; }
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))); } }
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
.
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.
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); } }
When a user logs out, you must clear the browser storage and reset their authentication state.
public class BlazorSchoolUserService { public void ClearBrowserUserData() => _authenticationDataMemoryStorage.Token = ""; }
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
.
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")); }
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.
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 authentication requests to be sent to the API, which can negatively impact performance and result in unnecessary traffic.
HttpContextAccessor
to retrieve user informationUsing 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.