🟥 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 theauthenticationTypeparameter. 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 theauthenticationTypeparameter, you ensure that theClaimsIdentityis 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 newClaimsIdentityobject.
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 authenticationTypeWhen 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.