- Introduction
CustomAuthStateProvider is a custom authentication state provider in Blazor WebAssembly. It checks whether a user is logged in by reading the JWT access token stored in browser local storage.
It is responsible for:
- Checking whether an access token exists.
- Creating an authenticated or unauthenticated user.
- Reading claims from a JWT token.
- Notifying Blazor when the authentication state changes.
2. Required Namespaces
using Blazored.LocalStorage; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; Explanation:
-
Blazored.LocalStorage: Provides methods to read and write data in browser local storage. -
Microsoft.AspNetCore.Components.Authorization: ProvidesAuthenticationStateProviderandAuthenticationState. -
Microsoft.IdentityModel.Tokens: Provides token-related security functionality. It is not directly used in the current code. -
System.IdentityModel.Tokens.Jwt: ProvidesJwtSecurityTokenHandlerfor reading JWT tokens. -
System.Security.Claims: ProvidesClaimsPrincipal,ClaimsIdentity, andClaimfor representing user identity and claims.
3. Namespace Declaration
namespace Frontend.Services { Explanation:
The namespace organizes the class inside the Frontend.Services section of the application. It helps keep authentication-related services separate from pages and components.
4. Class Declaration and Constructor
public class CustomAuthStateProvider(ILocalStorageService localStorage) : AuthenticationStateProvider { private readonly ILocalStorageService _localStorage = localStorage; Explanation:
-
CustomAuthStateProvider: The name of the custom class. -
AuthenticationStateProvider: The built-in Blazor class that this custom class extends. -
ILocalStorageService localStorage: Receives the local storage service through constructor injection. -
_localStorage: Stores the injected service so that other methods in the class can access browser local storage.
Why is it used?
Blazor needs to know whether a user is authenticated. By extending AuthenticationStateProvider, we can define our own logic for checking the user's authentication state.
5. GetAuthenticationStateAsync() Method
public override async Task<AuthenticationState> GetAuthenticationStateAsync() { var token = await _localStorage.GetItemAsync<string>("accessToken"); if (string.IsNullOrWhiteSpace(token)) { return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())); } return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity( ParseClaimFromJwt(token),"jwt","unique_name","role"))); } Explanation:
Step 1: Read the access token
var token = await _localStorage.GetItemAsync<string>("accessToken"); The method retrieves the value stored under the key accessToken in browser local storage.
The await keyword waits for the asynchronous operation to complete.
Step 2: Check whether the token exists
if (string.IsNullOrWhiteSpace(token)) { return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())); } If the token is null, empty, or contains only whitespace, the method returns an unauthenticated user.
-
ClaimsIdentityrepresents the user's identity. - An empty
ClaimsIdentityhas no authentication type and is unauthenticated. -
ClaimsPrincipalrepresents the current user. -
AuthenticationStatewraps that user for Blazor.
Step 3: Create an authenticated user
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity( ParseClaimFromJwt(token),"jwt","unique_name","role"))); If a token exists, the method extracts its claims and uses them to create a ClaimsIdentity.
-
ParseClaimFromJwt(token): Extracts claims from the token. -
"jwt": Sets the authentication type. -
"unique_name": Specifies the claim type Blazor should use as the user's name. -
"role": Specifies the claim type used to identify the user's roles.
The resulting identity is wrapped in a ClaimsPrincipal and then an AuthenticationState.
Important: This code checks whether a token exists; it does not verify its signature, expiration, or validity. Those checks must be handled appropriately by the authentication system and backend API.
6. ChangeState() Method
public async Task ChangeState(string? token ) { ClaimsPrincipal user; if (string.IsNullOrWhiteSpace(token)) { user = new ClaimsPrincipal(new ClaimsIdentity()); } else { user = new ClaimsPrincipal(new ClaimsIdentity(ParseClaimFromJwt(token), "jwt", "unique_name", "role")); } NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(user))); } Explanation:
This method updates Blazor's current authentication state when the user logs in or logs out.
Step 1: Declare the user
ClaimsPrincipal user; Creates a variable to hold the user identity.
Step 2: Handle logout or a missing token
if (string.IsNullOrWhiteSpace(token)) { user = new ClaimsPrincipal(new ClaimsIdentity()); } When the token is empty or null, an unauthenticated user is created. This is useful during logout.
Step 3: Handle login or a supplied token
else { user = new ClaimsPrincipal(new ClaimsIdentity(ParseClaimFromJwt(token), "jwt", "unique_name", "role")); } When a token is supplied, its claims are extracted and used to create the authenticated user identity.
Step 4: Notify Blazor
NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(user))); This is the most important line in the method.
It informs Blazor that the authentication state has changed. Components that depend on authentication state can then update their UI.
For example, an AuthorizeView can switch between its Authorized and NotAuthorized content when the state changes.
Note: The method is marked async, but it currently contains no await. It could be changed to return Task without the async modifier if its implementation remains the same.
7. ParseClaimFromJwt() Method
private IEnumerable<Claim> ParseClaimFromJwt(string jwt) { var handler = new JwtSecurityTokenHandler(); var token = handler.ReadJwtToken(jwt); foreach (var item in token.Claims) { Console.WriteLine(item.Value); } return token.Claims; } Explanation:
This private method reads the JWT token and extracts its claims.
Step 1: Create a JWT handler
var handler = new JwtSecurityTokenHandler(); Creates an object that can read and process JWT-formatted tokens.
Step 2: Read the JWT
var token = handler.ReadJwtToken(jwt); Reads the supplied JWT string and converts it into a JwtSecurityToken object.
This allows the application to access the claims contained in the token.
Step 3: Iterate through the claims
foreach (var item in token.Claims) { Console.WriteLine(item.Value); } Loops through all claims and prints each claim's value to the console.
A JWT might contain claims such as:
- User ID
- Name
- Role
The actual claims depend on what the backend puts into the token.
Step 4: Return the claims
return token.Claims; Returns the collection of claims so that GetAuthenticationStateAsync() and ChangeState() can use them to create a ClaimsIdentity.
8. Complete Execution Flow
The overall authentication flow is as follows:
- The user opens the Blazor application.
- Blazor calls
GetAuthenticationStateAsync(). - The provider reads
accessTokenfrom local storage. - If no token exists, Blazor receives an unauthenticated user.
- If a token exists, the provider extracts its claims and creates a user identity.
- When login or logout occurs, the application calls
ChangeState(). -
NotifyAuthenticationStateChanged()informs Blazor about the new authentication state. - Authentication-dependent components can update their UI.
9. Conclusion
CustomAuthStateProvider connects the JWT token stored in browser local storage with Blazor's authentication system.
GetAuthenticationStateAsync() retrieves the current authentication state, ChangeState() notifies Blazor of changes, and ParseClaimFromJwt() extracts claims from the JWT.
Remember that decoding a JWT is not the same as validating it. The backend API must independently validate access tokens and enforce authorization rules.
JwtAuthorizationHandler in Blazor WebAssembly
- Introduction
JwtAuthorizationHandler is a custom HTTP message handler used to manage JWT access tokens when the Blazor WebAssembly frontend communicates with the ASP.NET Core backend API.
Its main responsibilities are:
- Automatically attaching the access token to outgoing HTTP requests.
- Detecting when the backend returns
401 Unauthorized. - Attempting to obtain new tokens using the refresh token.
- Retrying the original API request after refreshing the token.
- Redirecting the user to the login page when refreshing the token fails.
This handler helps avoid manually adding the JWT access token to every API request.
2. Required Namespaces
using Blazored.LocalStorage; using System.Net.Http.Headers; using System.Net; using System.Net.Http.Json; using Frontend.Common; using System.Formats.Asn1; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components; Explanation:
-
Blazored.LocalStorage: Reads and writes access and refresh tokens in browser local storage. -
System.Net.Http.Headers: ProvidesAuthenticationHeaderValue, which is used to set the HTTP Authorization header. -
System.Net: ProvidesHttpStatusCode, includingUnauthorized. -
System.Net.Http.Json: Provides methods such asPostAsJsonAsync()andReadFromJsonAsync(). -
Frontend.Common: Contains theTokenResponsetype used to receive the refreshed tokens. -
System.Formats.Asn1: Not directly used in the current code. -
Microsoft.AspNetCore.Components.Authorization: ProvidesAuthenticationStateProvider. -
Microsoft.AspNetCore.Components: ProvidesNavigationManagerfor navigating to another page.
3. Namespace Declaration
namespace Frontend.Services; Explanation:
This declares that the class belongs to the Frontend.Services namespace. It keeps the HTTP authentication handler organized with the application's other services.
4. Class Declaration and Constructor
public class JwtAuthorizationHandler(ILocalStorageService localStorage,AuthenticationStateProvider stateProvider,NavigationManager navigation):DelegatingHandler { private readonly ILocalStorageService _localStorage = localStorage; private readonly AuthenticationStateProvider _stateProvider = stateProvider; private readonly NavigationManager _navigation = navigation; Explanation:
Class declaration
JwtAuthorizationHandler inherits from DelegatingHandler.
A DelegatingHandler allows us to intercept outgoing HTTP requests and incoming HTTP responses before passing them along the HTTP pipeline.
Constructor parameters
-
ILocalStorageService localStorage: Reads and writes tokens stored in the browser. -
AuthenticationStateProvider stateProvider: Updates Blazor's authentication state after the access token changes. -
NavigationManager navigation: Navigates the user to the login page if authentication cannot be restored.
Private fields
The three private readonly fields store the injected services so that the methods inside the class can use them.
Why is it used?
Without a custom handler, you would need to manually attach the access token, check unauthorized responses, refresh tokens, and retry requests in multiple places. This handler centralizes that behavior.
5. SendAsync() Method
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { await AddAccessToken(request); var response = await base.SendAsync(request, cancellationToken); if (response.StatusCode != HttpStatusCode.Unauthorized) { return response; } bool refreshSuccess = await RefreshToken(); if (!refreshSuccess) { _navigation.NavigateTo("login"); return response; } await AddAccessToken(request); response.Dispose(); return await base.SendAsync(request, cancellationToken); } Explanation:
This is the main method of the handler. It controls the complete HTTP request flow.
Step 1: Add the access token
await AddAccessToken(request); Calls the AddAccessToken() method to read the access token from local storage and attach it to the outgoing HTTP request.
Step 2: Send the HTTP request
var response = await base.SendAsync(request, cancellationToken); Sends the request to the next handler in the HTTP pipeline, which eventually sends it to the backend API.
The response contains the result returned by the server.
Step 3: Check the response status
if (response.StatusCode != HttpStatusCode.Unauthorized) { return response; } If the response is not 401 Unauthorized, the handler returns the response immediately.
For example:
-
200 OK: Request succeeded. -
400 Bad Request: Request contains invalid data. -
403 Forbidden: The server refuses access. -
500 Internal Server Error: The server encountered an error.
These responses are returned without attempting token refresh.
Step 4: Refresh the token
bool refreshSuccess = await RefreshToken(); If the server returns 401 Unauthorized, the handler calls RefreshToken() to attempt to obtain a new access token.
The method returns a Boolean value:
-
true: Token refresh succeeded. -
false: Token refresh failed.
Step 5: Handle refresh failure
if (!refreshSuccess) { _navigation.NavigateTo("login"); return response; } If refreshing the token fails, the user is redirected to the login page and the original unauthorized response is returned.
The NavigateTo() call in this branch is redundant because RefreshToken() already navigates on its failure paths, but it does not change the intended behavior.
Step 6: Attach the refreshed access token
await AddAccessToken(request); Reads the updated access token from local storage and attaches it to the original request.
Step 7: Dispose of the previous response
response.Dispose(); Releases resources held by the first HTTP response, which was unauthorized.
Step 8: Retry the request
return await base.SendAsync(request, cancellationToken); Sends the request again, this time with the refreshed access token.
Important: This implementation retries the original request. However, sending the same HttpRequestMessage twice can cause an InvalidOperationException in .NET. A production implementation should clone or recreate the request before retrying, especially for requests with content.
6. AddAccessToken() Method
private async Task AddAccessToken(HttpRequestMessage request) { var accessToken = await _localStorage.GetItemAsync<string>("accessToken"); if (!string.IsNullOrWhiteSpace(accessToken)) { request.Headers.Authorization = new AuthenticationHeaderValue("Bearer",accessToken); } } Explanation:
This method attaches the access token to an outgoing HTTP request.
Step 1: Read the access token
var accessToken = await _localStorage.GetItemAsync<string>("accessToken"); Retrieves the access token stored under the accessToken key in browser local storage.
Step 2: Check whether the token exists
if (!string.IsNullOrWhiteSpace(accessToken)) Checks whether the retrieved token is not null, empty, or whitespace.
Step 3: Add the Authorization header
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer",accessToken); Sets the HTTP Authorization header using the Bearer authentication scheme.
The resulting HTTP header looks like this:
Authorization: Bearer your_access_token The backend JWT authentication middleware can read this header and validate the token.
Why is it used?
The backend needs the access token to identify the caller and enforce authorization rules on protected endpoints.
7. RefreshToken() Method
private async Task<bool> RefreshToken() { var accessToken = await _localStorage.GetItemAsync<string>("accessToken"); var refreshToken = await _localStorage.GetItemAsync<string>("refreshToken"); try { using var client = new HttpClient(); var response = await client.PostAsJsonAsync("api/Auth/refreshToken", new { accessToken, refreshToken }); if (!response.IsSuccessStatusCode) { _navigation.NavigateTo("login"); return false; } var token = await response.Content.ReadFromJsonAsync<TokenResponse>(); await _localStorage.SetItemAsync("accessToken", token.AccessToken); await _localStorage.SetItemAsync("refreshToken", token.RefreshToken); var auth = (CustomAuthStateProvider)_stateProvider; await auth.ChangeState(token.AccessToken); return true; } catch (Exception ex) { await _localStorage.SetItemAsync("accessToken",""); await _localStorage.SetItemAsync("refreshToken",""); var auth = (CustomAuthStateProvider)_stateProvider; await auth.ChangeState(""); _navigation.NavigateTo("login"); return false; } } Explanation:
This method attempts to refresh the access token by sending the existing access token and refresh token to the backend API.
Step 1: Retrieve both tokens
var accessToken = await _localStorage.GetItemAsync<string>("accessToken"); var refreshToken = await _localStorage.GetItemAsync<string>("refreshToken"); Reads the two tokens from browser local storage.
- Access token: Used to authenticate API requests.
- Refresh token: Used to request a replacement access token when the current access token can no longer be used.
Step 2: Create an HTTP client
using var client = new HttpClient(); Creates a new HttpClient for sending the refresh request.
The using declaration ensures that the client is disposed when the method exits.
Important: In this code, the new client does not inherit the base address configured on your application's registered HttpClient. Therefore, the relative URL below may fail because it has no base address. A robust implementation should use a configured backend base address or an appropriately registered refresh client that does not invoke this same handler recursively.
Step 3: Send the refresh request
var response = await client.PostAsJsonAsync("api/Auth/refreshToken", new { accessToken, refreshToken }); Sends a POST request to the backend refresh endpoint with both tokens in the request body.
The anonymous object is serialized into JSON similar to:
{ "accessToken": "current_access_token", "refreshToken": "current_refresh_token" } The actual property names and endpoint must match the backend API contract.
Step 4: Check whether refreshing succeeded
if (!response.IsSuccessStatusCode) { _navigation.NavigateTo("login"); return false; } If the server returns an unsuccessful HTTP status code, the method redirects to the login page and returns false.
For example, the refresh token may be expired, revoked, invalid, or rejected by the server.
Step 5: Read the returned tokens
var token = await response.Content.ReadFromJsonAsync<TokenResponse>(); Deserializes the response JSON into a TokenResponse object.
The type is imported from Frontend.Common and should contain properties such as AccessToken and RefreshToken.
Step 6: Store the new access token
await _localStorage.SetItemAsync("accessToken", token.AccessToken); Saves the newly issued access token in local storage, replacing the old token.
Step 7: Store the new refresh token
await _localStorage.SetItemAsync("refreshToken", token.RefreshToken); Saves the newly issued refresh token.
This is particularly important when the backend uses refresh-token rotation, where each successful refresh invalidates the previous refresh token.
Step 8: Get the custom authentication provider
var auth = (CustomAuthStateProvider)_stateProvider; Casts the injected AuthenticationStateProvider to CustomAuthStateProvider so that its custom ChangeState() method can be called.
This cast requires the registered provider to actually be CustomAuthStateProvider.
Step 9: Update Blazor's authentication state
await auth.ChangeState(token.AccessToken); Passes the new access token to the custom ...
