A robust authentication and authorization system begins with a well-designed database schema. By defining custom tables tailored to your application’s requirements, you gain greater control and flexibility over user management.
Design the following tables to manage users and their roles:
UserId
(Primary Key)Username
PasswordHash
Email
RoleId
(Primary Key)RoleName
UserId
(Foreign Key)RoleId
(Foreign Key)If your application requires fine-grained permissions, consider adding:
PermissionId
(Primary Key)PermissionName
RoleId
(Foreign Key)PermissionId
(Foreign Key)Begin by creating a Blazor application tailored to your deployment needs.
Use the appropriate template in your development environment (e.g., Visual Studio) to initialize the project.
EF Core serves as the bridge between your application and the database, enabling efficient data manipulation and querying.
Install the necessary EF Core packages based on your database provider:
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
Define a custom DbContext
to represent your database session.
// Custom DbContext
public class CustomDbContext : DbContext
{
public DbSet<ApplicationUser> Users { get; set; }
public DbSet<Role> Roles { get; set; }
public DbSet<UserRole> UserRoles { get; set; }
// Additional DbSets for Permissions if needed
public CustomDbContext(DbContextOptions<CustomDbContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Configure composite keys, relationships, etc.
modelBuilder.Entity<UserRole>()
.HasKey(ur => new { ur.UserId, ur.RoleId });
modelBuilder.Entity<UserRole>()
.HasOne(ur => ur.User)
.WithMany(u => u.UserRoles)
.HasForeignKey(ur => ur.UserId);
modelBuilder.Entity<UserRole>()
.HasOne(ur => ur.Role)
.WithMany(r => r.UserRoles)
.HasForeignKey(ur => ur.RoleId);
}
}
Define the connection string in appsettings.json
:
{
"ConnectionStrings": {
"DefaultConnection": "Server=your_server;Database=your_database;Trusted_Connection=True;MultipleActiveResultSets=true"
},
// Other settings
}
Register the DbContext in Program.cs
:
builder.Services.AddDbContext<CustomDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
Blazor relies on the AuthenticationStateProvider
to manage and provide authentication states throughout the application. Implementing a custom provider allows integration with your custom database schema.
public class CustomAuthenticationStateProvider : AuthenticationStateProvider
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly CustomDbContext _dbContext;
public CustomAuthenticationStateProvider(IHttpContextAccessor httpContextAccessor, CustomDbContext dbContext)
{
_httpContextAccessor = httpContextAccessor;
_dbContext = dbContext;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var identity = new ClaimsIdentity();
if (_httpContextAccessor.HttpContext?.User?.Identity?.IsAuthenticated == true)
{
var username = _httpContextAccessor.HttpContext.User.Identity.Name;
var user = await _dbContext.Users.FirstOrDefaultAsync(u => u.Username == username);
if (user != null)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.Username),
new Claim(ClaimTypes.NameIdentifier, user.UserId.ToString())
// Add additional claims as needed
};
// Attach role claims
var roles = await _dbContext.UserRoles
.Where(ur => ur.UserId == user.UserId)
.Select(ur => ur.Role.RoleName)
.ToListAsync();
foreach (var role in roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
identity = new ClaimsIdentity(claims, "CustomAuth");
}
}
var userPrincipal = new ClaimsPrincipal(identity);
return new AuthenticationState(userPrincipal);
}
public void NotifyUserAuthentication(string username)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, username)
// Add additional claims if necessary
};
var identity = new ClaimsIdentity(claims, "CustomAuth");
var user = new ClaimsPrincipal(identity);
NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(user)));
}
public void NotifyUserLogout()
{
var anonymous = new ClaimsPrincipal(new ClaimsIdentity());
NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(anonymous)));
}
}
In Program.cs
, register the custom AuthenticationStateProvider and other related services:
builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();
builder.Services.AddScoped<IHttpContextAccessor, HttpContextAccessor>();
builder.Services.AddAuthorizationCore();
A User Service encapsulates the logic related to user management, including authentication, retrieval, and other user-centric operations.
public interface IUserService
{
Task<ApplicationUser> AuthenticateAsync(string username, string password);
Task<ApplicationUser> GetCurrentUserAsync();
}
public class UserService : IUserService
{
private readonly CustomDbContext _context;
public UserService(CustomDbContext context)
{
_context = context;
}
public async Task<ApplicationUser> AuthenticateAsync(string username, string password)
{
var user = await _context.Users.FirstOrDefaultAsync(u => u.Username == username);
if (user != null && VerifyPassword(password, user.PasswordHash))
{
return user;
}
return null;
}
private bool VerifyPassword(string password, string hashedPassword)
{
// Implement your password hashing and verification logic here, e.g., BCrypt
return BCrypt.Net.BCrypt.Verify(password, hashedPassword);
}
public async Task<ApplicationUser> GetCurrentUserAsync()
{
var username = _httpContextAccessor.HttpContext?.User?.Identity?.Name;
if (string.IsNullOrEmpty(username))
return null;
return await _context.Users.FirstOrDefaultAsync(u => u.Username == username);
}
}
Add the User Service to the dependency injection container in Program.cs
:
builder.Services.AddScoped<IUserService, UserService>();
Creating intuitive and secure login and logout functionalities ensures users can authenticate seamlessly while maintaining application security.
Develop a login page where users can input their credentials.
@page "/login"
@inject NavigationManager Navigation
@inject IUserService UserService
@inject AuthenticationStateProvider AuthenticationStateProvider
<h3>Login</h3>
<EditForm Model="@loginModel" OnValidSubmit="HandleLogin">
<InputText @bind-Value="loginModel.Username" placeholder="Username" />
<InputText @bind-Value="loginModel.Password" type="password" placeholder="Password" />
<button type="submit">Login</button>
</EditForm>
@code {
private LoginModel loginModel = new();
private async Task HandleLogin()
{
var user = await UserService.AuthenticateAsync(loginModel.Username, loginModel.Password);
if (user != null)
{
((CustomAuthenticationStateProvider)AuthenticationStateProvider).NotifyUserAuthentication(user.Username);
Navigation.NavigateTo("/");
}
else
{
// Handle failed login attempt
}
}
public class LoginModel
{
public string Username { get; set; }
public string Password { get; set; }
}
}
Provide users the ability to logout securely.
public async Task Logout()
{
((CustomAuthenticationStateProvider)AuthenticationStateProvider).NotifyUserLogout();
Navigation.NavigateTo("/login");
}
Role-based authorization ensures that users have access only to the parts of the application that are pertinent to their roles, enhancing security and user experience.
Apply the [Authorize]
attribute to components or pages to restrict access based on roles.
@page "/admin"
@attribute [Authorize(Roles = "Admin")]
<h1>Admin Dashboard</h1>
<!-- Admin-specific content -->
Use the AuthorizeView
component for conditional UI rendering based on user roles.
<AuthorizeView Roles="Admin">
<Authorized>
<p>Welcome, Admin!</p>
</Authorized>
<NotAuthorized>
<p>You do not have access to this section.</p>
</NotAuthorized>
</AuthorizeView>
Create custom authorization policies for more granular control.
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdminRole", policy => policy.RequireRole("Admin"));
});
Apply policies using the [Authorize]
attribute:
@attribute [Authorize(Policy = "RequireAdminRole")]
If your Blazor application interacts with backend APIs, it's crucial to secure these endpoints to prevent unauthorized access and ensure data integrity.
Implement JWT (JSON Web Tokens) for stateless and secure communication between the client and server.
// Generating JWT Token
public string GenerateJwtToken(ApplicationUser user)
{
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, user.Username),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(ClaimTypes.Name, user.Username),
new Claim(ClaimTypes.NameIdentifier, user.UserId.ToString())
// Add additional claims as needed
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("YourSecretKeyHere"));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: "yourdomain.com",
audience: "yourdomain.com",
claims: claims,
expires: DateTime.Now.AddHours(1),
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
Apply the [Authorize]
attribute to API controllers or specific actions to enforce security.
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class SecureController : ControllerBase
{
// Secure actions go here
}
Protecting sensitive data on the frontend involves restricting access to UI elements and routes based on user authentication and authorization states.
Use the [Authorize]
attribute on pages to prevent unauthorized access.
@page "/secure-page"
@attribute [Authorize]
<h3>Secure Page</h3>
<p>This content is protected and only visible to authenticated users.</p>
Render UI components conditionally based on user roles or permissions.
@inject AuthenticationStateProvider AuthenticationStateProvider
<AuthorizeView Roles="Admin">
<Authorized>
<button>Admin Only Action</button>
</Authorized>
<NotAuthorized>
<!-- Optionally render something else -->
</NotAuthorized>
</AuthorizeView>
After implementing authentication and authorization, comprehensive testing is essential to validate the security and functionality of your system.
Implementing authentication and authorization in Blazor using custom database tables provides a tailored and secure framework for managing user access and roles within your application. By meticulously designing your database schema, leveraging a custom AuthenticationStateProvider, and enforcing role-based access controls, you ensure that your application remains both functional and secure. Additionally, safeguarding backend APIs and frontend data presentation fortifies your application against unauthorized access and potential security threats. Thorough testing and adherence to best practices during deployment further enhance the robustness of your Blazor application.