Handling Authorization and Role-Based Access Control (RBAC)
Implement secure Role-Based Access Control (RBAC) in Clean Architecture. This guide shows how to design permission-based authorization, create custom authorization attributes, and integrate with JWT for secure API endpoints.
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.