Both authentication and authorization play a crucial part in every Blazor Server website. Authentication is the process of acquiring user information. Authorization is the process of using acquired information to check if the user has the right to access certain resources or not. This guide will walk you through on how to do authentication and authorization in Blazor Server.
You can download the example code used in this topic on GitHub.
To prepare your website for authentication and authorization, you should have a basic understanding of the following:
Authentication is the process of acquiring user information. Therefore, authentication must happen before you can authorize users. Once the user is successfully authenticated, you need to store the authenticated results into local storage and later on, retrieve the results and verify that.
AuthenticationStateProvider
Blazor Server uses AuthenticationStateProvider
to authenticating users. You need to replace the built-in AuthenticationStateProvider
to do the authentication yourself.
AuthenticationStateProvider
. We call it WebsiteAuthenticator
in our example.public class WebsiteAuthenticator : AuthenticationStateProvider { private readonly ProtectedLocalStorage _protectedLocalStorage; private readonly SimulatedDataProviderService _dataProviderService; public WebsiteAuthenticator(ProtectedLocalStorage protectedLocalStorage, SimulatedDataProviderService dataProviderService) { _protectedLocalStorage = protectedLocalStorage; _dataProviderService = dataProviderService; } public override async Task<AuthenticationState> GetAuthenticationStateAsync() { var principal = new ClaimsPrincipal(); try { var storedPrincipal = await _protectedLocalStorage.GetAsync<string>("identity"); if (storedPrincipal.Success) { var user = JsonConvert.DeserializeObject<User>(storedPrincipal.Value); var (_, isLookUpSuccess) = LookUpUser(user.Username, user.Password); if (isLookUpSuccess) { var identity = CreateIdentityFromUser(user); principal = new(identity); } } } catch { } return new AuthenticationState(principal); } public async Task LoginAsync(LoginFormModel loginFormModel) { var (userInDatabase, isSuccess) = LookUpUser(loginFormModel.Username, loginFormModel.Password); var principal = new ClaimsPrincipal(); if (isSuccess) { var identity = CreateIdentityFromUser(userInDatabase); principal = new ClaimsPrincipal(identity); await _protectedLocalStorage.SetAsync("identity", JsonConvert.SerializeObject(userInDatabase)); } NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(principal))); } public async Task LogoutAsync() { await _protectedLocalStorage.DeleteAsync("identity"); var principal = new ClaimsPrincipal(); NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(principal))); } private static ClaimsIdentity CreateIdentityFromUser(User user) { return new ClaimsIdentity(new Claim[] { new (ClaimTypes.Name, user.Username), new (ClaimTypes.Hash, user.Password), new("age", user.Age.ToString()) }, "BlazorSchool"); } private (User, bool) LookUpUser(string username, string password) { var result = _dataProviderService.Users.FirstOrDefault(u => username == u.Username && password == u.Password); return (result, result is not null); } }
You have to implement SimulatedDataProviderService
to store and retrieve the data from the database yourself.
You need an empty try-catch
block when retrieving local storage because local storage is only available in the client. If the Blazor Server renders any component that requires authenticated users, the method GetAuthenticationStateAsync
will be called and local storage will not be available there. The method GetAuthenticationStateAsync
will be called again later by the client. This also helps make sensitive data secure to any search engine.
WebsiteAuthenticator
in Startup.cs
.public void ConfigureServices(IServiceCollection services) { ... services.AddScoped<WebsiteAuthenticator>(); services.AddScoped<AuthenticationStateProvider>(sp => sp.GetRequiredService<WebsiteAuthenticator>()); }
The first line initilizes your class as a scoped service. The second line is to replace your scoped service into built-in AuthenticationStateProvider
.
CascadingAuthenticationState
around App.razor
.<CascadingAuthenticationState> <Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true"> ... </Router> </CascadingAuthenticationState>
The login page allows users to input their information for you to verify. It can be an independent page or you can also use the Content Projection technique to reduce the amount of work when you want to display a login form if the user is not authenticated.
AuthenticationStateProvider
.@inject WebsiteAuthenticator WebsiteAuthenticator
<EditForm Model="LoginFormModel" Context="Login"> <div> <label> Username: <InputText @bind-Value="LoginFormModel.Username"></InputText> </label> </div> <div> <label> Password: <InputText type="password" @bind-Value="LoginFormModel.Password"></InputText> </label> </div> <div> <button @onclick="TryLogin">Submit</button> </div> </EditForm> @code { private LoginFormModel LoginFormModel { get; set; } = new(); private async Task TryLogin() { await WebsiteAuthenticator.LoginAsync(LoginFormModel); } }
AuthorizeView
along with its render fragments: NotAuthorized
, Authorizing
, Authorized
to do that. AuthorizeView
is a pre-built component using Content Projection technique to determine what to display to the users according to their authenticated status.AuthorizeView
component and also, inside the NotAuthorized
render fragment. By doing this, you will hide the login form when a user is authenticated.<AuthorizeView Context="Account"> <NotAuthorized> <EditForm Model="LoginFormModel" Context="Login"> ... </EditForm> </NotAuthorized> </AuthorizeView>
Authorized
render fragment to notify them.<AuthorizeView Context="Account"> <NotAuthorized> ... </NotAuthorized> <Authorized> <button type="button" class="btn btn-primary" @onclick="TryLogout">Logout</button> </Authorized> </AuthorizeView> @code { ... private async Task TryLogout() { await WebsiteAuthenticator.LogoutAsync(); } }
Authorizing
render fragment to show the process of authenticating the user. The time taken to authenticate a user depends on the GetAuthenticationStateAsync
method of your class.<AuthorizeView Context="Account"> <NotAuthorized> ... </NotAuthorized> <Authorized> ... </Authorized> <Authorizing> Authorizing in process... </Authorizing> </AuthorizeView>