Build a fully customized MembershipProvider and RoleProvider

The problem:

UsreId is widely used in all kind of web applications. The question is how you can pass the userId to different location in your web application. By using Session variable? Make something like Session["UID"]=blahblahblah? Oh come on, it is 21st century already and we need something new. Session variable is a cool choice; I use it a lot before I switch to the custom MembershipProvider and the RoleProvider.

The main drawback of Session is that it can be lost due to variety reasons. Once your Session is gone, everything stored in it is gone too.

Take a quick look on this line of code.

var userName = User.Identity.Name;

Is it possible to make something like  User.Identity.UserId ?

The answer is yes, and that is why we need our CustomMembershipProvider.

The Solution:

Step 1: Create the CustomMembershipProvider file

This file is calling the build in MembershipProvider.

In the Properties section, I manually inject the IUnitOfWork (I am using Ninject) so what we have the data access. I also inject a ICacheManager; you don’t have to have it.

 

    public class CustomMembershipProvider : MembershipProvider
    {
        #region Properties

        private int _cacheTimeoutInMinutes = 30;

        [Inject]
        public IUnitOfWork _unitOfWork { get; set; }

        [Inject]
        public ICacheManager _cacheManager { get; set; }

        #endregion

        #region Overrides of MembershipProvider

        public override void Initialize(string name, NameValueCollection config)
        {
            // Set Properties
            int val;
            if (!string.IsNullOrEmpty(config["cacheTimeoutInMinutes"]) && Int32.TryParse(config["cacheTimeoutInMinutes"], out val))
                _cacheTimeoutInMinutes = val;

            // Call base method
            base.Initialize(name, config);
        }

        public override bool ValidateUser(string username, string password)
        {
            if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
                return false;

            //using (var context = new MvcDemoEntities())
            //{
            //    var user = (from u in context.Users
            //                where String.Compare(u.Username, username, StringComparison.OrdinalIgnoreCase) == 0
            //                      && String.Compare(u.Password, password, StringComparison.OrdinalIgnoreCase) == 0
            //                      && !u.Deleted
            //                select u).FirstOrDefault();

            //return user != null;
            return true;
        }

        public override MembershipUser GetUser(string userEmail, bool userIsOnline)
        {
            var cacheKey = string.Format("UserData_{0}", userEmail);
            if (HttpRuntime.Cache[cacheKey] != null)
                return (CustomMembershipUser)HttpRuntime.Cache[cacheKey];

            var thisUser = _cacheManager.Get(cacheKey, () => _unitOfWork.Repository<HaoFancyUser>()
                .Query()
                .Filter(x => x.Email == userEmail)
                .Include(x => x.UserRole)
                .Include(x => x.UserGroup)
                .Get()
                .FirstOrDefault());

            if (thisUser == null)
                return null;

            var membershipUser = new CustomMembershipUser(thisUser);

            //Store in cache
            //HttpRuntime.Cache.Insert(cacheKey, membershipUser, null, DateTime.Now.AddMinutes(_cacheTimeoutInMinutes), Cache.NoSlidingExpiration);

            return membershipUser;
        }

        #endregion

        #region Overrides of MembershipProvider that throw 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 ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer)
        {
            throw new NotImplementedException();
        }

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

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

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

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

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

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

        public override bool DeleteUser(string username, bool deleteAllRelatedData)
        {
            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 MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            throw new NotImplementedException();
        }

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

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

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

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

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

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

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

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

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

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

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

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

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

        #endregion
    }

For those method we don’t want to implement, you can always throw new NotImplementedException().

I am not using the ValidateUser method at all, because it returns a Boolean which didn’t fulfill my requirement.

The GetUser method is easy to understand. Query your database by using the provided Email. I am using cache here. In this case, it will check to see if the cache has any stored user, if not, it will query the database. If there is something returned, then insert the returned object to the cache.

That is pretty much everything for our CustomMembershipProvider.

 

Step 2: Create the CustomRoleProvider

In my database design, the User table and the UserRole table has a One-to-Many relationship. It is  up to you to make it Many-to-Many.

The CustomRoleProvider is calling the building in RoleProvider.

    public class CustomRoleProvider : RoleProvider
    {
        #region Properties

        private int _cacheTimeoutInMinutes = 30;

        [Inject]
        public IUnitOfWork _unitOfWork { get; set; }

        [Inject]
        public ICacheManager _cacheManager { get; set; }

        #endregion

        #region Overrides of RoleProvider

        public override void Initialize(string name, NameValueCollection config)
        {
            // Set Properties
            int val;
            if (!string.IsNullOrEmpty(config["cacheTimeoutInMinutes"]) && Int32.TryParse(config["cacheTimeoutInMinutes"], out val))
                _cacheTimeoutInMinutes = val;

            // Call base method
            base.Initialize(name, config);
        }

        public override bool IsUserInRole(string username, string roleName)
        {
            var userRoles = GetRolesForUser(username);
            return userRoles.Contains(roleName);
        }

        public override string[] GetRolesForUser(string userEmail)
        {
            //Return if the user is not authenticated
            if (!HttpContext.Current.User.Identity.IsAuthenticated)
                return null;

            //Return if present in Cache
            var cacheKey = string.Format("UserRoles_{0}", userEmail);
            if (HttpRuntime.Cache[cacheKey] != null)
                return (string[])HttpRuntime.Cache[cacheKey];

            //Get the roles from DB
            var userRoles = new string[] { };

            var thisUser = _cacheManager.Get(cacheKey, () => _unitOfWork.Repository<HaoFancyUser>()
                .Query()
                .Filter(x => x.Email == userEmail)
                .Include(x => x.UserRole)
                .Include(x => x.UserGroup)
                .Get()
                .FirstOrDefault());

            if (thisUser != null)
                userRoles = new[] { thisUser.UserRole.UserRoleName };

            //Store in cache
            //HttpRuntime.Cache.Insert(cacheKey, userRoles, null, DateTime.Now.AddMinutes(_cacheTimeoutInMinutes), Cache.NoSlidingExpiration);

            // Return
            return userRoles.ToArray();
        }

        #endregion

        #region Overrides of RoleProvider that throw NotImplementedException

        /// <summary>
        /// Adds a new role to the data source for the configured applicationName.
        /// </summary>
        /// <param name="roleName">The name of the role to create.</param>
        public override void CreateRole(string roleName)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Removes a role from the data source for the configured applicationName.
        /// </summary>
        /// <returns>
        /// true if the role was successfully deleted; otherwise, false.
        /// </returns>
        /// <param name="roleName">The name of the role to delete.</param><param name="throwOnPopulatedRole">If true, throw an exception if <paramref name="roleName"/> has one or more members and do not delete <paramref name="roleName"/>.</param>
        public override bool DeleteRole(string roleName, bool throwOnPopulatedRole)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Gets a value indicating whether the specified role name already exists in the role data source for the configured applicationName.
        /// </summary>
        /// <returns>
        /// true if the role name already exists in the data source for the configured applicationName; otherwise, false.
        /// </returns>
        /// <param name="roleName">The name of the role to search for in the data source.</param>
        public override bool RoleExists(string roleName)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Adds the specified user names to the specified roles for the configured applicationName.
        /// </summary>
        /// <param name="usernames">A string array of user names to be added to the specified roles. </param><param name="roleNames">A string array of the role names to add the specified user names to.</param>
        public override void AddUsersToRoles(string[] usernames, string[] roleNames)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Removes the specified user names from the specified roles for the configured applicationName.
        /// </summary>
        /// <param name="usernames">A string array of user names to be removed from the specified roles. </param><param name="roleNames">A string array of role names to remove the specified user names from.</param>
        public override void RemoveUsersFromRoles(string[] usernames, string[] roleNames)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Gets a list of users in the specified role for the configured applicationName.
        /// </summary>
        /// <returns>
        /// A string array containing the names of all the users who are members of the specified role for the configured applicationName.
        /// </returns>
        /// <param name="roleName">The name of the role to get the list of users for.</param>
        public override string[] GetUsersInRole(string roleName)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Gets a list of all the roles for the configured applicationName.
        /// </summary>
        /// <returns>
        /// A string array containing the names of all the roles stored in the data source for the configured applicationName.
        /// </returns>
        public override string[] GetAllRoles()
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Gets an array of user names in a role where the user name contains the specified user name to match.
        /// </summary>
        /// <returns>
        /// A string array containing the names of all the users where the user name matches <paramref name="usernameToMatch"/> and the user is a member of the specified role.
        /// </returns>
        /// <param name="roleName">The role to search in.</param><param name="usernameToMatch">The user name to search for.</param>
        public override string[] FindUsersInRole(string roleName, string usernameToMatch)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Gets or sets the name of the application to store and retrieve role information for.
        /// </summary>
        /// <returns>
        /// The name of the application to store and retrieve role information for.
        /// </returns>
        public override string ApplicationName
        {
            get { throw new NotImplementedException(); }
            set { throw new NotImplementedException(); }
        }

        #endregion
    }

That is it for our CustomRoleProvider.

Step 3: Create the CustomIdentity

That is one of the major part to define all the properties you want to pass along with the User.Identity.

    [Serializable]
    public class CustomIdentity : IIdentity
    {
        #region Properties

        public IIdentity Identity { get; set; }
        public string FullName { get; set; }
        public int UserRoleId { get; set; }
        public string UserRoleName { get; set; }
        public int UserGroupId { get; set; }
        public string UserGroupName { get; set; }
        public int LoginCount { get; set; }
        public int GlobalPoints { get; set; }
        public int UserId { get; set; }
        public decimal UserGroupPriceModifier { get; set; }

        #endregion

        #region Implementation of IIdentity

        /// <summary>
        /// Gets the name of the current user.
        /// </summary>
        /// <returns>
        /// The name of the user on whose behalf the code is running.
        /// </returns>
        public string Name
        {
            get { return Identity.Name; }
        }

        public string AuthenticationType
        {
            get { return Identity.AuthenticationType; }
        }

        public bool IsAuthenticated { get { return Identity.IsAuthenticated; } }

        #endregion

        #region Constructor

        public CustomIdentity(IIdentity identity)
        {
            Identity = identity;

            var customMembershipUser = (CustomMembershipUser)Membership.GetUser(identity.Name);
            if (customMembershipUser != null)
            {
                FullName = customMembershipUser.FullName;
                UserRoleId = customMembershipUser.UserRoleId;
                UserRoleName = customMembershipUser.UserRoleName;
                UserGroupName = customMembershipUser.UserGroupName;
                LoginCount = customMembershipUser.LoginCount;
                GlobalPoints = customMembershipUser.GlobalPoints;
                UserId = customMembershipUser.UserId;
                UserGroupPriceModifier = customMembershipUser.UserGroupPriceModifier;
            }
        }

        #endregion
    }

Take a look on the Properties section. I create multiple properties such as the FullName, the LoginCount, UserId and so on. In the construct, I bind those properties.

Step 4: Create the CustomPrincipal

    [Serializable]
    public class CustomPrincipal : IPrincipal
    {
        #region Implementation of IPrincipal

        public bool IsInRole(string role)
        {
            return Identity is CustomIdentity && string.Compare(role, ((CustomIdentity)Identity).UserRoleName, StringComparison.CurrentCultureIgnoreCase) == 0;
        }

        public IIdentity Identity { get; private set; }

        #endregion

        public CustomIdentity CustomIdentity { get { return (CustomIdentity)Identity; } set { Identity = value; } }

        public CustomPrincipal(CustomIdentity identity)
        {
            Identity = identity;
        }
    }

Now, all the basic setups are finished. We are going to make something really cool now.

Step 5: Create the ProviderInitializationHttpModule

This is a critical part, don’t skip it!

    public class ProviderInitializationHttpModule : IHttpModule
    {
        public ProviderInitializationHttpModule(MembershipProvider membershipProvider, RoleProvider roleProvider)
        {
        }

        public void Init(HttpApplication context)
        {
        }

        public void Dispose()
        {
        }
    }

Now, we have to go go Ninject’s NinjectWebCommon file, and create the binding.

        private static void RegisterServices(IKernel kernel)
        {
            kernel.Load<ServiceModule>();
            kernel.Bind<MembershipProvider>().ToMethod(ctx => Membership.Provider);
            kernel.Bind<RoleProvider>().ToMethod(ctx => Roles.Provider);
            kernel.Bind<IHttpModule>().To<ProviderInitializationHttpModule>();
        }

Step 6: Create CustomMembershipUser

    public class CustomMembershipUser : MembershipUser
    {
        #region Properties

        public string FullName { get; set; }
        public int UserRoleId { get; set; }
        public string UserRoleName { get; set; }
        public int UserGroupId { get; set; }
        public string UserGroupName { get; set; }
        public int LoginCount { get; set; }
        public int GlobalPoints { get; set; }
        public int UserId { get; set; }
        public decimal UserGroupPriceModifier { get; set; }

        #endregion

        public CustomMembershipUser(HaoFancyUser user)
            : base("CustomMembershipProvider", user.Email, user.HaoFancyUserId, user.Email, string.Empty, string.Empty, true, false, DateTime.Now, DateTime.Now, DateTime.Now, DateTime.Now, DateTime.Now)
        {
            FullName = user.FullName;
            UserRoleId = user.UserRoleId;
            UserRoleName = user.UserRole.UserRoleName;
            UserGroupName = user.UserGroup.GroupName;
            LoginCount = user.LoginCount;
            GlobalPoints = user.UserGlobalPoints;
            UserId = user.HaoFancyUserId;
            UserGroupPriceModifier = user.UserGroup.PriceModifier;
        }
    }

Step 7: Create the final UserRoleAuthorizeAttribute

That is our Authorize module.

    public class UserRoleAuthorizeAttribute : System.Web.Mvc.AuthorizeAttribute
    {
        public UserRoleAuthorizeAttribute() { }

        public UserRoleAuthorizeAttribute(params UserRole[] roles)
        {
            Roles = string.Join(",", roles.Select(r => r.ToString()));
        }

        public override void OnAuthorization(AuthorizationContext filterContext)
        {
            base.OnAuthorization(filterContext);

            if (!filterContext.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute),false) && !filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true))
            {
                if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
                {
                    filterContext.Result = new RedirectResult("~/Index/Login");
                    return;
                }

                if (filterContext.Result is HttpUnauthorizedResult)
                {
                    filterContext.Result = new RedirectResult("/Error/AccessDenied");
                }
            }
        }

        protected override bool AuthorizeCore(HttpContextBase httpContext)
        {
            var isAuthorized = base.AuthorizeCore(httpContext);
            if (isAuthorized)
            {
                var authCookie = httpContext.Request.Cookies[FormsAuthentication.FormsCookieName];
                if (authCookie != null)
                {
                    //var authTicket = FormsAuthentication.Decrypt(authCookie.Value);
                    //var identity = new GenericIdentity(authTicket.Name, "Forms");
                    //var principal = new GenericPrincipal(identity, new string[] { });
                    //httpContext.User = principal;

                    var identity = new CustomIdentity(HttpContext.Current.User.Identity);
                    var principal = new CustomPrincipal(identity);
                    HttpContext.Current.User = principal;
                }
            }
            return isAuthorized;
        }
    }

    public enum UserRole
    {
        Admin = 1,
        CustomerClient = 2,
        CustomerVendor = 3
    }

The OnAuthorization is where we check for the authorization. If the person is not logged in, he/she will be redirected to the login page. If the person is logged in, but doesn’t have the required permission to visit a specific page, based on his/her role, he/she will be redirected to the Access Denied page we defined.

In the OnAuthorization  method, I also define the situation for Anonymous.

IMPORTANT

In the enum UserRole, the name must exactly matched the name from the database.

4

5

Step 8: Modify the Global.asax file and create a extension method

Open the Global.asax file, and copy paste this code into it.

        protected void Application_PostAuthenticateRequest(object sender, EventArgs e)
        {
            if (Request.IsAuthenticated)
            {
                var identity = new CustomIdentity(HttpContext.Current.User.Identity);
                var principal = new CustomPrincipal(identity);
                HttpContext.Current.User = principal;
            }
        }

Create a extension file, and add this method.

        public static CustomPrincipal ToCustomPrincipal(this IPrincipal principal)
        {
            return (CustomPrincipal)principal;
        }

 

Step 9: Update your Web.Config file

This code must be put into the system.web section. Also, please make sure the namespace of the CustomMembershipProvider and the CustomRoleProvider is correct.

    <membership defaultProvider="CustomMembershipProvider">
      <providers>
        <clear />
        <add name="CustomMembershipProvider"
             type="HKCart.Web.Security.CustomMembershipProvider"
             cacheTimeoutInMinutes="5" />
      </providers>
    </membership>

    <roleManager defaultProvider="CustomRoleProvider" enabled="true">
      <providers>
        <clear />
        <add name="CustomRoleProvider"
             type="HKCart.Web.Security.CustomRoleProvider" cacheTimeoutInMinutes="5" />
      </providers>
    </roleManager>

The Final Step, call our custom membership provider

It is very easy to call it.

    [UserRoleAuthorize(UserRole.CustomerClient, UserRole.CustomerVendor)]
    public class AccountController : Controller

 

[AllowAnonymous]
public ActionResult Register()

We are all done. I hope this is helpful to you. Have a good day.

Special thanks to Mightymada. My solution is based on her post, @ This Link