Securing your Blazor application requires understanding both authentication (who the user is) and authorization (what they can do). This guide covers implementing both in Blazor Server and WebAssembly applications using ASP.NET Core Identity.
Setting Up ASP.NET Core Identity
First, add the required packages and configure Identity in your Program.cs:
var builder = WebApplication.CreateBuilder(args);
// Add database context
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
// Add Identity
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
// Password settings
options.Password.RequireDigit = true;
options.Password.RequireLowercase = true;
options.Password.RequireUppercase = true;
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = true;
// Lockout settings
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
// User settings
options.User.RequireUniqueEmail = true;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// Configure cookie authentication
builder.Services.ConfigureApplicationCookie(options =>
{
options.LoginPath = "/Account/Login";
options.LogoutPath = "/Account/Logout";
options.AccessDeniedPath = "/Account/AccessDenied";
options.ExpireTimeSpan = TimeSpan.FromDays(14);
options.SlidingExpiration = true;
});
// Add authorization policies
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy =>
policy.RequireRole("Admin"));
options.AddPolicy("RequireManager", policy =>
policy.RequireRole("Admin", "Manager"));
options.AddPolicy("PremiumUser", policy =>
policy.RequireClaim("Subscription", "Premium"));
options.AddPolicy("MinimumAge", policy =>
policy.RequireAssertion(context =>
{
var ageClaim = context.User.FindFirst("Age");
return ageClaim != null && int.Parse(ageClaim.Value) >= 18;
}));
});
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.Run();
Custom ApplicationUser
Extend IdentityUser with additional properties:
public class ApplicationUser : IdentityUser
{
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public DateTime DateOfBirth { get; set; }
public string? ProfilePictureUrl { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public bool IsActive { get; set; } = true;
public string FullName => $"{FirstName} {LastName}";
}
Authentication Service
Create a service to handle authentication operations:
public interface IAuthService
{
Task<AuthResult> RegisterAsync(RegisterRequest request);
Task<AuthResult> LoginAsync(LoginRequest request);
Task LogoutAsync();
Task<ApplicationUser?> GetCurrentUserAsync();
}
public class AuthService : IAuthService
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly ILogger<AuthService> _logger;
public AuthService(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
ILogger<AuthService> logger)
{
_userManager = userManager;
_signInManager = signInManager;
_logger = logger;
}
public async Task<AuthResult> RegisterAsync(RegisterRequest request)
{
var user = new ApplicationUser
{
UserName = request.Email,
Email = request.Email,
FirstName = request.FirstName,
LastName = request.LastName,
DateOfBirth = request.DateOfBirth
};
var result = await _userManager.CreateAsync(user, request.Password);
if (result.Succeeded)
{
_logger.LogInformation("User {Email} created successfully", request.Email);
// Assign default role
await _userManager.AddToRoleAsync(user, "User");
// Sign in the user
await _signInManager.SignInAsync(user, isPersistent: false);
return AuthResult.Success();
}
return AuthResult.Failure(result.Errors.Select(e => e.Description));
}
public async Task<AuthResult> LoginAsync(LoginRequest request)
{
var user = await _userManager.FindByEmailAsync(request.Email);
if (user == null || !user.IsActive)
{
return AuthResult.Failure(new[] { "Invalid email or password" });
}
var result = await _signInManager.PasswordSignInAsync(
user,
request.Password,
request.RememberMe,
lockoutOnFailure: true);
if (result.Succeeded)
{
_logger.LogInformation("User {Email} logged in", request.Email);
return AuthResult.Success();
}
if (result.IsLockedOut)
{
_logger.LogWarning("User {Email} account locked out", request.Email);
return AuthResult.Failure(new[] { "Account is locked. Try again later." });
}
return AuthResult.Failure(new[] { "Invalid email or password" });
}
public async Task LogoutAsync()
{
await _signInManager.SignOutAsync();
}
public async Task<ApplicationUser?> GetCurrentUserAsync()
{
var principal = await _signInManager.Context.AuthenticateAsync();
if (principal?.Principal?.Identity?.IsAuthenticated != true)
{
return null;
}
return await _userManager.GetUserAsync(principal.Principal);
}
}
public record RegisterRequest(
string Email,
string Password,
string FirstName,
string LastName,
DateTime DateOfBirth);
public record LoginRequest(
string Email,
string Password,
bool RememberMe);
public class AuthResult
{
public bool Succeeded { get; private set; }
public IEnumerable<string> Errors { get; private set; } = Enumerable.Empty<string>();
public static AuthResult Success() => new() { Succeeded = true };
public static AuthResult Failure(IEnumerable<string> errors) => new() { Succeeded = false, Errors = errors };
}
Login Component
Create a login page component:
@page "/account/login"
@inject IAuthService AuthService
@inject NavigationManager Navigation
<PageTitle>Login</PageTitle>
<div class="login-container">
<h3>Login</h3>
@if (errorMessage is not null)
{
<div class="alert alert-danger">@errorMessage</div>
}
<EditForm Model="loginModel" OnValidSubmit="HandleLogin" FormName="login">
<DataAnnotationsValidator />
<ValidationSummary class="text-danger" />
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<InputText id="email" @bind-Value="loginModel.Email" class="form-control" />
<ValidationMessage For="() => loginModel.Email" class="text-danger" />
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<InputText id="password" type="password" @bind-Value="loginModel.Password" class="form-control" />
<ValidationMessage For="() => loginModel.Password" class="text-danger" />
</div>
<div class="mb-3 form-check">
<InputCheckbox id="rememberMe" @bind-Value="loginModel.RememberMe" class="form-check-input" />
<label for="rememberMe" class="form-check-label">Remember me</label>
</div>
<button type="submit" class="btn btn-primary" disabled="@isLoading">
@if (isLoading)
{
<span class="spinner-border spinner-border-sm"></span>
}
Login
</button>
</EditForm>
<div class="mt-3">
<a href="/account/register">Create an account</a>
<span class="mx-2">|</span>
<a href="/account/forgot-password">Forgot password?</a>
</div>
</div>
@code {
private LoginModel loginModel = new();
private string? errorMessage;
private bool isLoading;
[SupplyParameterFromQuery]
public string? ReturnUrl { get; set; }
private async Task HandleLogin()
{
isLoading = true;
errorMessage = null;
try
{
var result = await AuthService.LoginAsync(new LoginRequest(
loginModel.Email,
loginModel.Password,
loginModel.RememberMe));
if (result.Succeeded)
{
Navigation.NavigateTo(ReturnUrl ?? "/", forceLoad: true);
}
else
{
errorMessage = string.Join(", ", result.Errors);
}
}
finally
{
isLoading = false;
}
}
private class LoginModel
{
[Required(ErrorMessage = "Email is required")]
[EmailAddress(ErrorMessage = "Invalid email address")]
public string Email { get; set; } = string.Empty;
[Required(ErrorMessage = "Password is required")]
public string Password { get; set; } = string.Empty;
public bool RememberMe { get; set; }
}
}
Protecting Components with Authorize Attribute
Use the Authorize attribute to protect components:
@page "/admin"
@attribute [Authorize(Roles = "Admin")]
<h3>Admin Dashboard</h3>
<p>Only admins can see this page.</p>
@page "/settings"
@attribute [Authorize]
<h3>User Settings</h3>
<p>Any authenticated user can see this page.</p>
@page "/premium-content"
@attribute [Authorize(Policy = "PremiumUser")]
<h3>Premium Content</h3>
<p>Only premium subscribers can access this.</p>
AuthorizeView Component
Show different content based on authentication state:
<AuthorizeView>
<Authorized>
<p>Hello, @context.User.Identity?.Name!</p>
<button @onclick="Logout">Logout</button>
</Authorized>
<NotAuthorized>
<a href="/account/login">Login</a>
<a href="/account/register">Register</a>
</NotAuthorized>
</AuthorizeView>
@code {
[Inject]
private IAuthService AuthService { get; set; } = default!;
[Inject]
private NavigationManager Navigation { get; set; } = default!;
private async Task Logout()
{
await AuthService.LogoutAsync();
Navigation.NavigateTo("/", forceLoad: true);
}
}
Role-based AuthorizeView:
<AuthorizeView Roles="Admin,Manager">
<Authorized>
<button @onclick="DeleteUser">Delete User</button>
</Authorized>
</AuthorizeView>
<AuthorizeView Policy="PremiumUser">
<Authorized>
<div class="premium-features">
<h4>Premium Features</h4>
<!-- Premium content here -->
</div>
</Authorized>
<NotAuthorized>
<div class="upgrade-prompt">
<p>Upgrade to Premium to access these features.</p>
<a href="/pricing">View Plans</a>
</div>
</NotAuthorized>
</AuthorizeView>
Accessing User Information in Components
Use the cascading AuthenticationState parameter:
@code {
[CascadingParameter]
private Task<AuthenticationState> AuthStateTask { get; set; } = default!;
private string? userName;
private bool isAdmin;
protected override async Task OnInitializedAsync()
{
var authState = await AuthStateTask;
var user = authState.User;
if (user.Identity?.IsAuthenticated == true)
{
userName = user.Identity.Name;
isAdmin = user.IsInRole("Admin");
}
}
}
Or inject AuthenticationStateProvider:
@inject AuthenticationStateProvider AuthProvider
@code {
private ClaimsPrincipal? user;
protected override async Task OnInitializedAsync()
{
var authState = await AuthProvider.GetAuthenticationStateAsync();
user = authState.User;
}
private string GetUserClaim(string claimType)
{
return user?.FindFirst(claimType)?.Value ?? string.Empty;
}
}
Custom Authorization Handler
Create custom authorization requirements:
public class MinimumAgeRequirement : IAuthorizationRequirement
{
public int MinimumAge { get; }
public MinimumAgeRequirement(int minimumAge)
{
MinimumAge = minimumAge;
}
}
public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
MinimumAgeRequirement requirement)
{
var dateOfBirthClaim = context.User.FindFirst(c => c.Type == "DateOfBirth");
if (dateOfBirthClaim is null)
{
return Task.CompletedTask;
}
if (DateTime.TryParse(dateOfBirthClaim.Value, out var dateOfBirth))
{
var age = DateTime.Today.Year - dateOfBirth.Year;
if (dateOfBirth.Date > DateTime.Today.AddYears(-age))
{
age--;
}
if (age >= requirement.MinimumAge)
{
context.Succeed(requirement);
}
}
return Task.CompletedTask;
}
}
// Registration in Program.cs
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AtLeast18", policy =>
policy.Requirements.Add(new MinimumAgeRequirement(18)));
});
Resource-Based Authorization
Authorize access to specific resources:
public class DocumentAuthorizationHandler : AuthorizationHandler<OperationAuthorizationRequirement, Document>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
OperationAuthorizationRequirement requirement,
Document resource)
{
if (requirement.Name == Operations.Read.Name)
{
if (resource.IsPublic || resource.AuthorId == context.User.FindFirstValue(ClaimTypes.NameIdentifier))
{
context.Succeed(requirement);
}
}
else if (requirement.Name == Operations.Update.Name || requirement.Name == Operations.Delete.Name)
{
if (resource.AuthorId == context.User.FindFirstValue(ClaimTypes.NameIdentifier)
|| context.User.IsInRole("Admin"))
{
context.Succeed(requirement);
}
}
return Task.CompletedTask;
}
}
public static class Operations
{
public static OperationAuthorizationRequirement Create = new() { Name = nameof(Create) };
public static OperationAuthorizationRequirement Read = new() { Name = nameof(Read) };
public static OperationAuthorizationRequirement Update = new() { Name = nameof(Update) };
public static OperationAuthorizationRequirement Delete = new() { Name = nameof(Delete) };
}
// Usage in a component
@inject IAuthorizationService AuthorizationService
@code {
private Document document = default!;
private bool canEdit;
private bool canDelete;
protected override async Task OnInitializedAsync()
{
var authState = await AuthStateTask;
var user = authState.User;
var editResult = await AuthorizationService.AuthorizeAsync(user, document, Operations.Update);
canEdit = editResult.Succeeded;
var deleteResult = await AuthorizationService.AuthorizeAsync(user, document, Operations.Delete);
canDelete = deleteResult.Succeeded;
}
}
Role Management
Manage user roles:
public class RoleService : IRoleService
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly RoleManager<IdentityRole> _roleManager;
public RoleService(
UserManager<ApplicationUser> userManager,
RoleManager<IdentityRole> roleManager)
{
_userManager = userManager;
_roleManager = roleManager;
}
public async Task<bool> CreateRoleAsync(string roleName)
{
if (await _roleManager.RoleExistsAsync(roleName))
{
return false;
}
var result = await _roleManager.CreateAsync(new IdentityRole(roleName));
return result.Succeeded;
}
public async Task<bool> AddUserToRoleAsync(string userId, string roleName)
{
var user = await _userManager.FindByIdAsync(userId);
if (user is null)
{
return false;
}
var result = await _userManager.AddToRoleAsync(user, roleName);
return result.Succeeded;
}
public async Task<bool> RemoveUserFromRoleAsync(string userId, string roleName)
{
var user = await _userManager.FindByIdAsync(userId);
if (user is null)
{
return false;
}
var result = await _userManager.RemoveFromRoleAsync(user, roleName);
return result.Succeeded;
}
public async Task<IList<string>> GetUserRolesAsync(string userId)
{
var user = await _userManager.FindByIdAsync(userId);
if (user is null)
{
return new List<string>();
}
return await _userManager.GetRolesAsync(user);
}
}
Conclusion
Blazor provides comprehensive tools for implementing authentication and authorization. Using ASP.NET Core Identity gives you a solid foundation for user management, while the authorization system provides flexible options for controlling access.
Key takeaways:
- Use ASP.NET Core Identity for user management
- Protect pages with the [Authorize] attribute
- Use AuthorizeView for conditional rendering
- Create custom policies for complex authorization rules
- Implement resource-based authorization when needed
- Access user information through AuthenticationState
With these patterns, you can build secure Blazor applications that properly authenticate users and authorize their actions throughout the application.