Monday, December 5, 2011

Custom Membership Provider

Custom Membership Provider

Custom Membership, Role Providers, Membership User Series.

 Since I noticed that this articles and the examples in them are pretty long, it could get pretty cumbersome to have all of them in one go so I split them up in several articles. Here goes the first one.

Custom Membership Provider

There are many times when the MembershipProvider and its underlying database construction aren’t sufficient enough for our needs. As MSDN states there are two reasons why one would want a custom MembersipProvider:
  • You need to store membership information in a data source that is not supported by the membership providers included with the .NET Framework, such as a FoxPro database, an Oracle database, or other data sources.
  • You need to manage membership information using a database schema that is different from the database schema used by the providers that ship with the .NET Framework. A common example of this would be membership data that already exists in a SQL Server database for a company or Web site.

To implement a custom membership provider, you create a class that inherits the MembershipProvider abstract class from the System.Web.Security namespace. The MembershipProvider  abstract class inherits the ProviderBase abstract class from the System.Configuration.Provider namespace, so you must implement the required members of the ProviderBase  class as well. 
For example this custom membership provider uses LINQ-to-SQL and my own tables in MS SQL Server to store and retrieve membership information in my database:

namespace Custom.Membership
{
    using System;
    using System.Linq;
    using System.Configuration;
    using System.Collections.Specialized;
    using System.Configuration.Provider;
    using System.Data;
    using System.Data.SqlClient;
    using System.Security.Cryptography;
    using System.Text;
    using System.Web.Configuration;
    using System.Web.Security;
 
    public sealed class CustomMembershipProvider : MembershipProvider
    {

        #region Class Variables

        private int newPasswordLength = 8;
        private string connectionString;
        private string applicationName;
        private bool enablePasswordReset;
        private bool enablePasswordRetrieval;
        private bool requiresQuestionAndAnswer;
        private bool requiresUniqueEmail;
        private int maxInvalidPasswordAttempts;
        private int passwordAttemptWindow;
        private MembershipPasswordFormat passwordFormat;
        private int minRequiredNonAlphanumericCharacters;
        private int minRequiredPasswordLength;
        private string passwordStrengthRegularExpression;
        private MachineKeySection machineKey; //Used when determining encryption key values.

        #endregion

        #region Properties

        public override string ApplicationName
        {
            get
            {
                return applicationName;
            }
            set
            {
                applicationName = value;
            }
        }

        public override bool EnablePasswordReset
        {
            get
            {
                return enablePasswordReset;
            }
        }

        public override bool EnablePasswordRetrieval
        {
            get
            {
                return enablePasswordRetrieval;
            }
        }

        public override bool RequiresQuestionAndAnswer
        {
            get
            {
                return requiresQuestionAndAnswer;
            }
        }

        public override bool RequiresUniqueEmail
        {
            get
            {
                return requiresUniqueEmail;
            }
        }

        public override int MaxInvalidPasswordAttempts
        {
            get
            {
                return maxInvalidPasswordAttempts;
            }
        }

        public override int PasswordAttemptWindow
        {
            get
            {
                return passwordAttemptWindow;
            }
        }

        public override MembershipPasswordFormat PasswordFormat
        {
            get
            {
                return passwordFormat;
            }
        }

        public override int MinRequiredNonAlphanumericCharacters
        {
            get
            {
                return minRequiredNonAlphanumericCharacters;
            }
        }

        public override int MinRequiredPasswordLength
        {
            get
            {
                return minRequiredPasswordLength;
            }
        }

        public override string PasswordStrengthRegularExpression
        {
            get
            {
                return passwordStrengthRegularExpression;
            }
        }

        #endregion

        #region MembershipProvider overrides

        public override void Initialize(string name, NameValueCollection config)
        {
            if (config == null)
            {
                string configPath = "~/web.config";
                Configuration NexConfig = WebConfigurationManager.OpenWebConfiguration(configPath);
                MembershipSection section = (MembershipSection)NexConfig.GetSection("system.web/membership");
                ProviderSettingsCollection settings = section.Providers;
                NameValueCollection membershipParams = settings[section.DefaultProvider].Parameters;
                config = membershipParams;
            }

            if (name == null || name.Length == 0)
            {
                name = "CustomMembershipProvider";
            }

            if (String.IsNullOrEmpty(config["description"]))
            {
                config.Remove("description");
                config.Add("description", "Custom Membership Provider");
            }

            //Initialize the abstract base class.
            base.Initialize(name, config);

            applicationName = GetConfigValue(config["applicationName"], System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath);
            maxInvalidPasswordAttempts = Convert.ToInt32(GetConfigValue(config["maxInvalidPasswordAttempts"], "5"));
            passwordAttemptWindow = Convert.ToInt32(GetConfigValue(config["passwordAttemptWindow"], "10"));
            minRequiredNonAlphanumericCharacters = Convert.ToInt32(GetConfigValue(config["minRequiredAlphaNumericCharacters"], "1"));
            minRequiredPasswordLength = Convert.ToInt32(GetConfigValue(config["minRequiredPasswordLength"], "7"));
            passwordStrengthRegularExpression = Convert.ToString(GetConfigValue(config["passwordStrengthRegularExpression"], String.Empty));
            enablePasswordReset = Convert.ToBoolean(GetConfigValue(config["enablePasswordReset"], "true"));
            enablePasswordRetrieval = Convert.ToBoolean(GetConfigValue(config["enablePasswordRetrieval"], "true"));
            requiresQuestionAndAnswer = Convert.ToBoolean(GetConfigValue(config["requiresQuestionAndAnswer"], "false"));
            requiresUniqueEmail = Convert.ToBoolean(GetConfigValue(config["requiresUniqueEmail"], "true"));

            string temp_format = config["passwordFormat"];
            if (temp_format == null)
            {
                temp_format = "Hashed";
            }

            switch (temp_format)
            {
                case "Hashed":
                    passwordFormat = MembershipPasswordFormat.Hashed;
                    break;
                case "Encrypted":
                    passwordFormat = MembershipPasswordFormat.Encrypted;
                    break;
                case "Clear":
                    passwordFormat = MembershipPasswordFormat.Clear;
                    break;
                default:
                    throw new ProviderException("Password format not supported.");
            }

            ConnectionStringSettings ConnectionStringSettings = ConfigurationManager.ConnectionStrings[config["connectionStringName"]];

            if ((ConnectionStringSettings == null) || (ConnectionStringSettings.ConnectionString.Trim() == String.Empty))
            {
                throw new ProviderException("Connection string cannot be blank.");
            }

            connectionString = ConnectionStringSettings.ConnectionString;

            //Get encryption and decryption key information from the configuration.
            System.Configuration.Configuration cfg = WebConfigurationManager.OpenWebConfiguration(System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath);
            machineKey = cfg.GetSection("system.web/machineKey") as MachineKeySection;

            if (machineKey.ValidationKey.Contains("AutoGenerate"))
            {
                if (PasswordFormat != MembershipPasswordFormat.Clear)
                {
                    throw new ProviderException("Hashed or Encrypted passwords are not supported with auto-generated keys.");
                }
            }
        }

        public override bool ChangePassword(string username, string oldPassword, string newPassword)
        {
            if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(oldPassword) || string.IsNullOrWhiteSpace(newPassword)) return false;

            if (oldPassword == newPassword) return false;

            CustomMembershipUser user = GetUser(username);

            if (user == null) return false;

            CustomDataDataContext db = new CustomDataDataContext();
            var RawUser = (from u in db.Users
                           where u.UserName == user.UserName && u.DeletedOn == null
                           select u).FirstOrDefault();

            if (string.IsNullOrWhiteSpace(RawUser.Password)) return false;

            RawUser.Password = EncodePassword(newPassword);

            db.SubmitChanges();

            return true;
        }

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

        /// <summary>
        /// Create custom CustomMembershipUser.
        /// </summary>
        /// <param name="username"></param>
        /// <param name="password"></param>
        /// <param name="email"></param>
        /// <param name="passwordQuestion"></param>
        /// <param name="passwordAnswer"></param>
        /// <param name="isApproved"></param>
        /// <param name="providerUserKey"></param>
        /// <param name="status"></param>
        /// <param name="companyID"></param>
        /// <param name="name"></param>
        /// <returns></returns>
        public CustomMembershipUser CreateUser(
                string username,
                string password,
                string email,
                string passwordQuestion,
                string passwordAnswer,
                bool isApproved,
                object providerUserKey,
                out MembershipCreateStatus status,
                int companyID,
                string name,
                string phoneNumber)
        {
            ValidatePasswordEventArgs args = new ValidatePasswordEventArgs(username, password, true);

            OnValidatingPassword(args);

            if (args.Cancel)
            {
                status = MembershipCreateStatus.InvalidPassword;
                return null;
            }

            if ((RequiresUniqueEmail && (GetUserNameByEmail(email) != String.Empty)))
            {
                status = MembershipCreateStatus.DuplicateEmail;
                return null;
            }

            CustomMembershipUser CustomMembershipUser = GetUser(username);

            if (CustomMembershipUser == null)
            {
                try
                {
                    using (CustomDataDataContext _db = new CustomDataDataContext())
                    {
                        User user = new User();
                        user.CompanyFK = companyID;
                        user.Name = name;
                        user.UserName = username;
                        user.Password = EncodePassword(password);
                        user.Email = email.ToLower();
                        user.CreatedOn = DateTime.Now;
                        user.ModifiedOn = DateTime.Now;
                        user.Phone = phoneNumber;
                        _db.Users.InsertOnSubmit(user);

                        _db.SubmitChanges();

                        status = MembershipCreateStatus.Success;

                        return GetUser(username);
                    }

                }
                catch
                {
                    status = MembershipCreateStatus.ProviderError;
                }
            }
            else
            {
                status = MembershipCreateStatus.DuplicateUserName;
            }

            return null;
        }

        /// <summary>
        /// Createa MembershipUser.
        /// </summary>
        /// <param name="username"></param>
        /// <param name="password"></param>
        /// <param name="email"></param>
        /// <param name="passwordQuestion"></param>
        /// <param name="passwordAnswer"></param>
        /// <param name="isApproved"></param>
        /// <param name="providerUserKey"></param>
        /// <param name="status"></param>
        /// <returns></returns>
        public override MembershipUser CreateUser(
            string username,
            string password,
            string email,
            string passwordQuestion,
            string passwordAnswer,
            bool isApproved,
            object providerUserKey,
            out MembershipCreateStatus status)
        {
            ValidatePasswordEventArgs args = new ValidatePasswordEventArgs(username, password, true);

            OnValidatingPassword(args);

            if (args.Cancel)
            {
                status = MembershipCreateStatus.InvalidPassword;
                return null;
            }

            if ((RequiresUniqueEmail && (GetUserNameByEmail(email) != String.Empty)))
            {
                status = MembershipCreateStatus.DuplicateEmail;
                return null;
            }

            MembershipUser membershipUser = GetUser(username, false);

            if (membershipUser == null)
            {
                try
                {
                    using (CustomDataDataContext _db = new CustomDataDataContext())
                    {
                        User user = new User();
                        user.CompanyFK = 0;
                        user.Name = "";
                        user.UserName = username;
                        user.Password = EncodePassword(password);
                        user.Email = email.ToLower();
                        user.CreatedOn = DateTime.Now;
                        user.ModifiedOn = DateTime.Now;

                        _db.Users.InsertOnSubmit(user);

                        _db.SubmitChanges();

                        status = MembershipCreateStatus.Success;

                        return GetUser(username, false);
                    }

                }
                catch
                {
                    status = MembershipCreateStatus.ProviderError;
                }
            }
            else
            {
                status = MembershipCreateStatus.DuplicateUserName;
            }

            return null;
        }

        /// <summary>
        /// Delete user
        /// </summary>
        /// <param name="username"></param>
        /// <param name="deleteAllRelatedData"></param>
        /// <returns></returns>
        public override bool DeleteUser(string username, bool deleteAllRelatedData)
        {
            bool ret = false;

            using (CustomDataDataContext _db = new CustomDataDataContext())
            {
                try
                {
                    User user = (from u in _db.Users
                                 where u.UserName == username && u.DeletedOn == null
                                 select u).FirstOrDefault();

                    if (user != null)
                    {
                        _db.Users.DeleteOnSubmit(user);

                        _db.SubmitChanges();

                        ret = true;
                    }
                }
                catch
                {
                    ret = false;
                }
            }

            return ret;
        }

        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)
        {
            using (CustomDataDataContext _db = new CustomDataDataContext())
            {
                try
                {
                    var pass = (from p in _db.Users
                                where p.UserName == username && p.DeletedOn == null
                                select p.Password).FirstOrDefault();
                    if (!string.IsNullOrWhiteSpace(pass))
                        return UnEncodePassword(pass);
                }
                catch { }
            }
            return null;
        }


        public CustomMembershipUser GetUser(string username)
        {
            CustomMembershipUser CustomMembershipUser = null;
            using (CustomDataDataContext _db = new CustomDataDataContext())
            {
                try
                {
                    User user = (from u in _db.Users
                                 where u.UserName == username && u.DeletedOn == null
                                 select u)
                                 .FirstOrDefault();

                    if (user != null)
                    {
                        CustomMembershipUser = new CustomMembershipUser(
                            this.Name,
                            user.UserName,
                            null,
                            user.Email,
                            "",
                            "",
                            true,
                            false,
                            user.CreatedOn,
                            DateTime.Now,
                            DateTime.Now,
                            default(DateTime),
                            default(DateTime),
                            user.CompanyFK,
                            user.Name);
                    }
                }
                catch
                {
                }
            }

            return CustomMembershipUser;
        }

        /// <summary>
        /// Get MembershipUser.
        /// </summary>
        /// <param name="username"></param>
        /// <param name="userIsOnline"></param>
        /// <returns></returns>
        public override MembershipUser GetUser(string username, bool userIsOnline)
        {
            MembershipUser membershipUser = null;
            using (CustomDataDataContext _db = new CustomDataDataContext())
            {
                try
                {
                    User user = (from u in _db.Users
                                 where u.UserName == username && u.DeletedOn == null
                                 select u)
                                 .FirstOrDefault();

                    if (user != null)
                    {
                        membershipUser = new MembershipUser(this.Name,
                            user.UserName,
                            null,
                            user.Email,
                            "",
                            "",
                            true,
                            false,
                            user.CreatedOn,
                            DateTime.Now,
                            DateTime.Now,
                            default(DateTime),
                            default(DateTime));
                    }
                }
                catch
                {
                }
            }

            return membershipUser;
        }

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

        public override string GetUserNameByEmail(string email)
        {
            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)
        {
            using (CustomDataDataContext _db = new CustomDataDataContext())
            {
                try
                {
                    User userToEdit = (from u in _db.Users
                                       where u.UserName == user.UserName && u.DeletedOn == null
                                       select u).FirstOrDefault();

                    if (userToEdit != null)
                    {

                        // submit changes
                        //_db.SubmitChanges();
                    }
                }
                catch
                {
                }
            }
        }

        public void UpdateCustomUser(CustomMembershipUser user)
        {
            using (CustomDataDataContext _db = new CustomDataDataContext())
            {
                try
                {
                    User userToEdit = (from u in _db.Users
                                       where u.UserName == user.UserName && u.DeletedOn == null
                                       select u).FirstOrDefault();

                    if (userToEdit != null)
                    {
                        userToEdit.Name = user.Name;
                        userToEdit.Email = user.Email;
                        userToEdit.CompanyFK = user.CompanyFK;


                        // submit changes
                        _db.SubmitChanges();
                    }
                }
                catch
                {
                }
            }
        }

        /// <summary>
        /// Validate user.
        /// </summary>
        /// <param name="username"></param>
        /// <param name="password"></param>
        /// <returns></returns>
        public override bool ValidateUser(string username, string password)
        {
            bool isValid = false;

            using (CustomDataDataContext _db = new CustomDataDataContext())
            {
                try
                {
                    User user = (from u in _db.Users
                                 where u.UserName == username && u.DeletedOn == null
                                 select u).FirstOrDefault();

                    if (user != null)
                    {
                        string storedPassword = user.Password;
                        if (CheckPassword(password, storedPassword))
                        {
                            isValid = true;
                        }
                    }
                }
                catch
                {
                    isValid = false;
                }
            }
            return isValid;
        }
        #endregion

        #region Utility Methods

        /// <summary>
        /// Check the password format based upon the MembershipPasswordFormat.
        /// </summary>
        /// <param name="password">Password</param>
        /// <param name="dbpassword"></param>
        /// <returns></returns>
        /// <remarks></remarks>
        private bool CheckPassword(string password, string dbpassword)
        {
            string pass1 = password;
            string pass2 = dbpassword;

            switch (PasswordFormat)
            {
                case MembershipPasswordFormat.Encrypted:
                    pass2 = UnEncodePassword(dbpassword);
                    break;
                case MembershipPasswordFormat.Hashed:
                    pass1 = EncodePassword(password);
                    break;
                default:
                    break;
            }

            if (pass1 == pass2)
            {
                return true;
            }

            return false;
        }

        /// <summary>
        /// UnEncode password.
        /// </summary>
        /// <param name="encodedPassword">Password.</param>
        /// <returns>Unencoded password.</returns>
        private string UnEncodePassword(string encodedPassword)
        {
            string password = encodedPassword;

            switch (PasswordFormat)
            {
                case MembershipPasswordFormat.Clear:
                    break;
                case MembershipPasswordFormat.Encrypted:
                    password =
                      Encoding.Unicode.GetString(DecryptPassword(Convert.FromBase64String(password)));
                    break;
                case MembershipPasswordFormat.Hashed:
                    //HMACSHA1 hash = new HMACSHA1();
                    //hash.Key = HexToByte(machineKey.ValidationKey);
                    //password = Convert.ToBase64String(hash.ComputeHash(Encoding.Unicode.GetBytes(password)));

                    throw new ProviderException("Not implemented password format (HMACSHA1).");
                default:
                    throw new ProviderException("Unsupported password format.");
            }

            return password;
        }

        /// <summary>
        /// Get config value.
        /// </summary>
        /// <param name="configValue"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        private string GetConfigValue(string configValue, string defaultValue)
        {
            if (String.IsNullOrEmpty(configValue))
            {
                return defaultValue;
            }

            return configValue;
        }

        /// <summary>
        /// Encode password.
        /// </summary>
        /// <param name="password">Password.</param>
        /// <returns>Encoded password.</returns>
        private string EncodePassword(string password)
        {
            string encodedPassword = password;

            switch (PasswordFormat)
            {
                case MembershipPasswordFormat.Clear:
                    break;
                case MembershipPasswordFormat.Encrypted:
                    byte[] encryptedPass = EncryptPassword(Encoding.Unicode.GetBytes(password));
                    encodedPassword = Convert.ToBase64String(encryptedPass);
                    break;
                case MembershipPasswordFormat.Hashed:
                    HMACSHA1 hash = new HMACSHA1();
                    hash.Key = HexToByte(machineKey.ValidationKey);
                    encodedPassword =
                      Convert.ToBase64String(hash.ComputeHash(Encoding.Unicode.GetBytes(password)));
                    break;
                default:
                    throw new ProviderException("Unsupported password format.");
            }

            return encodedPassword;
        }

        /// <summary>
        /// Converts a hexadecimal string to a byte array. Used to convert encryption key values from the configuration
        /// </summary>
        /// <param name="hexString"></param>
        /// <returns></returns>
        /// <remarks></remarks>
        private byte[] HexToByte(string hexString)
        {
            byte[] returnBytes = new byte[hexString.Length / 2];
            for (int i = 0; i < returnBytes.Length; i++)
                returnBytes[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);
            return returnBytes;
        }

        #endregion
    }
Of course, not all the class members are implemented, but for illustration I think the example is long enough so that you can get the point.
There are some methods here like:

public CustomMembershipUser GetUser(string username)

I deliberately put that there so that I can illustrate that you could return a custom membership user. We will discuss this in one of the next parts of these series.

In order for this to work you need to tell the web application that we are going to use a custom membership provider. So, add the following line to web.config:

<membership defaultProvider="CustomMembershipProvider">
      <providers>
        <clear />
        <add name="CustomMembershipProvider" type="Custom.Membership.CustomMembershipProvider" connectionStringName="CustomConnectionString" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" requiresUniqueEmail="false" maxInvalidPasswordAttempts="5" minRequiredPasswordLength="6" minRequiredNonalphanumericCharacters="0" passwordAttemptWindow="10" applicationName="/" passwordFormat="Encrypted" />
      </providers>
    </membership>

This way you are telling the application which provider to use and initialize its members with default values.
Now, to the other most important part of this - the usage. The default usage is pretty straightforward i.e. you just call any of the methods trough the Membership custom class:
if(Membership.ValidateUser(userName.Text, password.Text))
{
   // do something
}

You might have added extra methods to your custom membership class, like IsUserActive(string username) in that case you can get your custom provider trough the Provider or Providers properties of Membership and call the method:
CustomMembershipProvider customMemebership = (CustomMembershipProvider)System.Web.Security.Membership.Providers["CustomMembershipProvider"];
bool active = customMembership.IsUserActive(username);


 That's that. You have your custom membership provider. This is far from over as far as the example is concerned. We will continue our discussion in the next part of our series.


Happy coding.


Bojan

Other chapters from these series:

8 comments:

  1. Hi there, I'm fairly new to .net. I'm currently implementing .net framework 4.5 mvc3 using mysql as a database. Would this article be applicable to my case. T

    Regards,

    Best

    ReplyDelete
  2. Hi Best,

    You could implement the entire provider just using interface. For these interfaces you could later make implementations in whichever db you want and inject the dependency.

    In my post I'm just doing it using Linq-To-Sql as an illustration. I would definetly do it using interfaces if it was for production. That way you would not be depending on any concrete implementation or usage of any concrete db, but sort of "plug in" any of them you want.

    Cheers,
    Bojan

    ReplyDelete
  3. I can follow your process to create custom provider with mysql database for sharepoint FBA

    ReplyDelete
  4. can you provide me with the database table fields schema. Im using mysql

    ReplyDelete
  5. Hello Bojan
    I'm trying to implement the Custom Membership Provider, but in the Initialize method, the line "base.Initialize(name, config);" seems to clear out the config collection, resulting in subsequent error messages - after executing that line, all the config values are null. I've seen this code on many websites - including Microsoft, so nobody seems to have this problem.
    Can you please help? Thank you very much.

    Regards
    Mihai


    ReplyDelete
  6. Hi Bojan,

    Thank you very much for this nice walktrough. It is simple and straight forward. I implemented most of the stuff you showed here, but there is one spot I couldn't get. After I validate the user with Membership.ValidateUser I use RedirectToAction method to redirect the user to another page. But when I use User.Identity.IsAuthenticated I get false. Actually User.Identity is empty.

    How can I populate the Identity of the user and keep it in the session ?

    ReplyDelete
  7. Hi Bojan, Thank you very much for acticer. I have a question. If CustomConnectionString was encrypted on connectionstring then how to CustomMembershipProvider decrypt name CustomConnectionString. I'm looking for Protect web.config. Thanks you agian. I hope you'll reply for me. Sorry i am not good English.

    ReplyDelete
  8. Wow! You just saved me a ton of time typing and researching. Thank you for posting this.

    ReplyDelete