Implementing a Custom Resource-based Authorization In Asp.Net MVC



In this article I am going to walk you through how to implement a custom authentication and a custom resource based authentication.


Introduction

Resource-based authorization allows you to attach resources such as view, button etc to a role or group, and in turn users that has the particular role or group will automatically have access to the attached resource(s) .

The Concept

In this tutorial, I will be taking ASP.Net MVC ActionResult  as resources. Since I will be building a custom authentication and authorization, I will make use of membership provider class which has methods that check the user credentials (username & password) and role provider class that is has methods that get returns logged-in user roles.

Resources - (views) will be attached to roles and then roles in turn will be assigned to users. Users will only have access to those views that was attached to those roles that user belong to. Unauthorized users will be redirected to Unauthorized page even if they navigated via Url in the browser .


Creating the Project

Let's fire open our visual studio and create a new ASP.Net MVC web application project named AuthApp and change the Authentication to No Authentication like so:




Next, right click on the model's folder and create a class name CustomMembership, let it inherit from MembershipProvider class like so:
using System;
using System.Web.Security;

namespace AuthApp.Models
{
    public class CustomMembership: MembershipProvider
    {
        public override string ApplicationName
        {
            get
            {
                throw new NotImplementedException();
            }
            set
            {
                throw new NotImplementedException();
            }
        }

        public override bool ChangePassword(string username, string oldPassword, string newPassword)
        {
            throw new NotImplementedException();
        }

        public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer)
        {
            throw new NotImplementedException();
        }

        public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
        {
            throw new NotImplementedException();
        }

        public override bool DeleteUser(string username, bool deleteAllRelatedData)
        {
            throw new NotImplementedException();
        }

        public override bool EnablePasswordReset
        {
            get { throw new NotImplementedException(); }
        }

        public override bool EnablePasswordRetrieval
        {
            get { throw new NotImplementedException(); }
        }

        public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            throw new NotImplementedException();
        }

        public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            throw new NotImplementedException();
        }

        public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
        {
            throw new NotImplementedException();
        }

        public override int GetNumberOfUsersOnline()
        {
            throw new NotImplementedException();
        }

        public override string GetPassword(string username, string answer)
        {
            throw new NotImplementedException();
        }

        public override MembershipUser GetUser(string username, bool userIsOnline)
        {
            throw new NotImplementedException();
        }

        public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
        {
            throw new NotImplementedException();
        }

        public override string GetUserNameByEmail(string email)
        {
            throw new NotImplementedException();
        }

        public override int MaxInvalidPasswordAttempts
        {
            get { throw new NotImplementedException(); }
        }

        public override int MinRequiredNonAlphanumericCharacters
        {
            get { throw new NotImplementedException(); }
        }

        public override int MinRequiredPasswordLength
        {
            get { throw new NotImplementedException(); }
        }

        public override int PasswordAttemptWindow
        {
            get { throw new NotImplementedException(); }
        }

        public override MembershipPasswordFormat PasswordFormat
        {
            get { throw new NotImplementedException(); }
        }

        public override string PasswordStrengthRegularExpression
        {
            get { throw new NotImplementedException(); }
        }

        public override bool RequiresQuestionAndAnswer
        {
            get { throw new NotImplementedException(); }
        }

        public override bool RequiresUniqueEmail
        {
            get { throw new NotImplementedException(); }
        }

        public override string ResetPassword(string username, string answer)
        {
            throw new NotImplementedException();
        }

        public override bool UnlockUser(string userName)
        {
            throw new NotImplementedException();
        }

        public override void UpdateUser(MembershipUser user)
        {
            throw new NotImplementedException();
        }

        public override bool ValidateUser(string username, string password)
        {
            return true;
           
        }
    }
}



The next important class is the role provider class, in the model's folder create a class named CustomRole that inherit from RoleProvider class like so:
using System;
using System.Web.Security;

namespace AuthApp.Models
{
    public class CustomRole:RoleProvider
    {
        public override void AddUsersToRoles(string[] usernames, string[] roleNames)
        {
            throw new NotImplementedException();
        }

        public override string ApplicationName
        {
            get
            {
                throw new NotImplementedException();
            }
            set
            {
                throw new NotImplementedException();
            }
        }

        public override void CreateRole(string roleName)
        {
            throw new NotImplementedException();
        }

        public override bool DeleteRole(string roleName, bool throwOnPopulatedRole)
        {
            throw new NotImplementedException();
        }

        public override string[] FindUsersInRole(string roleName, string usernameToMatch)
        {
            throw new NotImplementedException();
        }

        public override string[] GetAllRoles()
        {
            throw new NotImplementedException();
        }

        public override string[] GetRolesForUser(string username)
        {
            throw new NotImplementedException();

        }

        public override string[] GetUsersInRole(string roleName)
        {
            throw new NotImplementedException();
        }

        public override bool IsUserInRole(string username, string roleName)
        {
            return true;
        }

        public override void RemoveUsersFromRoles(string[] usernames, string[] roleNames)
        {
            throw new NotImplementedException();
        }

        public override bool RoleExists(string roleName)
        {
            return true;
        }
    }
}


Web Config Modification

In order to make use of the custom membership and custom role in our application we need to add it to our web config. First, add these two lines to the appSettings section like so:



Within the system.web section add these lines of code:

      
 
    
      
        
        
      

    
    
      
        
        

      
    



Application Entities 

Create a folder named Entities in the root of the web application. We add classes that represent all the tables needed for our application.

ApplcationUser

This table will be used for storing user information. Add a class within the Entities folder named ApplicationUser like so:
namespace AuthApp.Entities
{
    public class ApplicationUser
    {
        public int ApplicationUserId { get; set; }
        [Required]
        [StringLength(50)]
        public string Username { get; set; }
        [Required]
        [StringLength(50)]
        public string Password { get; set; }
    }
}

Role

This table will act as datastore for various roles in the application. The structure is very simple:
 public class Role
    {
        public int RoleId { get; set; }
        [Required]
        [StringLength(50)]
        public string Name { get; set; }
    }



UserRole

The UserRole table will house userid and associated roles. Since a user can belong to multiple roles, it is better to have this table like this:
 public class UserRole
    {
        public int UserRoleId { get; set; }
        public int UserId { get; set; }
        public int RoleId { get; set; }
        public virtual Role Role { get; set; }
        public virtual ApplicationUser User { get; set; }
    }


Resources

The Resources table will be used to store the application resources - in this case the views. The structure is also fairly simple
public class Resource
    {
        public int ResourceId { get; set; }
        [Required]
        [StringLength(50)]
        public string Controller { get; set; }
        [Required]
        [StringLength(50)]
        public string Action { get; set; }
        [StringLength(50)]
        public string Area { get; set; }
    }

RoleResource

The RoleResource table will hold the roles and associated resources. Since multiple resources can be assigned to a role hence this structure:
public class RoleResource
    {
        public int RoleResourceId { get; set; }
        public int RoleId { get; set; }
        public int ResourceId { get; set; }
        public virtual Resource Resource { get; set; }
        public virtual Role Role { get; set; }
    }

Resource-Based Implementation

I will not be writing about how authentication is done, although you will find it in the sample app in my github repository.

We will implement this by using ASP.Net MVC ActionFilterAttribute. Action Filters are attributes which inherit from the ActionFilterAttribute class, and can execute either before, during or after an action execution.

The first thing we need to do is to create a class that inherit from ActionFilterAttribute, then fetch all controllers, actions and areas that a logged in user have access to and then check it against the current executing controller, action and area. Deny or allow user before any action is executed depend on what actionresult permission.

The ActionFilter Class

Create a folder named Infrastructure in the root of the project and add a class named FilterResourceAttribute then override the OnActionExecuting method like so:
 public class FilterResourceAttribute: ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            base.OnActionExecuting(filterContext);
        }
    }

The first thing we need to do here is to detect the name of the controller, action and area names that is been requested. To do this let's add this to our class:
 var areaName = string.Empty;
 var routeData = filterContext.RouteData;
 areaName = routeData.DataTokens["area"] as string;
 var controllerName = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName;
 var actionName = filterContext.ActionDescriptor.ActionName;

 var requiredPermission = $"{controllerName}-{actionName}";
 if (!string.IsNullOrWhiteSpace(areaName)) //Area may be an empty string
     {
        requiredPermission += $"-{areaName}";
     }


The next thing is to check if the current user has permission to view the controller, action or area been accessed. That will be done like this:
 if (!srv.hasPermission(requiredPermission))
    {
      filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary {{ "action", "Unauthorized" }, { "controller", "Error" }, { "area", "" } });
    }

The code above checked if user does not has permission and send the user to an unthorized page if need be.
The full Actionfilter class should now look like this:
public class FilterResourceAttribute: ActionFilterAttribute
    {
        AuthorizationService srv = new AuthorizationService(new Domain.AuthAppContext());
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var areaName = string.Empty;
            var routeData = filterContext.RouteData;
            areaName = routeData.DataTokens["area"] as string;
            var controllerName = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName;
            var actionName = filterContext.ActionDescriptor.ActionName;

            var requiredPermission = $"{controllerName}-{actionName}";
            if (!string.IsNullOrWhiteSpace(areaName))
            {
                requiredPermission += $"-{areaName}";
            }

            if (!srv.hasPermission(requiredPermission))
            {
                filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary {{ "action", "Unauthorized" }, { "controller", "Error" }, { "area", "" } });
            }
            
            base.OnActionExecuting(filterContext);
        }
    }


AuthorizationService Class
 public class AuthorizationService
    {
        private AuthAppContext db;
        public AuthorizationService(AuthAppContext _db)
        {
            this.db = _db;
        }

        public bool hasPermission(string resource)
        {
            if (!HttpContext.Current.User.Identity.IsAuthenticated)
            {
                return false;
            }
            var userName = HttpContext.Current.User.Identity.Name;

            var loggedInUser = db.Users.Where(x => x.Username == userName).FirstOrDefault();

            List userpages = null;

            if (loggedInUser == null) return false;

            var userResource = (from rr in db.RoleResources
                                join ur in db.UserRoles on rr.RoleId equals ur.RoleId
                                where ur.UserId == loggedInUser.ApplicationUserId
                                select rr.ResourceId).ToList();

            if (userResource.Any())
            {


                userpages = (from m in db.Resources
                             where userResource.Contains(m.ResourceId)
                             select new RequiredPermissionVM
                             {
                                 controllerName = m.Controller,
                                 actionName = m.Action,
                                 areaName = m.Area
                             }).ToList();
            }

            if (!userpages.Any()) return false;

            return userpages.Any(x => x.requiredPermision.ToLower() == resource.ToLower());
        }
    }

Usage

To use the Fiter Attribute just decorate each controller with the attribute like so:
 [Authorize]
    [FilterResource]
    public class PagesController : Controller
    {
        // GET: Pages
        public ActionResult Index()
        {
            return View();
        }

        public ActionResult Portfolio()
        {
            return View();
        }
    }    


Now, when we run the app, the system will look to see which user we currently logged in, and if we try to access a page that that user didn't have access to, we will get kicked back out to the unauthorized page. As always, you can check out the sample project which contains both the login and seeded db on GitHub, and please let me know what you think on how to make this better.

Happy Coding!

No comments:

Powered by Blogger.