Handling Authorization and Role-Based Access Control (RBAC)
Learn to implement secure, permission-based authorization in Clean Architecture with custom attributes and JWT integration for effective API security.
Introduction
Role-Based Access Control (RBAC) is a critical component of secure application design that restricts access to resources based on user roles and permissions. This article explores how the Contact Management Application implements a flexible and maintainable RBAC system that integrates with JWT authentication to secure API endpoints, while maintaining the separation of concerns that Clean Architecture demands.
This article provides a detailed, step-by-step guide to implementing RBAC in the Contact Management Application, covering:
- Entities and relationships for RBAC. 
- Dynamic policy registration during application startup. 
- Custom authorization attributes for securing API endpoints. 
- Custom permission handler to validate user access. 
- The benefits of this modular and flexible approach. 
Part of the Series
This article is part of a series covering the Contact Management Application, a sample project demonstrating Clean Architecture principles. Other articles include:
- Clean Architecture: Implementing AutoMapper for DTO Mapping and Audit Logging 
- Clean Architecture: Dependency Injection Setup Across Layers 
- Clean Architecture: Handling Authorization and Role-Based Access Control (RBAC) (You are here) 
- Clean Architecture: Implementing Activity Logging with Custom Attributes 
- Clean Architecture: Unit of Work Pattern and Its Role in Managing Transactions 
- Clean Architecture: Using Dapper for Data Access and Repository Pattern 
- Clean Architecture: Best Practices for Creating and Using DTOs in the API 
- Clean Architecture: Error Handling and Exception Management in the API 
- Clean Architecture: Dockerizing the .NET Core API, Angular and MS SQL Server 
- Clean Architecture: Seeding Initial Data Using Docker Compose and SQL Scripts 
1. RBAC Architecture and Entities
1.1 Core Entities
The system uses a modular approach where entities define relationships between roles, permissions, pages, and operations.
Page Entity
Represents a resource or section in the application (e.g., Contacts). For example, the “Contacts” feature allows users to interact with contact-related data.
1
2
3
4
5
public class Page : BaseEntity
{
    public required string Name { get; set; }
    public required string Url { get; set; }
}
| Id | Name | Url / Feature / Api | 
|---|---|---|
| 3f2b1c4d-1234-5678-8901-abcd12345678 | Contacts | /contacts | 
#### 
 Operation Entity
The Operation entity defines the actions that can be performed on a page / feature/ api. These include operations like reading, writing, updating, and deleting data.
1
2
3
4
5
6
public class Operation : BaseEntity
{
    public required string Name { get; set; }
    public required string Description { get; set; }
}
| Id | Name | Description | 
|---|---|---|
| op-1-1234-5678-8901-abcd12345678 | Read | Allows reading data | 
| op-2-2234-5678-8901-abcd23456789 | Write | Allows writing data | 
| op-3-3234-5678-8901-abcd34567890 | Update | Allows updating data | 
| op-4-4234-5678-8901-abcd45678901 | Delete | Allows deleting data | 
#### 
 PageOperationMapping Entity
The PageOperationMapping entity connects a page to its corresponding operations. This forms the base for assigning granular permissions.
1
2
3
4
5
public class PageOperationMapping : BaseEntity
{
    public string PageName { get; set; }
    public string OperationName { get; set; }
}
| Id | PageId | OperationId | 
|---|---|---|
| pom-1-1234-5678-8901-abcd12345678 | 3f2b1c4d-1234-5678-8901-abcd12345678 | op-1-1234-5678-8901-abcd12345678 | 
| pom-2-2234-5678-8901-abcd23456789 | 3f2b1c4d-1234-5678-8901-abcd12345678 | op-2-2234-5678-8901-abcd23456789 | 
| pom-3-3234-5678-8901-abcd34567890 | 3f2b1c4d-1234-5678-8901-abcd12345678 | op-3-3234-5678-8901-abcd34567890 | 
| pom-4-4234-5678-8901-abcd45678901 | 3f2b1c4d-1234-5678-8901-abcd12345678 | op-4-4234-5678-8901-abcd45678901 | 
#### 
 Role Entity
The Role entity defines user roles such as Admin or User. Roles determine the level of access users have to different operations on a page.
1
2
3
4
5
public class Role : BaseEntity
{
    public required string Name { get; set; }
    public required string Description { get; set; }
}
| Id | Name | Description | 
|---|---|---|
| role-1-1234-5678-8901-abcd12345678 | Admin | Full access to Contacts | 
| role-2-2234-5678-8901-abcd23456789 | User | Read-only access to Contacts | 
#### 
 RolePermission Entity
The RolePermission entity connects roles to specific permissions by linking to the PageOperationMapping table. This determines what operations each role can perform on a page.
1
2
3
4
5
public class RolePermission : BaseEntity
{
    public Guid RoleId { get; set; }
    public Guid PermissionId { get; set; }
}
| Id | RoleId | PageOperationMappingId | 
|---|---|---|
| rp-1-1234-5678-8901-abcd12345678 | role-1-1234-5678-8901-abcd12345678 | pom-1-1234-5678-8901-abcd12345678 | 
| rp-2-2234-5678-8901-abcd23456789 | role-1-1234-5678-8901-abcd12345678 | pom-2-2234-5678-8901-abcd23456789 | 
| rp-3-3234-5678-8901-abcd34567890 | role-1-1234-5678-8901-abcd12345678 | pom-3-3234-5678-8901-abcd34567890 | 
| rp-4-4234-5678-8901-abcd45678901 | role-1-1234-5678-8901-abcd12345678 | pom-4-4234-5678-8901-abcd45678901 | 
| rp-5-1234-5678-8901-abcd56789012 | role-2-2234-5678-8901-abcd23456789 | pom-1-1234-5678-8901-abcd12345678 | 
#### 
 UserRole Entity
The UserRole entity maps users to their roles, defining what permissions they inherit.
1
2
3
4
5
public class UserRole : BaseEntity
{
    public Guid UserId { get; set; }
    public Guid RoleId { get; set; }
}
| Id | UserId | RoleId | 
|---|---|---|
| ur-1-1234-5678-8901-abcd12345678 | user-1-1234-5678-8901-abcd12345678 | role-1-1234-5678-8901-abcd12345678 | 
| ur-2-2234-5678-8901-abcd23456789 | user-2-2234-5678-8901-abcd23456789 | role-2-2234-5678-8901-abcd23456789 | 
#### 
 User Entity
The User entity represents application users. Each user can be assigned a role that determines their access level.
1
2
3
4
5
6
7
8
9
public class User : BaseEntity
{
    public required string FirstName { get; set; }
    public required string LastName { get; set; }
    public required string Username { get; set; }
    public required long Mobile { get; set; }
    public required string Email { get; set; }
    public required string Password { get; set; }
}
| Id | FirstName | LastName | Username | Mobile | Password | |
|---|---|---|---|---|---|---|
| user-1-1234-5678-8901-abcd12345678 | Alice | Smith | alice.smith | 1234567890 | alice@email.com | hashedPw1 | 
| user-2-2234-5678-8901-abcd23456789 | Bob | Johnson | bob.johnson | 9876543210 | bob@email.com | hashedPw2 | 
2. Dynamic Policy Registration
Policies are dynamically created at application startup based on the mappings between pages and operations.
2.1 Fetching Permission Mappings
The PermissionRepository retrieves mappings of pages and operations:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public async Task<IEnumerable<PageOperationMapping>> GetPageOperationMappingsAsync()
{
    var sql = @"
        SELECT
            p.Name AS PageName,
            o.Name AS OperationName
        FROM Permissions perm
        INNER JOIN Pages p ON perm.PageId = p.Id
        INNER JOIN Operations o ON perm.OperationId = o.Id
        ORDER BY p.Name, o.Name;";
    return await _dapperHelper.GetAll<PageOperationMapping>(sql, null);
}
2.2 Registering Policies in Startup.cs
Dynamic policies are added based on the mappings fetched by IPermissionService:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
builder.Services.AddAuthorization(options =>
{
    var permissionService = builder.Services.BuildServiceProvider().GetRequiredService<IPermissionService>();
    var permissionMappings = permissionService.GetAllPageOperationMappingsAsync().Result;
    foreach (var mapping in permissionMappings)
    {
        var policyName = $"{mapping.PageName}.{mapping.OperationName}Policy";
        options.AddPolicy(policyName, policy =>
        {
            policy.Requirements.Add(new PermissionRequirement(policyName));
        });
    }
});
3. Custom Authorization Attribute
3.1 AuthorizePermission Attribute
This custom attribute simplifies the application of permissions:
1
2
3
4
5
6
7
8
public class AuthorizePermissionAttribute : AuthorizeAttribute
{
    public AuthorizePermissionAttribute(string permission)
    {
        Policy = $"{permission}Policy";
    }
}
3.2 Securing Endpoints
Apply the AuthorizePermission attribute to secure actions in the API:
1
2
3
4
5
6
7
8
[HttpPost]
[ActivityLog("Creating new Contact")]
[AuthorizePermission("Contacts.Create")]
public async Task<IActionResult> Add(CreateContactPerson createContactPerson)
{
    var createdContactPerson = await _contactPersonService.Add(createContactPerson);
    return CreatedAtAction(nameof(GetById), new { id = createdContactPerson.Id }, createdContactPerson);
}
Each action enforces a specific permission, such as Contacts.Create.
4. Custom Permission Handler
4.1 PermissionHandler Implementation
The PermissionHandler validates whether the current user has the required permissions:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class PermissionHandler : AuthorizationHandler<PermissionRequirement>
{
    private readonly IServiceProvider _serviceProvider;
    public PermissionHandler(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
    {
        using var scope = _serviceProvider.CreateScope();
        var _userService = scope.ServiceProvider.GetRequiredService<IUserService>();
        var _rolePermissionService = scope.ServiceProvider.GetRequiredService<IRolePermissionService>();
        if (context.User.Identity.IsAuthenticated)
        {
            var userId = _userService.GetUserId(context.User);
            var roles = await _userService.GetUserRolesAsync(context.User);
            var rolePermissionMappings = await_rolePermissionService.GetRolePermissionMappingsAsync();
            var userPermissions = rolePermissionMappings
                .Where(rpm => roles.Contains(rpm.RoleName))
                .Select(rpm => $"{rpm.PageName}.{rpm.OperationName}Policy");
            if (userPermissions.Contains(requirement.Permission))
            {
                context.Succeed(requirement);
            }
        }
    }
}
5. Benefits of the RBAC Implementation
- Granular Access Control: Each API action can be tied to a fine-grained permission. 
- Dynamic Scalability: New permissions can be added without rewriting existing code. 
- Modularity: Each layer (API, Application, Infrastructure) handles its responsibilities. 
- Maintainability: Simplified management of roles and permissions through dynamic policies. 
Conclusion
This article demonstrated how to implement Role-Based Access Control in the Contact Management Application using Clean Architecture principles. By leveraging dynamic policies, custom attributes, and a modular structure, this approach provides flexibility, security, and scalability.
For a more detailed look at the project, you can access the full source code on GitHub:
**GitHub Repository: Contact Management Application **
In the next article, we will discuss Activity Logging to track user actions within the system.

