ASP.NET Identity is currently the main framework used to add authentication and authorization capabilities to an ASP.NET site. After ASP.NET Identity is integrated with an ASP.NET project it creates a few database tables where relevant user data can be stored. Let’s look at some of these tables:
AspNetUsers – the table where application users are stored
AspNetRoles – this is where we store application roles (you can also think of them as groups)
AspNetUserRoles – a mapping table where we store information about what users belong to what roles
So far, so good. What we get out of the box is a way to create users, create some roles and assign users to roles. This follows the best-practice where we want to eventually assign application permissions to roles instead of individual users.
But how exactly do we handle application permissions given the functionality that ASP.NET Identity provides for us out of the box? Most developers and projects at this point follow the pattern that we’re so used to from other areas in IT – we check if a user belongs to a certain role and if so, we allow that particular operation to proceed. In ASP.NET this is done using the Authorize attribute (allow the operation to proceed if the current user belongs to a certain role):
[Authorize(Roles = "Admin")] public ActionResult AdministratorsOnly() { return View(); }
What is wrong with this approach? Let’s consider a few scenarios:
- Let’s say that you’re working on a somewhat complex site that has 20 roles. It could totally be the case then that you’d have certain functions where, let’s say, 15 of those roles would be allowed access … so that code would look something like this:
[Authorize(Roles = "Role1,Role2,Role3,Role4,Role5,Role6,Role7,Role8,Role9,Role10,Role11,Role12,Role13,Role14,Role15")] public ActionResult SpecialRolesOnly() { return View(); }
This is getting ugly and quite unmanageable.
- Imagine that your boss comes and requires a permission update – “Allow RoleX to access function A.” The only answer to that given the default implementation is “sure, but that requires a project recompilation and code push to production”. Do that a few times and management will want a better way to handle this aspect of the site.
- Consider this: an audit request comes down – “We need to know exactly what RoleZ can do in the application”. Good luck with that one – you’re now stuck running searches across the entire project code trying to see where that role might appear in an Authorize attribute.
I think you get the picture. The default approach using the Authorize attribute in ASP.NET has all sorts of problems. We’re basically asking the question “Is the user in this role” and based on the answer to that question we’re hardcoding behavior into our application code.
Let’s look at this problem from a different point of view. Instead of checking whether the user belongs to a role let’s ask this question instead – “does the current user have the particular permission that’s required to access this particular function“?
It’s pretty clear that the Authorize attribute won’t help us here anymore – we need another tool, another attribute. We need to customize the framework’s behavior and this is probably the main reason why most projects will just stick to the default behavior since it’s so much simpler (at least initially).
A possible attribute to give us what we need could look like this:
[HasPermission("PermissionA")] public ActionResult AdministratorsOnly() { return View(); }
… and the implementation for the HasPermission attribute might look like this:
public class HasPermissionAttribute : ActionFilterAttribute { private string _permission; public HasPermissionAttribute(string permission) { this._permission = permission; } public override void OnActionExecuting(ActionExecutingContext filterContext) { if (! CHECK_IF_USER_OR_ROLE_HAS_PERMISSION(_permission)) { // If this user does not have the required permission then redirect to login page var url = new UrlHelper(filterContext.RequestContext); var loginUrl = url.Content("/Account/Login"); filterContext.HttpContext.Response.Redirect(loginUrl, true); } } }
The function CHECK_IF_USER_OR_ROLE_HAS_PERMISSION is where we determine whether this user or role has the particular permission we’re looking for in order to allow that operation to proceed. What we do in that function is based on how we actually store our permissions data and how we tie that to application roles.
Consider for example the following database tables:
Permissions (Id, Description) – this table stores the list of all permissions we support in our application. The values from the ‘Description’ column are the values we’d use in our code as parameters to the HasPermission attribute (we could certainly check permissions by Id instead of Description but using the description can make the code more readable – we’d immediately know if we have the right permission in place for a given operation.
AspNetRolePermissions (RoleId, PermissionId) – a mapping table where we link roles to permissions. If a row exists in this table it would mean that the role has that particular permission – otherwise, that role does not have the permission.
With this table structure we now have enough information for CHECK_IF_USER_OR_ROLE_HAS_PERMISSION to determine if a user or role has a particular permission. We know the current user or role (we get that from ASP.NET Identity). We know the permission we’re checking for – that’s passed in to our custom attribute. If we find an entry in AspNetRolePermissions for that role/permission combination we allow the operation to proceed.
So what did we do here? We went from the default implementation where we answered the question “is the user in this role” (with the hardcoded answer basically driving our application permissions implementation) and we moved to a more flexible approach where we answer the question “does the user/role have this permission” where the permission data is stored in metadata tables outside of the actual application code.
Is this a better approach? Let’s consider again the original list of problems with the default approach:
- It does not matter how complex our application is or how many roles are involved – we only need to check whether the current user/role has the permission needed to perform that operation (we will not have a long list of comma-separated roles in our new attribute).
- Does management want to modify some permissions? No problem – we just need to update data in AspNetRolePermissions and the application will pick it up right away – no compilations or code releases needed for this. For bonus points – build an admin web interface around the new permission metadata tables and allow management to directly manage application permissions.
- Permission audits? No problem – we just need to check our permissions metadata tables to see what permissions a particular user or role has.
This particular article focused on ASP.NET Identity but a similar problem exists in previous ASP.NET authentication frameworks and probably other such similar frameworks in other languages. It’s pretty clear that what we get out of the box with such frameworks is not always ideal. Instead of hardcoding such critical data in the application code spend some time upfront to come up with a site architecture that allows privileged users to maintain this type of data without having to dig through application code – dynamic, flexible site architectures are a win-win for everybody.