Blogs

The One With

January 2009 - Posts

  • What Membership Provider do you use?

         

    What do you use as your Membership Provider? Chances are you have a SqlMembershipProvider in your Web.config. All your login controls work via the Membership API and all is great. But what if you have an existing user/role db model that is different from the default one generated by aspnet_regsql.exe. And what if your web site is powered by something other than the SQL Server?

    Luckily, the Membership API is extensible enough and allows you to roll your own providers. In my case, I had an existing user/role model that I wanted to use via the Membership API.

    The  Users

    [Persistent("Users")]
    public class UserBase : XPLiteObject, IContactInfo {
        
        public UserBase(Session session)
            : base(session) {
        }
        
        public UserBase(IContactInfo contactInfo, Session session)
            : this(session) {
            if (contactInfo != null) {
                this.Email = contactInfo.Email;
                this.Prefix = contactInfo.Prefix;
                this.DisplayName = contactInfo.DisplayName;
                this.FirstName = contactInfo.FirstName;
                this.LastName = contactInfo.LastName;
                this.Address1 = contactInfo.Address1;
                this.Address2 = contactInfo.Address2;
                this.City = contactInfo.City;
                this.State = contactInfo.State;
                this.PostalCode = contactInfo.PostalCode;
                this.Country = contactInfo.Country;
            }
        }
    
        [NonPersistent]
        public string DisplayNameEx {
            get {
                return this.ToString();
            }
        }
    
        public override string ToString() {
            if (!string.IsNullOrEmpty(this.DisplayName)) {
                return this.DisplayName.Trim();
            }
            string s = this.FirstName;
            if (!string.IsNullOrEmpty(this.LastName)) {
                if (!string.IsNullOrEmpty(s)) {
                    s += " ";
                }
                s += this.LastName.Trim();
            }
            if (!string.IsNullOrEmpty(s)) {
                return s;
            }
            if (!string.IsNullOrEmpty(this.Email)) {
                return this.Email.Trim();
            }
            return this.UserName;
        }
    
        public string ToString(bool displayOnly) {
            if (!string.IsNullOrEmpty(this.DisplayName)) {
                return this.DisplayName.Trim();
            }
            string s = this.FirstName;
            if (!string.IsNullOrEmpty(this.LastName)) {
                if (!string.IsNullOrEmpty(s)) {
                    s += " ";
                }
                s += this.LastName.Trim();
            }
            if (!string.IsNullOrEmpty(s)) {
                return s;
            }
            if (displayOnly) {
                return string.Empty;
            }
            if (!string.IsNullOrEmpty(this.Email)) {
                return this.Email.Trim();
            }
            return this.UserName;
        }
    
        Guid _userId;
        [Key(true)]
        public Guid UserId {
            get {
                return _userId;
            }
            set {
                SetPropertyValue<Guid>("UserId", ref _userId, value);
            }
        }
        string _identityList;
        [Size(256)]
        public string IdentityList {
            get {
                return _identityList;
            }
            set {
                SetPropertyValue<string>("IdentityList", ref _identityList, value);
            }
        }    
        string _userName;
        [Size(256)]
        [Indexed(Unique = true)]
        public string UserName {
            get {
                return _userName;
            }
            set {
                SetPropertyValue<string>("UserName", ref _userName, value);
            }
        }
        string _email;
        [Size(256)]
        [Indexed(Unique = true)]
        public string Email {
            get {
                return _email;
            }
            set {
                SetPropertyValue<string>("Email", ref _email, value);
            }
        }
        string _password;
        [Size(128)]
        public string Password {
            get {
                return _password;
            }
            set {
                SetPropertyValue<string>("Password", ref _password, value);
            }
        }
        string _passwordSalt;
        [Size(128)]
        public string PasswordSalt {
            get {
                return _passwordSalt;
            }
            set {
                SetPropertyValue<string>("PasswordSalt", ref _passwordSalt, value);
            }
        }
        int _passwordFormat;
        public int PasswordFormat {
            get { 
                return _passwordFormat; 
            }
            set {
                SetPropertyValue<int>("PasswordFormat", ref _passwordFormat, value); 
            }
        }
        string _prefix;
        [Size(16)]
        public string Prefix {
            get {
                return _prefix;
            }
            set {
                SetPropertyValue<string>("Prefix", ref _prefix, value);
            }
        }
        string _displayName;
        [Size(128)]
        public string DisplayName {
            get {
                return _displayName;
            }
            set {
                SetPropertyValue<string>("DisplayName", ref _displayName, value);
            }
        }
        string _firstName;
        [Size(64)]
        public string FirstName {
            get {
                return _firstName;
            }
            set {
                SetPropertyValue<string>("FirstName", ref _firstName, value);
            }
        }
        string _lastName;
        [Size(64)]
        public string LastName {
            get {
                return _lastName;
            }
            set {
                SetPropertyValue<string>("LastName", ref _lastName, value);
            }
        }
        string _address1;
        [Size(128)]
        public string Address1 {
            get {
                return _address1;
            }
            set {
                SetPropertyValue<string>("Address1", ref _address1, value);
            }
        }
        string _address2;
        [Size(128)]
        public string Address2 {
            get {
                return _address2;
            }
            set {
                SetPropertyValue<string>("Address2", ref _address2, value);
            }
        }
        string _city;
        [Size(128)]
        public string City {
            get {
                return _city;
            }
            set {
                SetPropertyValue<string>("City", ref _city, value);
            }
        }
        string _state;
        [Size(64)]
        public string State {
            get {
                return _state;
            }
            set {
                SetPropertyValue<string>("State", ref _state, value);
            }
        }
        string _postalCode;
        [Size(16)]
        public string PostalCode {
            get {
                return _postalCode;
            }
            set {
                SetPropertyValue<string>("PostalCode", ref _postalCode, value);
            }
        }
        string _country;
        [Size(64)]
        public string Country {
            get {
                return _country;
            }
            set {
                SetPropertyValue<string>("Country", ref _country, value);
            }
        }
        Guid _themeId;
        public Guid ThemeId {
            get {
                return _themeId;
            }
            set {
                SetPropertyValue<Guid>("ThemeId", ref _themeId, value);
            }
        }
        int _flags;
        public int Flags {
            get {
                return _flags;
            }
            set {
                SetPropertyValue<int>("Flags", ref _flags, value);
            }
        }
    }

    My user object has all the boiler plate fields like User Name, Password, Contact Info etc... plus some special ones that are applicable only to my app, IdentityList (Windows Identities), ThemeId etc..

    The default MembershipUser does not contain these values, so we will need to roll our own as well.

    public class MembershipUserEx : MembershipUser {
        private Guid _themeId;
        public virtual Guid ThemeId {
            get {
                return _themeId;
            }
            set {
                _themeId = value;
            }
        }
    
        IContactInfo _contactInfo;
        public virtual IContactInfo ContactInfo {
            get {
                if (_contactInfo == null) {
                    _contactInfo = new ContactInfo();
                }
                return _contactInfo;
            }
            set {
                _contactInfo = new ContactInfo(value);
            }
        }
    }

     

     

     The MembershipProvider

    There is a couple of methods that we need to implement when inheriting from the MembershipProvider:

    public abstract class MembershipProvider : ProviderBase {
        public abstract string ApplicationName { get; set; }
        public abstract bool EnablePasswordReset { get; }
        public abstract bool EnablePasswordRetrieval { get; }
        public abstract int MaxInvalidPasswordAttempts { get; }
        public abstract int MinRequiredNonAlphanumericCharacters { get; }
        public abstract int MinRequiredPasswordLength { get; }
        public abstract int PasswordAttemptWindow { get; }
        public abstract MembershipPasswordFormat PasswordFormat { get; }
        public abstract string PasswordStrengthRegularExpression { get; }
        public abstract bool RequiresQuestionAndAnswer { get; }
        public abstract bool RequiresUniqueEmail { get; }
        public abstract bool ChangePassword(string username, string oldPassword, string newPassword);
        public abstract bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer);
        public abstract MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status);
        public abstract bool DeleteUser(string username, bool deleteAllRelatedData);
        public abstract MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords);
        public abstract MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords);
        public abstract MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords);
        public abstract int GetNumberOfUsersOnline();
        public abstract string GetPassword(string username, string answer);
        public abstract MembershipUser GetUser(object providerUserKey, bool userIsOnline);
        public abstract MembershipUser GetUser(string username, bool userIsOnline);
        public abstract string GetUserNameByEmail(string email);
        protected virtual void OnValidatingPassword(ValidatePasswordEventArgs e);
        public abstract string ResetPassword(string username, string answer);
        public abstract bool UnlockUser(string userName);
        public abstract void UpdateUser(MembershipUser user);
        public abstract bool ValidateUser(string username, string password);
    }

    The most important ones are: CreateUser, GetUser and ValidateUser:

    CreateUser

    Just new up a UserBase object and save it.

    using (IDataLayer dataLayer = MembershipConnectionHelper.GetDataLayer(username, 
                    this._connectionString, 
                    AutoCreateOption.DatabaseAndSchema)) {
                    
                    using (Session session = new Session(dataLayer)) {
                        session.BeginTransaction();                
                        try {
                            DateTime time = this.RoundToSeconds(DateTime.UtcNow).ToLocalTime();
    
                            UserBase user = new UserBase(contactInfo, session);
                            user.UserName = username;
                            user.Email = email;
                            user.ThemeId = themeId;
                            user.Password = encodedPassword;
                            user.PasswordSalt = salt;
                            user.PasswordFormat = (int)this.PasswordFormat;
                            user.Save();
                            
                            session.CommitTransaction();
                            
                            status = MembershipCreateStatus.Success;
    
                            return new MembershipUserEx(
                                this.Name,
                                username,
                                user.UserId,
                                email,
                                passwordQuestion,
                                null,
                                isApproved,
                                false,
                                time,
                                time,
                                time,
                                time,
                                DateTime.MinValue,
                                user) {
                                    ThemeId = themeId
                                };
                        }
                        catch {
                            status = MembershipCreateStatus.ProviderError;
                            session.RollbackTransaction();
                            throw;
                        }
                    }
                }            

    A helper class that creates our IDataLayer based on config.

    public static class MembershipConnectionHelper {
            public static IDataLayer GetDataLayer(string hashString, string connectionString, 
                AutoCreateOption autoCreateOption) {
                
                XPDictionary dictionary = new DevExpress.Xpo.Metadata.ReflectionDictionary();
                dictionary.GetDataStoreSchema(typeof(UserBase).Assembly);
    
                IDataStore dataStore = XpoDefault.GetConnectionProvider(connectionString,
                    autoCreateOption);
                
                return new DevExpress.Xpo.ThreadSafeDataLayer(dictionary, dataStore);
    }
        }

    GetUser

    public UserBase GetUser(string username) {
        using (IDataLayer dataLayer = MembershipConnectionHelper.GetDataLayer(username,
            this._connectionString,
            AutoCreateOption.DatabaseAndSchema)) {
        
            using (Session session = new Session(dataLayer)) {
                return session.FindObject<UserBase>(
                    new BinaryOperator("UserName", username));
            }
        }
    }

    ValidateUser

    ValidateUser is also simple. For example my simple validation is to check if the passwords match:

    public override bool ValidateUser(string username, string password) {
            if ((!ValidateParameter(ref username, true, true, true, 256) || 
                !ValidateParameter(ref password, true, true, false, 128))) {
                return false;
            }
            
            UserBase user = GetUser(username);
            if (user == null) {
                return false;
            }
            
            string encodedPassword = this.EncodePassword(
                password, 
                (MembershipPasswordFormat)user.PasswordFormat, 
                user.PasswordSalt);
            
            return encodedPassword.Equals(user.Password);
    }

    After implementing the MembershipProvider we just register it in the Web.config (we'll need to GAC our assembly for it to be loaded properly.)

    <connectionStrings>
        <!-- Generate a custom key pair if the specified password format is "Encrypted" -->
        <!--
        <add name="%CONNECTION_STRING_NAME%" connectionString="%CONNECTION_STRING%" />
        -->
      </connectionStrings>
      <system.web>
        <!-- Generate a custom key pair if the specified password format is "Encrypted" -->
        <!--
        <machineKey
          validationKey='E81B8940E6D6588E20F5A79D2DD35CCE9E3471081D4F40AE330662EB38AE0F64455CB3E277E7AAD92E76A6D8D4332EA736BF31831CC4A11A8A653EEFD98A68DA'
          decryptionKey='0751C7C4A520D221A84AE8541B003AF2ABD047FE12D662D6'
          validation='SHA1' />
        -->
        <membership defaultProvider="%MEMBERSHIP_PROVIDER_NAME%">
          <providers>
            <add
              connectionStringName="%CONNECTION_STRING_NAME%"
              enablePasswordRetrieval="false"
              enablePasswordReset="true"
              requiresQuestionAndAnswer="false"
              applicationName="/"
              requiresUniqueEmail="true"
              passwordFormat="Hashed"
              maxInvalidPasswordAttempts="5"
              minRequiredPasswordLength="1"
              minRequiredNonalphanumericCharacters="0"
              passwordAttemptWindow="10"
              passwordStrengthRegularExpression=""
              name="%MEMBERSHIP_PROVIDER_NAME%"
              type="DevExpress.Private.Web.Xpo.MembershipProvider, DevExpress.Private.Web, Version=1.0.0.0, Culture=neutral, PublicKeyToken=3cc6fdd91321e5a7"  />
          </providers>
        </membership>
      </system.web>

    Download Sample Code

    Cheers,

    Azret

More from DevExpress
Live Chat
Have a pre-sales question?
Need assistance with your evaluation?
We are here to help.
Chat is one of the many ways you can contact members of the DevExpress Team. We are available Monday-Friday between 8:30am and 5:00pm Pacific Time.
If you need additional product information, require pre-sales assistance, or want help with your order, write to us at info@devexpress.com or call us at
+1 (818) 844-3383.