.NET 8 Series has started! Join Now for FREE

25 min read

Permission-Based Authorization in ASP.NET Core - Complete User Management Guide in .NET 5

#dotnet

Setting up Permissions to access your resources is always a crucial part of your application’s security. In this article, we will implement Permission-Based Authorization in ASP.NET Core that builds upon the concept of Claim-Based Authorization in ASP.NET Core. As usual, we will be building this application right from scratch to get some detailed knowledge about the whole scenario and how it would actually help you secure your projects. You can find the complete source code of the implementation here.

Previously, we built an implementation for Custom User Management where we extended the IdentityUser class and defined custom properties like First Name, Last name, Profile Picture. etc. In this tutorial, we will go a step ahead and implement Permissions for Each User Roles. It is recommended to go through the previous article to get a complete picture of User Management and Securing ASP.NET Core Web Applications.

Let me set up the scenario. We have an application, say Stock Management that is to be secured with access levels. For instance, our application has features that can add new products, view product details, delete/modify products. As an ideal business requirement, not all the users should have the permissions to do everything, right? Admins can Add, Edit Products. Basic users can only view the product. Super-Admin can do whatever he wants to. So, this is our requirement. What’s your first approach to it? Role-Based Authorization is the most common and easy approach that would come into your mind naturally.

Please note that this is the exact implementation of Permission Management that is already implemented in the ASP.NET Core Hero Boilerplate Template (.NET 5 Clean Architecture Template). If you haven’t checked out the Boilerplate template yet, read about it here.

What’s Role-Based Authorization?

Role-Based Authorization in ASP.NET Core is a way to restrict/allow users to access specific resources in the application. The [Authorize] attribute when declared in the Controller or any action methods, restricts users bases on his/her role settings. For instance, the Delete method is accessible only to users who have the Role ‘SuperAdmin’ assigned to them, and so on. You are getting the point, yeah? Let me write down a simple code snippet to give a better picture.

[Authorize]
public class ProductsController : Controller
{
public ActionResult View()
{ }
[Authorize(Roles = "Administrator,SuperAdmin")]
public ActionResult Create()
{ }
[Authorize(Roles = "SuperAdmin")]
public ActionResult Delete()
{ }
[Authorize(Roles = "Administrator")]
[Authorize(Roles = "SuperAdmin")]
public ActionResult SpecialFeature()
{ }
}

The above code is a super-easy demonstration of the concept mentioned.

Line #1 - This Attribute ensures that only Authorized (Logged In) Users can access the ProductsController. If you haven’t logged in and try to access the /products resource, you will be re-directed to the login page or throws a 401 UnAuthorized Exception.
Line #4 - Any Logged In User, no matter of what Role they belong to, can view the products list.
Line #6 - Only Administrators OR SuperAdmins can create new products.
Line #9 - Only SuperAdmin can delete resources.
Line #13 - Now, if there is a feature that can be accessed by users who are both Administrators as well as a SuperAdmin, this is how you would ideally achieve.

That’s almost covering everything in our requirement, right? But here is the catch, does this approach always work in practical scenarios?

Limitations of Role-Based Authorization

Imagine that we decided to add much more entities into our Stock Management Application like Customers, Orders, Suppliers, Companies. Now, let’s say that we need Admins for each department, like CustomerAdmins, OrderAdmins (you get the point). The list goes on and on as long as the application keeps scaling. As the Roles-Count increase, it becomes harder and harder to properly manage roles, right?

And imagine a scenario where you want to dynamically change the permission of a Role without touching the code. Quite not possible with Role-Based Authorization, yeah?

What about scenarios where you want to dynamically add/remove a particular role from existence? Wouldn’t this prove fatal since you have already hardcoded a role on to your controller?

What would you do if you wanted to implement a UI for User & Role Management?

These are the major limitations of Role-Based Authorization. We need a more flexible way to handle roles even though it keeps on increasing as well the make everything dynamic so that the super admin gets to change the permissions of each role on the go. This is where Permission-Based Authorization in ASP.NET Core comes in.

Permission-Based Authorization in ASP.NET Core

Let’s go with a mechanism that is finer than the Role-Based Authorization. We will introduce permissions at the role level so that only specific roles get to access protected resources. We will be using RoleClaims to set the permissions of roles.ASP.NET Core has some cool features to implement this easily.

Here a few advantages of taking this approach:

  • Roles are no longer static to the code. It can be easily modified/added/deleted at runtime.
  • The Permissions set to each role also can be modified easily at runtime.
  • More Control on Authorization.

What we will build? (Screenshots Included)

Let’s get to know what our final implementation will look like. Here are a few screenshots of the final product.

List of all Registered Users

In this view, you get to see a list of all registered users. Note that the currently logged in user won’t be listed here. On Clicking the Action Dropdown, the SuperAdmin can Manage Roles for the corresponding user.

permission-based-authorization-in-aspnet-core

Manage User Roles

From the User List, the Superadmin will be able to assign roles to each user. For example, in the below screenshot, we will able to assign any of the available roles to the selected user. PS, This view is available for the super admin only.

permission-based-authorization-in-aspnet-core

Add New Roles

In this view, the super admin will be able to add new roles as and when required. This is where Permission Management comes into play.

permission-based-authorization-in-aspnet-core

Each of the Roles has Permissions assigned to itself. For instance, let’s select ‘Manage Permissions’ of the Admin Role. This would open up the permission management UI.

permission-based-authorization-in-aspnet-core

Manage Permissions - UI

This is where the magic happens. On this view, a super admin will be able to grant/revoke permissions to any roles available. This will be further drilled down to the users too. For instance, you can set the view, create, read, write, delete permissions of the product entity with ease, without having to touch the code. Pretty practical yeah? Let’s give the Role permissions to view and create products.

permission-based-authorization-in-aspnet-core

Now, let’s login as the previously modified user and open up the products page. You can see that only the Create and View Features are visible.

permission-based-authorization-in-aspnet-core

And this is what the super admin (with all permissions) would see on the product management page.

permission-based-authorization-in-aspnet-core

These are quite vital to any business out there. I believe such features should come out of the box with ASP.NET Core. One day. Hopefully.

However, let’s get started with the implementation without any further delay. You can find the complete source code here.

Getting Started with Permission-Based Authorization

So, the idea is simple. We will build a .NET 5 Web Application (MVC) with the default Identity package from Microsoft. To this project we will add features like :

  • User List - To display all the registered users
  • Roles List - To Display / Add Roles
  • User - Roles Management - To assign various roles to each user.
  • Default Seeding - Seed default roles and users on application startup
  • Permission Management - Role-based permissions controller
  • Dummy Products Management - To Create / Read / Modify Products (Note that this will not be implemented completely). The aim is to dynamically restrict users based on their roles from various actions on the Product Entity. For instance, only Admins can modify the data.

Let’s start by creating a new ASP.NET Core MVC Project / .NET 5 MVC (I am still worried about the naming convention Microsoft takes :P ). Note that we will be using the default Microsoft Identity in our project. Hence make sure that you select the Authentication Type as Individual accounts.

permission-based-authorization-in-aspnet-core

Create the project and wait for the scaffolding to get over.

Pre-Defined Roles

We would need some predefined roles to get started with. These roles will be available as soon as your application runs on the server. You guessed it right, we will be seeding some data to the database in a while as well. Let’s create a new folder Constants and add a new enum Roles.cs

public enum Roles
{
SuperAdmin,
Admin,
Basic
}

These are the default roles that we would try to seed to the database as soon as the application fires up.

Similarly, let’s define permissions based on features. To get started, we assume that our System has a Products Entity with features like Create, View, Edit, Delete. These are more like CRUD Permissions.

public static class Permissions
{
public static List<string> GeneratePermissionsForModule(string module)
{
return new List<string>()
{
$"Permissions.{module}.Create",
$"Permissions.{module}.View",
$"Permissions.{module}.Edit",
$"Permissions.{module}.Delete",
};
}
public static class Products
{
public const string View = "Permissions.Products.View";
public const string Create = "Permissions.Products.Create";
public const string Edit = "Permissions.Products.Edit";
public const string Delete = "Permissions.Products.Delete";
}
}

Line #3 to #12 is a simple function that returns a list of permissions based on the string passed. We will be using this later for seeding permissions for the SuperAdmin. Remember the SuperAdmin would ideally be granted all the permissions.

Seeding Users and Roles

As we have defined the default users, roles, and a couple of permissions, let’s ensure that these data get into our database as soon as the application starts up. This process, in other words, is called Data Seeding. Create a New Folder named Seeds and add in a new class, DefaultRoles.cs

public static class DefaultRoles
{
public static async Task SeedAsync(UserManager<IdentityUser> userManager, RoleManager<IdentityRole> roleManager)
{
await roleManager.CreateAsync(new IdentityRole(Roles.SuperAdmin.ToString()));
await roleManager.CreateAsync(new IdentityRole(Roles.Admin.ToString()));
await roleManager.CreateAsync(new IdentityRole(Roles.Basic.ToString()));
}
}

The above snippet will create Default Roles into our application via the RoleManager Helper class of Identity.

Similarly, let’s add a mechanism to seed default users. For this demonstration, let’s seed 2 users - SuperAdmin and a Basic User.

public static class DefaultUsers
{
public static async Task SeedBasicUserAsync(UserManager<IdentityUser> userManager, RoleManager<IdentityRole> roleManager)
{
var defaultUser = new IdentityUser
{
UserName = "[email protected]",
Email = "[email protected]",
EmailConfirmed = true
};
if (userManager.Users.All(u => u.Id != defaultUser.Id))
{
var user = await userManager.FindByEmailAsync(defaultUser.Email);
if (user == null)
{
await userManager.CreateAsync(defaultUser, "123Pa$$word!");
await userManager.AddToRoleAsync(defaultUser, Roles.Basic.ToString());
}
}
}
public static async Task SeedSuperAdminAsync(UserManager<IdentityUser> userManager, RoleManager<IdentityRole> roleManager)
{
var defaultUser = new IdentityUser
{
UserName = "[email protected]",
Email = "[email protected]",
EmailConfirmed = true
};
if (userManager.Users.All(u => u.Id != defaultUser.Id))
{
var user = await userManager.FindByEmailAsync(defaultUser.Email);
if (user == null)
{
await userManager.CreateAsync(defaultUser, "123Pa$$word!");
await userManager.AddToRoleAsync(defaultUser, Roles.Basic.ToString());
await userManager.AddToRoleAsync(defaultUser, Roles.Admin.ToString());
await userManager.AddToRoleAsync(defaultUser, Roles.SuperAdmin.ToString());
}
await roleManager.SeedClaimsForSuperAdmin();
}
}
private async static Task SeedClaimsForSuperAdmin(this RoleManager<IdentityRole> roleManager)
{
var adminRole = await roleManager.FindByNameAsync("SuperAdmin");
await roleManager.AddPermissionClaim(adminRole, "Products");
}
public static async Task AddPermissionClaim(this RoleManager<IdentityRole> roleManager, IdentityRole role, string module)
{
var allClaims = await roleManager.GetClaimsAsync(role);
var allPermissions = Permissions.GeneratePermissionsForModule(module);
foreach (var permission in allPermissions)
{
if (!allClaims.Any(a => a.Type == "Permission" && a.Value == permission))
{
await roleManager.AddClaimAsync(role, new Claim("Permission", permission));
}
}
}
}

Line #3 - #20: Creates a User with Basic Role. You can see that we are setting the email and username of this particular user in the code. Once the user is created/seeded, we add the user to the Basic Role.
Line #21 - #41: Similarly, we create another user and add it to Basic, Admin, and SuperAdmin Roles. Basically, this user is granted all roles.

Line #39: Here we add claims to the SuperAdmin user. The idea is that this super admin should have all permissions that exist in our system. Remember defining Product Permissions? We are going to build this seed in such a way that the SuperAdmin gets all of the product permissions.

Line #44: Get’s the role by name.
Line #49: Get all the existing claims that are already existing for the role.
Line #50: Here, we pass a string parameter “Product” to the GeneratePermissionsForModule method which will return a list of permissions for the Product Module (Create, Read, View, Delete, Modify). Remember we were working with this earlier?

Line #51 - #58: We loop through all the generated permissions, check if it doesn’t already exist within the role, and finally add it as a new claim to the specific, which in our case is ‘superadmin’.

Adding Claims to Roles - Seed Default Claims / Permissions for SuperAdmin

As we are done with creating our seed classes, let’s now hook it up with our application so that seed classes actually get executed when the application is fired. Program.cs / Main is the usual choice for placing the Seeding process as it’s the entry point of our application.

Open up Program.cs and replace the Main Method with the following code snippet.

public async static Task Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger("app");
try
{
var userManager = services.GetRequiredService<UserManager<IdentityUser>>();
var roleManager = services.GetRequiredService<RoleManager<IdentityRole>>();
await Seeds.DefaultRoles.SeedAsync(userManager, roleManager);
await Seeds.DefaultUsers.SeedBasicUserAsync(userManager, roleManager);
await Seeds.DefaultUsers.SeedSuperAdminAsync(userManager, roleManager);
logger.LogInformation("Finished Seeding Default Data");
logger.LogInformation("Application Starting");
}
catch (Exception ex)
{
logger.LogWarning(ex, "An error occurred seeding the DB");
}
}
host.Run();
}

Line #13 - #15 is where we invoke the methods of the Seed classes that are responsible to write the default users/roles to the database. This is a pretty straightforward piece of code.

Displaying a List of Registered Users

With that out of the way, let’s get started with building the UI. We will start with creating a view for displaying all the registered users, except the currently logged in user. Note that this view is accessible only for logged-in users.

Create a New MVC Controller, UsersController. Make sure to include the [Authorize] decorator moving forward.

[Authorize]
public class UsersController : Controller
{
private readonly UserManager<IdentityUser> _userManager;
public UsersController(UserManager<IdentityUser> userManager)
{
_userManager = userManager;
}
public async Task<IActionResult> Index()
{
var currentUser = await _userManager.GetUserAsync(HttpContext.User);
var allUsersExceptCurrentUser = await _userManager.Users.Where(a => a.Id != currentUser.Id).ToListAsync();
return View(allUsersExceptCurrentUser);
}
}

In this controller, we will have an Index Action Method which returns a list of users except for the current user. Nothing complicated. As the next step, let’s add in the view for the Index Method. This would be a view that takes in a list of users and displays the data in table format.

BEGINNER TIP: You can add a Corresponding View for any Action Method by right-clicking on the Method Name itself, and clicking on Add New View. Similarly, you can navigate to the created View easily by right-clicking the Method name and selecting ‘Go-To View’.

@using Microsoft.AspNetCore.Identity;
@model IEnumerable<IdentityUser>
<h1>User List</h1>
<br />
<table class="table table-striped" id="userTable">
<thead>
<tr>
<th>
User
</th>
<th>
Email
</th>
<th style="width:10%">
Actions
</th>
</tr>
</thead>
<tbody>
@foreach (var user in Model)
{
<tr>
<td>
<div class="row">
<div class="col-sm">
<a>
@user.UserName
</a>
<br>
<small>
@user.Id
</small>
</div>
</div>
</td>
<td>
<a>
@user.Email
</a>
</td>
<td>
<div class="btn-group">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Action
</button>
<div class="dropdown-menu shadow animated--grow-in">
<a style='margin-right:16px' asp-controller="userroles" asp-action="Index" asp-route-userId="@user.Id" class="dropdown-item">
<i class="fas fa-wrench"></i> Manage Roles
</a>
</div>
</div>
</td>
</tr>
}
</tbody>
</table>

Role Management UI

Next, we will be building a Role management UI that is accessible only by the Superadmin. The view would help us to list all the roles available as well as add new roles to the system. Let’s create a new controller, RolesController.

[Authorize(Roles ="SuperAdmin")]
public class RolesController : Controller
{
private readonly RoleManager<IdentityRole> _roleManager;
public RolesController(RoleManager<IdentityRole> roleManager)
{
_roleManager = roleManager;
}
public async Task<IActionResult> Index()
{
var roles = await _roleManager.Roles.ToListAsync();
return View(roles);
}
[HttpPost]
public async Task<IActionResult> AddRole(string roleName)
{
if (roleName != null)
{
await _roleManager.CreateAsync(new IdentityRole(roleName.Trim()));
}
return RedirectToAction("Index");
}
}

Line #1: Only SuperAdmins can access this controller.
Line #10 - #14: The index method returns a list of Roles back to the view.
Line #15 - #23: This is a POST method that facilitates the super admins to add new roles.

After that, let’s add a View for the Index Method of RolesController. This is also quite a simple UI with a Table and a provision to add new roles (textbox and a button).

@using Microsoft.AspNetCore.Identity;
@model IEnumerable<IdentityRole>
<h1>Roles</h1>
<br />
<form method="post" asp-action="addrole" asp-controller="roles">
<div class="input-group">
<input name="roleName" class="form-control w-25">
<span class="input-group-btn">
<button class="btn btn-info">Add New Role</button>
</span>
</div>
</form>
<table class="table table-striped" id="roleTable">
<thead>
<tr>
<th>
Role
</th>
<th>
Id
</th>
<th style="width:10%">
Actions
</th>
</tr>
</thead>
<tbody>
@foreach (var role in Model)
{
<tr>
<td>
@role.Name
</td>
<td>
@role.Id
</td>
<td class="text-right">
<div class="btn-group">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Action
</button>
<div class="dropdown-menu shadow animated--grow-in">
<a class="dropdown-item">
<i class="fas fa-pencil-alt"></i> Edit
</a>
<form class="d-inline">
<a class="dropdown-item"><i class="fas fa-trash-alt"></i> Delete</a>
</form>
@if (role.Name != "SuperAdmin")
{
<a style='margin-right:16px' asp-controller="Permission" asp-action="Index" asp-route-roleId="@role.Id" class="dropdown-item">
<i class="fas fa-wrench"></i> Manage Permissions
</a>
}
</div>
</div>
</td>
</tr>
}
</tbody>
</table>

Since we have implemented UIs for both Users and Roles, let’s add a feature where the superadmin can assign users to roles. Next-Up, let’s get started with the User-Role management.

Under the Models folder, add a new Class, UserRolesViewModel. This will be the Model (DTO) we send to the User-Role Management UI. It’s a simple structure that will hold the userId and a List of Roles assigned to the user.

public class ManageUserRolesViewModel
{
public string UserId { get; set; }
public IList<UserRolesViewModel> UserRoles { get; set; }
}
public class UserRolesViewModel
{
public string RoleName { get; set; }
public bool Selected { get; set; }
}

With that out of the ways, let’s create a new controller, UserRolesController which again is accessible only by the SuperAdmins. This Controller will have 2 major methods - List All the Roles Available (this will be a checked list denoting the active roles as well for the user), and an update method to add the selected roles to the user.

Here is how it would look like:

permission-based-authorization-in-aspnet-core

Get the idea? Let’s see what the Controller would contain.

[Authorize(Roles = "SuperAdmin")]
public class UserRolesController : Controller
{
private readonly SignInManager<IdentityUser> _signInManager;
private readonly UserManager<IdentityUser> _userManager;
private readonly RoleManager<IdentityRole> _roleManager;
public UserRolesController(UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager, RoleManager<IdentityRole> roleManager)
{
_userManager = userManager;
_signInManager = signInManager;
_roleManager = roleManager;
}
public async Task<IActionResult> Index(string userId)
{
var viewModel = new List<UserRolesViewModel>();
var user = await _userManager.FindByIdAsync(userId);
foreach (var role in _roleManager.Roles.ToList())
{
var userRolesViewModel = new UserRolesViewModel
{
RoleName = role.Name
};
if (await _userManager.IsInRoleAsync(user, role.Name))
{
userRolesViewModel.Selected = true;
}
else
{
userRolesViewModel.Selected = false;
}
viewModel.Add(userRolesViewModel);
}
var model = new ManageUserRolesViewModel()
{
UserId = userId,
UserRoles = viewModel
};
return View(model);
}
public async Task<IActionResult> Update(string id, ManageUserRolesViewModel model)
{
var user = await _userManager.FindByIdAsync(id);
var roles = await _userManager.GetRolesAsync(user);
var result = await _userManager.RemoveFromRolesAsync(user, roles);
result = await _userManager.AddToRolesAsync(user, model.UserRoles.Where(x => x.Selected).Select(y => y.RoleName));
var currentUser = await _userManager.GetUserAsync(User);
await _signInManager.RefreshSignInAsync(currentUser);
await Seeds.DefaultUsers.SeedSuperAdminAsync(_userManager, _roleManager);
return RedirectToAction("Index", new { userId = id });
}
}

Line #13 - #40: Here, we cycle through each of the available roles and check if the selected user is already in the corresponding role. If the user is not in the iterated role, we change the status of the Selected boolean to false. We would go on later in the View to uncheck the roles in list based on this algorithm. Finally, we return the model to the View.

Line #40 - #50: This is another crucial method to update the user-role linkage. To make the entire process cleaner, we first remove all the existing roles from the user. And finally, we re-add the roles which are selected in UI when the user clicks Save.
Line #48: This is just a fallback code in case one admin tries to change the roles of another. You get the point. You are always free to modify these logics as your organization requires.

As usual, let’s create a UI for the User-Role management. This will also be for the Index Action Method.

@model PermissionManagement.MVC.Models.ManageUserRolesViewModel
<h3>Manage Roles for @Model.UserId</h3>
<br />
<div class="card">
<div id="viewAll" class="card-body table-responsive">
<form asp-controller="userroles" method="post" asp-action="update" asp-route-id="@Model.UserId" class="d-inline">
<input asp-for="@Model.UserId" type="hidden" />
<table class="table table-striped" id="userRolesTable">
<thead>
<tr>
<th>
Role
</th>
<th>
Status
</th>
</tr>
</thead>
<tbody>
@for (int i = 0; i < Model.UserRoles.Count(); i++)
{
<tr>
<td>
<input asp-for="@Model.UserRoles[i].RoleName" type="hidden" />
@Model.UserRoles[i].RoleName
</td>
<td>
<div class="form-check m-1">
<input asp-for="@Model.UserRoles[i].Selected" class="form-check-input" />
</div>
</td>
</tr>
}
</tbody>
</table>
<div class="col-sm-12" style=" padding: 20px 20px 0px 0px">
<button type="submit" id="save" class="btn bg-primary">
<i class="fa fa-check"></i>
Save
</button>
</div>
</form>
</div>
</div>

Roles-Permission Management UI

In the previous sections, we learnt how to create a Management UI for User-Roles. Now, let’s create one for Roles-Permissions/Claims. Before that, let’s add a new Model to hold the Permission and Roles Data. We will name it PermissionViewModel.cs. This is almost the same approach that we had used for the previous Management UI.

public class PermissionViewModel
{
public string RoleId { get; set; }
public IList<RoleClaimsViewModel> RoleClaims { get; set; }
}
public class RoleClaimsViewModel
{
public string Type { get; set; }
public string Value { get; set; }
public bool Selected { get; set; }
}

After that, let’s add a new Controller, PermissionController.

[Authorize(Roles = "SuperAdmin")]
public class PermissionController : Controller
{
private readonly RoleManager<IdentityRole> _roleManager;
public PermissionController(RoleManager<IdentityRole> roleManager)
{
_roleManager = roleManager;
}
public async Task<ActionResult> Index(string roleId)
{
var model = new PermissionViewModel();
var allPermissions = new List<RoleClaimsViewModel>();
allPermissions.GetPermissions(typeof(Permissions.Products), roleId);
var role = await _roleManager.FindByIdAsync(roleId);
model.RoleId = roleId;
var claims = await _roleManager.GetClaimsAsync(role);
var allClaimValues = allPermissions.Select(a => a.Value).ToList();
var roleClaimValues = claims.Select(a => a.Value).ToList();
var authorizedClaims = allClaimValues.Intersect(roleClaimValues).ToList();
foreach (var permission in allPermissions)
{
if (authorizedClaims.Any(a => a == permission.Value))
{
permission.Selected = true;
}
}
model.RoleClaims = allPermissions;
return View(model);
}
public async Task<IActionResult> Update(PermissionViewModel model)
{
var role = await _roleManager.FindByIdAsync(model.RoleId);
var claims = await _roleManager.GetClaimsAsync(role);
foreach (var claim in claims)
{
await _roleManager.RemoveClaimAsync(role, claim);
}
var selectedClaims = model.RoleClaims.Where(a => a.Selected).ToList();
foreach (var claim in selectedClaims)
{
await _roleManager.AddPermissionClaim(role, claim.Value);
}
return RedirectToAction("Index", new { roleId = model.RoleId });
}
}

Line #10 - #30: The index method returns a list of DTO models that contains the details on which Permissions are active for the corresponding UserRole.
Line #14: Note that we are adding the Product Permissions List to the UI Model. Every time you add a new Entity like Brands / Customer, you will have to add those permissions to the UI this way.

Line #31 - #44: Once the SuperAdmin Maps new Permission to a selected user and click the Save Button, the enabled permissions are added to the Role.

Line #42 & #14: Please note that we will be adding these extension (‘AddPermissionClaim’ and ‘GetPermissions’) later in this tutorial. Do not worry if the reference is not resolved. Just follow along. ;)

Finally, Let’s add a straightforward UI to display the Role-Permissions Mappings and a feature to enable/disable Permissions (only by the superadmin).

@model PermissionManagement.MVC.Models.PermissionViewModel
<h1>Permissions</h1>
<br />
<div class="card">
<div id="viewAll" class="card-body table-responsive">
<form asp-controller="permission" method="post" asp-action="Update" class="d-inline">
<input asp-for="@Model.RoleId" type="hidden" />
<table class="table table-striped" id="permissionTable">
<thead>
<tr>
<th>
Type
</th>
<th>
Permission
</th>
<th>
Status
</th>
</tr>
</thead>
<tbody>
@for (int i = 0; i < Model.RoleClaims.Count(); i++)
{
<tr>
<td>
<input asp-for="@Model.RoleClaims[i].Type" type="hidden" />
@Model.RoleClaims[i].Type
</td>
<td>
<input asp-for="@Model.RoleClaims[i].Value" type="hidden" />
@Model.RoleClaims[i].Value
</td>
<td>
<div class="form-check m-1">
<input asp-for="@Model.RoleClaims[i].Selected" class="form-check-input" />
</div>
</td>
</tr>
}
</tbody>
</table>
<div class="col-sm-12" style="padding: 20px 20px 0px 0px">
<button type="submit" id="save" class="btn btn-success">
<i class="fa fa-check"></i>
Save
</button>
</div>
</form>
</div>
</div>

Line #23 - #40: A for loop to iterate through the available permissions and set the current status of the permission in relation to the user role.

Claims Helper

As mentioned in the previous section, we have not implemented AddPermissionClaim and GetPermissions yet. Let’s create a new folder, Helpers, and add a new class, ClaimsHelper.cs

public static class ClaimsHelper
{
public static void GetPermissions(this List<RoleClaimsViewModel> allPermissions, Type policy, string roleId)
{
FieldInfo[] fields = policy.GetFields(BindingFlags.Static | BindingFlags.Public);
foreach (FieldInfo fi in fields)
{
allPermissions.Add(new RoleClaimsViewModel { Value = fi.GetValue(null).ToString(), Type = "Permissions" });
}
}
public static async Task AddPermissionClaim(this RoleManager<IdentityRole> roleManager, IdentityRole role, string permission)
{
var allClaims = await roleManager.GetClaimsAsync(role);
if (!allClaims.Any(a => a.Type == "Permission" && a.Value == permission))
{
await roleManager.AddClaimAsync(role, new Claim("Permission", permission));
}
}
}

GetPermissions Extension method takes in a list of available permissions, the type of permission (for instance ProductPermissions) to be added, and Role Id. It then adds all the properties mentioned in the ProductPermissions using Reflection. This is a simple helper class that can be further optimized.

AddPermissionClaim: Remember that we have already used this while seeding permissions for the SuperAdmin Role? I am just duplicating it here for better understanding. Putting the code into words, this extension method is responsible for adding the selected claims from the UI to the user role.

Permission Requirement

Next, Let’s create a class that will essentially help to evaluate permission. Create a New Folder, Permission, and add a new class PermissionRequirement.cs

internal class PermissionRequirement : IAuthorizationRequirement
{
public string Permission { get; private set; }
public PermissionRequirement(string permission)
{
Permission = permission;
}
}

Authorization Handler

Second, we need an authorization handler that verifies if a user has the needed permission to access the resource. Create another class and name it PermissionAuthorizationHandler.cs

internal class PermissionAuthorizationHandler : AuthorizationHandler<PermissionRequirement>
{
public PermissionAuthorizationHandler(){}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
{
if (context.User == null)
{
return;
}
var permissionss = context.User.Claims.Where(x => x.Type == "Permission" &&
x.Value == requirement.Permission &&
x.Issuer == "LOCAL AUTHORITY");
if (permissionss.Any())
{
context.Succeed(requirement);
return;
}
}
}

Line #10: Gets all the Claims of the User of Type ‘Permission’ and checks if anyone matches the required permission.
Line #13: If there is a match, the user is allowed to access the protected resource. Else, the user will be presented with an Access Denied Page. Pretty Simple yeah?

Permission Policy Provider

This is a smarter way to add policies within the system. As and when the number of permissions keeps on increasing, we do not have to add each of these permissions into our policies. Rather, here is a dynamic way to achieve the same.

Create another class, PermissionPolicyProvider.cs

internal class PermissionPolicyProvider : IAuthorizationPolicyProvider
{
public DefaultAuthorizationPolicyProvider FallbackPolicyProvider { get; }
public PermissionPolicyProvider(IOptions<AuthorizationOptions> options)
{
FallbackPolicyProvider = new DefaultAuthorizationPolicyProvider(options);
}
public Task<AuthorizationPolicy> GetDefaultPolicyAsync() => FallbackPolicyProvider.GetDefaultPolicyAsync();
public Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
{
if (policyName.StartsWith("Permission", StringComparison.OrdinalIgnoreCase))
{
var policy = new AuthorizationPolicyBuilder();
policy.AddRequirements(new PermissionRequirement(policyName));
return Task.FromResult(policy.Build());
}
return FallbackPolicyProvider.GetPolicyAsync(policyName);
}
public Task<AuthorizationPolicy> GetFallbackPolicyAsync() => FallbackPolicyProvider.GetDefaultPolicyAsync();
}

Line #11 - #16: here is where we dynamically add the incoming permission to the PolicyBuilder.

This method adds the permission to the AuthorizationPolicyBuilder and returns the policy.

Registering the Service

As the final step of the entire process, let’s start registering our services into the ASP.NET Core Container. Open up the Startup.cs/ConfigureServices method and add in the following. Line #3 and #4 are related to the Permission classes we created in the previous sections.

public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>();
services.AddScoped<IAuthorizationHandler, PermissionAuthorizationHandler>();
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultUI()
.AddDefaultTokenProviders();
services.AddControllersWithViews();
}

Product Controller

Now, as a demonstration, let me create a Product Controller that protects the CRUD Operations and display the features only to the roles it is meant for. Create a new Controller, ProductController. The idea is going to be simple, a view with 4 Buttons namely Create, Read, Edit, Delete. To keep things minimal, we are not going to implement any CRUD operations here. Rather it will be just those buttons to verify that our implementation works.

For an Advanced CRUD Implementation in Razor Pages using jQuery, please refer to this article - Razor Page CRUD in ASP.NET Core with jQuery AJAX – Ultimate Guide

public class ProductController: Controller
{
public IActionResult Index()
{
return View();
}
}

Next, let’s add a View for the Product’s Index method.

@using Microsoft.AspNetCore.Authorization
@inject IAuthorizationService AuthorizationService
@using PermissionManagement.MVC.Constants
<h1>Product Management</h1>
<br />
@if ((AuthorizationService.AuthorizeAsync(User, Permissions.Products.Create)).Result.Succeeded)
{
<button class="btn btn-success">Create</button>
}
@if ((AuthorizationService.AuthorizeAsync(User, Permissions.Products.View)).Result.Succeeded)
{
<button class="btn btn-info">View</button>
}
@if ((AuthorizationService.AuthorizeAsync(User, Permissions.Products.Edit)).Result.Succeeded)
{
<button class="btn btn-warning">Modify</button>
}
@if ((AuthorizationService.AuthorizeAsync(User, Permissions.Products.Delete)).Result.Succeeded)
{
<button class="btn btn-danger">Delete</button>
}

Here we will use the IAuthorizationService that comes with ASP.NET Core as part of Authorization. This is quite helpful when you want to control the Authorization at the View level. Yes, you can do a similar task by restricting at the Controller level using the [Authorize(policyName)] decorator, but getting this level of detailed auth control is pretty cool, yeah?

AuthorizationService.AuthorizeAsync(User, Permissions.Products.Edit) - This is an async method that checks if the user role of the current user has the permission to Edit Products. Makes sense?

Let’s run the application now, with SuperAdmin credentials. Navigate to /products. As excepted, the superadmin has access to all the features of the Product module, thanks to seeding. I have showcased the other screenshots earlier in this article in the What we will build? (Screenshots Included) section.

permission-based-authorization-in-aspnet-core

Let’s wrap up the article.

Summary

In this article, we have implemented a complete Permission-Based Authorization in ASP.NET Core using .NET 5 and Microsoft Identity package. We built the entire system from scratch to control the level of authorization on the basis of User Roles. The SuperAdmin will be able to control the permission and add new roles as well. This is a neat way to secure your ASP.NET Core Web Applications from users with limited permissions. You can find the entire source code of the implementation here.

Leave behind your valuable queries, suggestions in the comment section below. Also, if you think that you learned something new from this article, do not forget to share this within your developer community. Happy Coding!

Source Code ✌
Grab the source code of the entire implementation by clicking here. Do Follow me on GitHub .
Support ❀
If you have enjoyed my content and code, do support me by buying a couple of coffees. This will enable me to dedicate more time to research and create new content. Cheers!
Share this Article
Share this article with your network to help others!
What's your Feedback?
Do let me know your thoughts around this article.

Boost your .NET Skills

I am starting a .NET 8 Zero to Hero Series soon! Join the waitlist.

Join Now

No spam ever, we are care about the protection of your data. Read our Privacy Policy