Ithy Logo

Implementing Authentication and Authorization in Blazor with Custom Database Tables

A comprehensive guide to securing your Blazor applications

Blazor authentication process

Key Takeaways

  • Custom Schema Design: Tailor your database schema with essential tables like Users, Roles, and UserRoles to manage authentication and authorization effectively.
  • Custom Authentication Provider: Implement a custom AuthenticationStateProvider to integrate your authentication logic seamlessly with Blazor's security mechanisms.
  • Role-Based Access Control: Utilize role-based authorization to restrict access to specific parts of your application based on user roles, enhancing security and user experience.

1. Defining a Custom Database Schema

Establishing the Foundation for Authentication and Authorization

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.

Essential Tables

Design the following tables to manage users and their roles:

  • Users Table: Stores user credentials and profile information.
    • UserId (Primary Key)
    • Username
    • PasswordHash
    • Email
    • Additional custom fields as needed.
  • Roles Table: Defines various roles within the application.
    • RoleId (Primary Key)
    • RoleName
  • UserRoles Table: Establishes a many-to-many relationship between users and roles.
    • UserId (Foreign Key)
    • RoleId (Foreign Key)

Additional Tables

If your application requires fine-grained permissions, consider adding:

  • Permissions Table: Defines specific permissions.
    • PermissionId (Primary Key)
    • PermissionName
  • RolePermissions Table: Maps roles to their permissions.
    • RoleId (Foreign Key)
    • PermissionId (Foreign Key)

2. Setting Up the Blazor Project

Creating a Blazor Server or WebAssembly Application

Begin by creating a Blazor application tailored to your deployment needs.

  • Blazor Server: Ideal for applications requiring real-time updates and server-side processing.
  • Blazor WebAssembly (WASM): Suitable for client-side applications with offline capabilities.

Use the appropriate template in your development environment (e.g., Visual Studio) to initialize the project.

3. Integrating Entity Framework Core (EF Core)

Configuring the Database Context

EF Core serves as the bridge between your application and the database, enabling efficient data manipulation and querying.

Installing EF Core Packages

Install the necessary EF Core packages based on your database provider:


    dotnet add package Microsoft.EntityFrameworkCore.SqlServer
    dotnet add package Microsoft.EntityFrameworkCore.Tools
    

Creating the DbContext

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);
    }
}

Configuring the Connection String

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")));
    

4. Implementing a Custom Authentication State Provider

Managing Authentication States in Blazor

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.

Creating the CustomAuthenticationStateProvider


    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)));
        }
    }
    

Registering the Custom Provider

In Program.cs, register the custom AuthenticationStateProvider and other related services:


    builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();
    builder.Services.AddScoped<IHttpContextAccessor, HttpContextAccessor>();
    builder.Services.AddAuthorizationCore();
    

5. Developing the User Service

Handling User Operations and Validation

A User Service encapsulates the logic related to user management, including authentication, retrieval, and other user-centric operations.

Defining the IUserService Interface


    public interface IUserService
    {
        Task<ApplicationUser> AuthenticateAsync(string username, string password);
        Task<ApplicationUser> GetCurrentUserAsync();
    }
    

Implementing the UserService


    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);
        }
    }
    

Registering the User Service

Add the User Service to the dependency injection container in Program.cs:


    builder.Services.AddScoped<IUserService, UserService>();
    

6. Implementing Login and Logout Mechanisms

Facilitating User Authentication

Creating intuitive and secure login and logout functionalities ensures users can authenticate seamlessly while maintaining application security.

Creating the Login Page

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; }
        }
    }
    

Implementing Logout Functionality

Provide users the ability to logout securely.


    public async Task Logout()
    {
        ((CustomAuthenticationStateProvider)AuthenticationStateProvider).NotifyUserLogout();
        Navigation.NavigateTo("/login");
    }
    

7. Enforcing Role-Based Authorization

Controlling Access Based on User Roles

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.

Using [Authorize] Attributes

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 -->
    

Employing the AuthorizeView Component

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>
    

Defining Authorization Policies

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")]
    

8. Securing API Endpoints

Protecting Backend Services

If your Blazor application interacts with backend APIs, it's crucial to secure these endpoints to prevent unauthorized access and ensure data integrity.

Using JWT Tokens

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);
    }
    

Protecting API Controllers

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
    }
    

9. Securing the Frontend

Ensuring Data Protection on the Client Side

Protecting sensitive data on the frontend involves restricting access to UI elements and routes based on user authentication and authorization states.

Implementing Route Guards

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>
    

Conditional UI Rendering

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>
    

10. Testing and Deployment

Ensuring Robustness and Security Post-Implementation

After implementing authentication and authorization, comprehensive testing is essential to validate the security and functionality of your system.

Testing Scenarios

  • Authenticating with valid and invalid credentials.
  • Accessing restricted pages with different user roles.
  • Ensuring JWT tokens are correctly issued and validated.
  • Verifying that unauthorized API requests are appropriately blocked.

Deployment Considerations

  • Use HTTPS to encrypt data in transit.
  • Store secrets, such as JWT signing keys, securely (e.g., environment variables or Azure Key Vault).
  • Regularly update dependencies to patch security vulnerabilities.
  • Implement proper error handling to prevent information leakage.

Conclusion

Achieving a Secure and Customizable Blazor Application

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.

References


Last updated January 18, 2025
Search Again