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.
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.
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.
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.
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.
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.
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.
And this is what the super admin (with all permissions) would see on the product management page.
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.
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
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.
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
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.
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.
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.
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â.
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.
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).
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.
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:
Get the idea? Letâs see what the Controller would contain.
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.
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.
After that, letâs add a new Controller, PermissionController.
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).
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
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
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
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
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.
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
Next, letâs add a View for the Productâs Index method.
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.
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!