After taking a look at the Identity Individual Users template for Blazor Web App 9 (with Server/Global render mode) I've managed to make JWT Auth Work!
Step 1 - Program.cs
All the code required for authentication / Authorization is as follows:
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = "your-issuer",
ValidAudience = "your-audience",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("your-secr479458959et-ke41882928418191y1"))
};
options.Events = new JwtBearerEvents
{
OnChallenge = context =>
{
context.HandleResponse();
context.Response.Redirect("/login");
return Task.CompletedTask;
}
};
});
builder.Services.AddAuthorizationCore();
Also add:
app.UseAuthentication();
app.UseAuthorization();
Step 2 - CustomAuthenticationStateProvider
I've used BlazoredLocalStorage to store my Token, but anything else can be used
public class CustomAuthenticationStateProvider(ILocalStorageService localStorageService) : AuthenticationStateProvider
{
private readonly ILocalStorageService _localStorage = localStorageService;
private const string TokenKey = "authToken";
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
try
{
var token = await _localStorage.GetItemAsync<string>(TokenKey);
if (string.IsNullOrWhiteSpace(token))
{
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
}
var claims = ParseClaimsFromJwt(token);
var identity = new ClaimsIdentity(claims, "jwt");
var user = new ClaimsPrincipal(identity);
return new AuthenticationState(user);
}
catch (JSException ex)
{
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
}
catch (Exception)
{
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
}
}
public void NotifyUserAuthentication(string token)
{
var claims = ParseClaimsFromJwt(token);
var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(claims, "jwt"));
var authState = Task.FromResult(new AuthenticationState(authenticatedUser));
NotifyAuthenticationStateChanged(authState);
}
public void NotifyUserLogout()
{
var anonymousUser = new ClaimsPrincipal(new ClaimsIdentity());
var authState = Task.FromResult(new AuthenticationState(anonymousUser));
NotifyAuthenticationStateChanged(authState);
}
private static IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
{
var handler = new JwtSecurityTokenHandler();
var token = handler.ReadJwtToken(jwt);
return token.Claims;
}
}
Step 3 - Modify the App.razor file
No clue what this actually does but i guess it is required
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="@Assets["lib/bootstrap/dist/css/bootstrap.min.css"]" />
<link rel="stylesheet" href="@Assets["app.css"]" />
<link rel="stylesheet" href="@Assets["BlazorApp1-Jwt.styles.css"]" />
<ImportMap />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet @rendermode="PageRenderMode" />
</head>
<body>
<Routes @rendermode="PageRenderMode" />
<script src="_framework/blazor.web.js"></script>
</body>
</html>
@code {
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
private IComponentRenderMode? PageRenderMode =>
HttpContext.AcceptsInteractiveRouting() ? InteractiveServer : null;
}
Step 4 - Configure the Routes.razor file
@using Microsoft.AspNetCore.Components.Authorization
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
<NotAuthorized>
<RedirectToLogin />
</NotAuthorized>
</AuthorizeRouteView>
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>
This code is missing the NotFound attribute becuase it is no longer being used by Blazor Web App 8 and 9
Step 5 - RedirectToLogin
create a component that redirects to the Login page when the user is trying to access a secured page
@inject NavigationManager NavigationManager
@code {
protected override void OnInitialized()
{
NavigationManager.NavigateTo("login", forceLoad: true);
}
}
Step 6 - Login the User
Aquire your Token, save it in LocalStorage (or handle it however you want) and Notify the AuthStateProvider
((CustomAuthenticationStateProvider)AuthenticationStateProvider).NotifyUserAuthentication(token);
Step 7 - Secure Pages
Use the Authorize attribute on any page you want secured
Note: I am no expert. This is how i got it to work.. Please write / redirect to a better solution if what i wrote here is terrible code