DEV Community

Cover image for Permission-Based Authorization in ASP.NET Core: A Step-by-Step Guide
Alex Mutegi
Alex Mutegi

Posted on

Permission-Based Authorization in ASP.NET Core: A Step-by-Step Guide

Introduction

Authorization is a critical aspect of building secure applications. In ASP.NET Core, the built-in authorization framework can be extended to support fine-grained permission-based control. This article explains how to implement a permission-based authorization system using custom attributes, policies, and handlers.

Overview of Permission-Based Authorization

Permissions provide a more granular approach to controlling user access compared to role-based authorization. By associating specific actions with permissions, developers can enforce precise rules for accessing resources.

Key Components of the System

1. PermissionsEnum
The PermissionsEnum defines all the permissions in the system. Each permission is assigned a unique value:

public enum PermissionsEnum
{
    // Users
    UserRead = 1,
    UserCreate = 2,
    UserModify = 3,
    UserDelete = 4,
    // Roles
    RoleRead = 5,
    RoleCreate = 6,
    RoleModify = 7,
    RoleDelete = 8
}
Enter fullscreen mode Exit fullscreen mode

2. Controller with Permission Attribute
Permissions are applied to controller actions using the HasPermission attribute. For example:

[HttpGet("users")]
[HasPermission(PermissionsEnum.UserRead)]
public async Task<ActionResult<PaginatedResponse<GetUserDTO>>> GetAllUsers(int pageNumber = 1, int pageSize = 10, string search = null)
{
    var result = await _user.GetUsersAsync(pageNumber, pageSize, search);

    return Ok(result);
}
Enter fullscreen mode Exit fullscreen mode

The HasPermission attribute ensures that users must have at least one of the specified permissions to access this endpoint. ie.
[HasPermission(PermissionsEnum.UserRead, PermissionsEnum.UserRead)]

3. Custom Attribute: HasPermission
The HasPermission attribute simplifies permission checks by associating multiple permissions with a policy:

using Domain.Enums; // specify directory for permissions enums
using Microsoft.AspNetCore.Authorization;

public sealed class HasPermissionAttribute : AuthorizeAttribute
{
    public HasPermissionAttribute(params PermissionsEnum[] permissions)
        : base(policy: string.Join(",", permissions.Select(p => p.ToString())))
    {
    }
}
Enter fullscreen mode Exit fullscreen mode

4. Policy Provider
The PermissionAuthorizationPolicyProvider dynamically generates authorization policies based on permission names:

using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;

public class PermissionAuthorizationPolicyProvider : DefaultAuthorizationPolicyProvider
{
    public PermissionAuthorizationPolicyProvider(IOptions<AuthorizationOptions> options)
        : base(options) { }

    public override async Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
    {
        var policy = await base.GetPolicyAsync(policyName);

        if (policy is not null)
            return policy;

        var permissions = policyName.Split(',');

        return new AuthorizationPolicyBuilder()
            .AddRequirements(new PermissionRequirement(permissions))
            .Build();
    }
}
Enter fullscreen mode Exit fullscreen mode

5. Authorization Handler
The PermissionAuthorizationHandler evaluates if the user has any of the required permissions:

using Microsoft.AspNetCore.Authorization;

public class PermissionAuthorizationHandler : AuthorizationHandler<PermissionRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
    {
        var userPermissions = context.User.Claims
                                      .Where(c => c.Type == "permission")
                                      .Select(c => c.Value);

        if (requirement.Permissions.Any(permission => userPermissions.Contains(permission)))
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}
Enter fullscreen mode Exit fullscreen mode

Integration in ASP.NET Core

Service Configuration
Register the required services in Program.cs:

services.AddAuthorization();
services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();
services.AddSingleton<IAuthorizationPolicyProvider, PermissionAuthorizationPolicyProvider>();
Enter fullscreen mode Exit fullscreen mode

Adding Permissions to Users

Permissions should be added as claims to user identities during login or token generation:

public async Task<LoginResponse> LoginUserAsync(LoginDTO loginDTO)
{
    var getUser = await FindUserByEmail(loginDTO.Email!);
    if (getUser == null)
        return new LoginResponse(false, "Invalid credentials!");

    bool checkPassword = BCrypt.Net.BCrypt.Verify(loginDTO.Password, getUser.Password);
    if (checkPassword)
    {
        if (getUser.EmailVerifiedOn is null)
            return new LoginResponse(false, "Your email is not verified");

        var accessToken = await _authTokenService.GenerateToken(getUser);
        var refreshToken = _authTokenService.GenerateRefreshToken();
        _authTokenService.SetRefreshTokenAsHttpOnlyCookie(refreshToken);

        // Update database
        getUser.RefreshToken = refreshToken.TokenValue;
        getUser.RefreshTokenExpiryTime = refreshToken.Expires;
        await _appDbContext.SaveChangesAsync();

        return new LoginResponse(true, "Login successful", accessToken);
    }
    else
        return new LoginResponse(false, "Invalid credentials!");
}
Enter fullscreen mode Exit fullscreen mode
public async Task<string> GenerateToken(User user)
{
    var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]!));
    var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

    var userClaims = new List<Claim>()
    {
        new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
        new Claim(JwtRegisteredClaimNames.Name, user.Name),
        new Claim(JwtRegisteredClaimNames.Email, user.Email)
    };

    foreach (var permission in await _user.GetUserPermissions(user))
        userClaims.Add(new Claim("permission", permission.Name));

    var token = new JwtSecurityToken(
        issuer: _configuration["Jwt:Issuer"],
        audience: _configuration["Jwt:Audience"],
        claims: userClaims,
        expires: DateTime.UtcNow.AddHours(1),
        signingCredentials: credentials
    );

    return new JwtSecurityTokenHandler().WriteToken(token);
}

public RefreshToken GenerateRefreshToken()
{
    var refreshToken = new RefreshToken
    {
        TokenValue = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64)),
        Expires = DateTime.UtcNow.AddDays(7)
    };

    return refreshToken;
}

public void SetRefreshTokenAsHttpOnlyCookie(RefreshToken refreshToken)
{
    var cookieOptions = new CookieOptions
    {
        HttpOnly = true,
        Secure = true,
        SameSite = SameSiteMode.None,
        Expires = refreshToken.Expires
    };

    var httpContext = _httpContextAccessor.HttpContext;
    httpContext?.Response.Cookies.Append(_configuration["Jwt:RefreshTokenCookieKey"], refreshToken.TokenValue, cookieOptions);
}
Enter fullscreen mode Exit fullscreen mode

Database Seeder

To get started, you may need to seed permissions into your database. Additionally, it’s a good practice to seed an Admin role and user when running your application for the first time. Here's how you can do it:

public static class DatabaseSeeder
{
    public static async void SeedAsync(IServiceProvider serviceProvider)
    {
        using var scope = serviceProvider.CreateScope();
        var _context = scope.ServiceProvider.GetRequiredService<AppDbContext>();

        // Ensure the database is migrated
        await _context.Database.MigrateAsync();

        // 1. Seed Permissions
        var permissions = Enum.GetValues(typeof(PermissionsEnum))
                                  .Cast<PermissionsEnum>()
                                  .Select(e => new Permission { Name = e.ToString() })
                                  .ToList(); // Get permissions from Enum File

        var permissionToAdd = new List<Permission>();

        foreach (var permission in permissions)
        {
            if (!_context.Permissions.Any(p => p.Name == permission.Name))
            {
                permissionToAdd.Add(new Permission
                {
                    Name = permission.Name,
                });
            }
        }

        await _context.AddRangeAsync(permissionToAdd);
        await _context.SaveChangesAsync();

        // 2.Seed Admin Role
        var adminRole = new Role { Name = "Admin" };

        if (!_context.Roles.Any(r => r.Name == adminRole.Name))
        {
            _context.Roles.Add(adminRole);
            await _context.SaveChangesAsync();
        }
        else
        {
            adminRole = _context.Roles.FirstOrDefault(r => r.Name == adminRole.Name);
        }

        // 3. Assign all permissions to Admin role
        var storedPermissions = await _context.Permissions.ToListAsync();
        foreach (var permission in storedPermissions)
        {
            if (!_context.RolePermissions.Any(rp => rp.RoleId == adminRole.Id && rp.PermissionId == permission.Id))
            {
                _context.RolePermissions.Add(new RolePermission
                {
                    RoleId = adminRole.Id,
                    PermissionId = permission.Id
                });
            }
        }
        await _context.SaveChangesAsync();

        // 4. Seed Admin user
        var adminUser = new User
        {
            Name = "Alex Mutegi",
            Email = "username@example.com",
            Password = BCrypt.Net.BCrypt.HashPassword("MyStrongPassword"),
            EmailVerifiedOn = DateTime.UtcNow
        };

        if (!_context.Users.Any(u => u.Email == adminUser.Email))
        {
            _context.Users.Add(adminUser);
            await _context.SaveChangesAsync();
        }
        else
        {
            adminUser = _context.Users.FirstOrDefault(u => u.Email == adminUser.Email);
        }

        // 5. Assign Admin Role to Admin User
        if (!_context.UserRoles.Any(ur => ur.UserId == adminUser.Id && ur.RoleId == adminRole.Id))
        {
            _context.UserRoles.Add(new UserRole
            {
                UserId = adminUser.Id,
                RoleId = adminRole.Id
            });
        }

        await _context.SaveChangesAsync();
    }
}
Enter fullscreen mode Exit fullscreen mode

On the Program.cs, register the seeder like below:

var app = builder.Build();
DatabaseSeeder.SeedAsync(app.Services);
Enter fullscreen mode Exit fullscreen mode

Database diagram might look like the below:

Image description

Conclusion

With this setup, your ASP.NET Core application now supports a robust permission-based authorization system. By leveraging the flexibility of custom attributes, policy providers, and handlers, you can enforce fine-grained control over your application’s resources.

Top comments (0)