
Blazor WASM Authentication Tutorial: Secure C# Web Apps with Identity
Implement JWT, OAuth, and Role-Based Access in Blazor WebAssembly Using C# for Enterprise-Level Security
Updated 04 Oct 2025
Introduction
If you're building modern web applications with C#, Blazor WebAssembly (WASM) offers a powerful way to create interactive, client-side experiences without relying on JavaScript. But as your app grows, especially in enterprise settings, securing it becomes crucial. That's where Blazor WASM authentication comes in. In this tutorial, we'll walk through how to implement robust authentication using ASP.NET Core Identity, JWT tokens, OAuth flows, and role-based access control. By the end, you'll have a secure C# web app ready for production, complete with enterprise-level security features that protect user data and prevent unauthorized access.I've been working with Blazor for a few years now, and one of the biggest challenges developers face is getting authentication right in a WASM environment. Unlike server-side Blazor, WASM runs entirely in the browser, so you need to handle tokens and API calls carefully to avoid common pitfalls like token leakage or session hijacking. Let's break this down step by step, starting from the basics and moving into practical implementation.
Why Authentication Matters in Blazor WebAssembly Apps
Blazor WASM lets you write C# code that compiles to WebAssembly, giving you the full power of .NET on the client side. It's great for single-page applications (SPAs) that need to feel responsive and native. However, without proper authentication, your app is vulnerable. Users could access sensitive features, data could be exposed, and compliance with standards like GDPR or HIPAA becomes impossible.- Protecting API endpoints from unauthorized requests.
- Managing user sessions across browser tabs.
- Integrating with external identity providers for seamless logins.
That's why tools like ASP.NET Core Identity are essential. It provides a built-in framework for user management, while JWT (JSON Web Tokens) handles stateless authentication. For broader integration, OAuth 2.0 allows secure connections to services like Google or Azure AD. And role-based access ensures only the right users see or do certain things, which is a must for enterprise apps.
In this guide, we'll focus on securing C# web apps with these elements. No fluff, just actionable steps to get you up and running.
Setting Up Your Blazor WASM Project with Authentication Basics
First things first: create a new Blazor WASM project. If you're using Visual Studio, go to File > New > Project, search for "Blazor WebAssembly App," and check the box for "ASP.NET Core hosted" to include a backend API. This setup gives you a server project for handling authentication and a client project for the UI.Once your project is scaffolded, install the necessary NuGet packages. For the server side (your API project), add:
- Microsoft.AspNetCore.Authentication.JwtBearer
- Microsoft.AspNetCore.Identity.EntityFrameworkCore
- Microsoft.EntityFrameworkCore.SqlServer (or your preferred database)
For the client side, you'll need Microsoft.AspNetCore.Components.WebAssembly.Authentication.
- Microsoft.AspNetCore.Authentication.JwtBearer
- Microsoft.AspNetCore.Identity.EntityFrameworkCore
- Microsoft.EntityFrameworkCore.SqlServer (or your preferred database)
- Microsoft.AspNetCore.Components.WebAssembly.Authentication
Run `dotnet restore` and fire up the app with `dotnet run`. You should see a basic Blazor app with a weather forecast page. Now, let's add Identity for user registration and login.
Configuring ASP.NET Core Identity
ASP.NET Core Identity is the backbone of user management in .NET. It handles everything from password hashing to email confirmation. Start by updating your `Program.cs` in the server project.using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
public class ApplicationDbContext : IdentityDbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }
}builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddIdentity<IdentityUser, IdentityRole
>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
};
});{
"Jwt": {
"Key": "YourSuperSecretKeyHereAtLeast32CharsLong",
"Issuer": "YourAppIssuer",
"Audience": "YourAppAudience"
},
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\mssqllocaldb;Database=BlazorAuthDb;Trusted_Connection=true;"
}
}Don't forget to add your JWT settings in `appsettings.json`.
Run migrations with `dotnet ef migrations add InitialCreate` and `dotnet ef database update`. Now, scaffold Identity pages if you want UI for registration (use the Add > New Scaffolded Item wizard in Visual Studio).
This setup gives you a solid foundation for Blazor WASM authentication. Users can now register and log in, but we need to issue JWT tokens upon successful login.
Implementing JWT Authentication in Blazor WASM
JWT tokens are perfect for Blazor apps because they're compact, self-contained, and work well with HTTP-only cookies or local storage. The goal is to authenticate users on the server and send a token back to the client for subsequent API calls.[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly UserManager<IdentityUser> _userManager;
private readonly IConfiguration _configuration;
public AuthController(UserManager<IdentityUser> userManager, IConfiguration configuration)
{
_userManager = userManager;
_configuration = configuration;
}
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginModel model)
{
var user = await _userManager.FindByNameAsync(model.Username);
if (user != null && await _userManager.CheckPasswordAsync(user, model.Password))
{
var token = GenerateJwtToken(user);
return Ok(new { Token = token });
}
return Unauthorized();
}
private string GenerateJwtToken(IdentityUser user)
{
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(ClaimTypes.NameIdentifier, user.Id)
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _configuration["Jwt:Issuer"],
audience: _configuration["Jwt:Audience"],
claims: claims,
expires: DateTime.Now.AddMinutes(30),
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}public class LoginModel
{
public string Username { get; set; }
public string Password { get; set; }
}builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<CustomAuthStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider>(provider => provider.GetRequiredService<CustomAuthStateProvider>());public class CustomAuthStateProvider : AuthenticationStateProvider
{
private readonly HttpClient _http;
private readonly ILocalStorageService _localStorage;
public CustomAuthStateProvider(HttpClient http, ILocalStorageService localStorage)
{
_http = http;
_localStorage = localStorage;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var token = await _localStorage.GetItemAsync<string>("authToken");
if (string.IsNullOrEmpty(token))
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var claims = ParseClaimsFromJwt(token);
var identity = new ClaimsIdentity(claims, "jwt");
return new AuthenticationState(new ClaimsPrincipal(identity));
}
// Add methods for login/logout, like storing the token after a successful API call
}You'll need Blazored.LocalStorage for the `_localStorage` service. Install it via NuGet.
@page "/login"
@inject HttpClient Http
@inject CustomAuthStateProvider AuthStateProvider
@inject NavigationManager Navigation
<EditForm Model="loginModel" OnValidSubmit="HandleLogin">
<!-- Input fields for username and password -->
<button type="submit">Login</button>
</EditForm>@code {
private LoginModel loginModel = new();
private async Task HandleLogin()
{
var response = await Http.PostAsJsonAsync("api/auth/login", loginModel);
if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadFromJsonAsync<LoginResponse>();
await LocalStorage.SetItemAsync("authToken", result.Token);
AuthStateProvider.NotifyAuthenticationStateChanged(GetAuthenticationStateAsync().Result);
Navigation.NavigateTo("/");
}
}
}In your login component, call the API and store the token:
Protect your pages with `[Authorize]` attribute on components or routes. This integrates JWT seamlessly into your Blazor WASM authentication flow, ensuring API calls are authorized.
Adding OAuth for External Provider Integration
For enterprise apps, users often want to log in with Google, Microsoft, or other providers. OAuth 2.0 makes this straightforward. In your server `Program.cs`, add:builder.Services.AddAuthentication()
.AddGoogle(options =>
{
options.ClientId = builder.Configuration["Authentication:Google:ClientId"];
options.ClientSecret = builder.Configuration["Authentication:Google:ClientSecret"];
});Configure in `appsettings.json` with your OAuth app credentials from Google Developer Console.
<AuthorizeView>
<NotAuthorized>
<button @onclick="LoginWithGoogle">Login with Google</button>
</NotAuthorized>
</AuthorizeView>@code {
private async Task LoginWithGoogle()
{
// Redirect to /authentication/login?provider=Google
Navigation.NavigateTo("authentication/login?provider=Google", true);
}
}On the client, use `AuthorizeView` in Razor components to show login buttons:
Scaffold the authentication pages again, selecting "Individual User Accounts" with OAuth support. This sets up the flow where users authenticate externally, and Identity links the external account to a local user.
Role-Based Access Control in Blazor WASM
Roles add granularity to your security. Admins might edit data, while users only view it. After login, assign roles via Identity:var roles = await _userManager.GetRolesAsync(user);
var roleClaims = roles.Select(r => new Claim(ClaimTypes.Role, r));
var allClaims = claims.Concat(roleClaims);builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin"));
});In Blazor components, use policy-based authorization:
<AuthorizeView Policy="AdminOnly">
<Authorized>
<p>Admin content here.</p>
</Authorized>
<NotAuthorized>
<p>Access denied.</p>
</NotAuthorized>
</AuthorizeView>Then, in a component:
For API endpoints, add `[Authorize(Roles = "Admin")]` to controllers. This ensures role-based access works across your C# web app.
Best Practices for Enterprise-Level Security
To take your Blazor WASM authentication to enterprise standards:- Use HTTPS everywhere to encrypt tokens.
- Implement token refresh to avoid frequent logins.
- Add two-factor authentication (2FA) via Identity's built-in support.
- Regularly audit claims and validate tokens server-side.
- Monitor for common attacks like XSS or CSRF, which Blazor helps mitigate but doesn't eliminate.
Test thoroughly: try logging in with invalid credentials, accessing protected routes without roles, and integrating OAuth logins.