Migrate from Universal Membership Provider to ASP.NET Identity 2.1.0 - PasswordHarsher

1792 views 2 comments posted at about 3 years ago Raymond

In my previous post, I demonstrated how to migrate from ASP.NET Universal Membership Provider to ASP.NET Identity 2.1.0.

http://kosmisch.net/Blog/DotNetEssential/Archive/2015/1/31/migrate-from-systemwebproviders-to-aspnet-identity-10-and-then-to-210.html

Based on the researches, I have consolidated one ApplicationUserManager class to help you handle the password rehash. It also added two properties for keeping legacy password format.

Code for ApplicationUserManager class

using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using System;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;

namespace KosmischStudio.Website.Models
{
    public class ApplicationUserManager : UserManager<ApplicationUser>
    {
        public ApplicationUserManager()
            : base(new UserStore<ApplicationUser>(new ApplicationDbContext()))
        {
            this.PasswordHasher = new SqlPasswordHasher()
            {
                KeepLegacyHashFormat = false,
                PasswordFormat = 1
            };
        }

        protected async override Task<bool> VerifyPasswordAsync(IUserPasswordStore<ApplicationUser, string> store, ApplicationUser user, string password)
        {
            var hash = await store.GetPasswordHashAsync(user).ConfigureAwait(false);

            if (this.PasswordHasher.VerifyHashedPassword(hash, password) == PasswordVerificationResult.SuccessRehashNeeded)
            {
                // Make our new hash
                hash = PasswordHasher.HashPassword(password);

                // Save it to the DB
                await store.SetPasswordHashAsync(user, hash).ConfigureAwait(false);

                // Invoke internal method to upgrade the security stamp
                BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic;
                MethodInfo minfo = typeof(UserManager<ApplicationUser>).GetMethod("UpdateSecurityStampInternal", bindingFlags);
                var updateSecurityStampInternalTask = (Task)minfo.Invoke(this, new[] { user });
                await updateSecurityStampInternalTask.ConfigureAwait(false);

                // Update user
                await UpdateAsync(user).ConfigureAwait(false);
            }

            return PasswordHasher.VerifyHashedPassword(hash, password) != PasswordVerificationResult.Failed;
        }
    }

    public class SqlPasswordHasher : PasswordHasher
    {
        /// <summary>
        /// Whether to keep the legacy hash format: {$HashedPassword}|{$Format}|{$Salt}
        /// </summary>
        /// <returns></returns>
        public bool KeepLegacyHashFormat { get; set; }

        /// <summary>
        /// Format of the password: 1 : Hashed, 0 Clear, 2 Encrypt
        /// </summary>
        /// <returns></returns>
        public int PasswordFormat { get; set; }

        public override string HashPassword(string password)
        {

            if (KeepLegacyHashFormat)
            {
                StringBuilder passwordNew = new StringBuilder();
                string salt = GenerateSalt();
                string hashedPassword = EncryptPassword(password, PasswordFormat, salt);
                StringBuilder newPassword = new StringBuilder();
                newPassword.AppendFormat("{0}|{1}|{2}", hashedPassword, PasswordFormat, salt);
                return newPassword.ToString();
            }
            else
                return base.HashPassword(password);
        }

        private static string GenerateSalt()
        {
            string base64String;
            using (RNGCryptoServiceProvider rNGCryptoServiceProvider = new RNGCryptoServiceProvider())
            {
                byte[] numArray = new byte[16];
                rNGCryptoServiceProvider.GetBytes(numArray);
                base64String = Convert.ToBase64String(numArray);
            }
            return base64String;
        }

        public override PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword)
        {
            string[] passwordProperties = hashedPassword.Split('|');
            if (passwordProperties.Length != 3)
            {
                return base.VerifyHashedPassword(hashedPassword, providedPassword);
            }
            else
            {
                string passwordHash = passwordProperties[0];
                int passwordformat = 1;
                string salt = passwordProperties[2];
                if (String.Equals(EncryptPassword(providedPassword, passwordformat, salt), passwordHash, StringComparison.CurrentCultureIgnoreCase))
                {
                    if (!this.KeepLegacyHashFormat)
                        return PasswordVerificationResult.SuccessRehashNeeded;
                    else
                        return PasswordVerificationResult.Success;
                }
                else
                {
                    return PasswordVerificationResult.Failed;
                }
            }
        }



        //This is copied from the existing SQL providers and is provided only for back-compat.
        private string EncryptPassword(string pass, int passwordFormat, string salt)
        {
            if (passwordFormat == 0) // MembershipPasswordFormat.Clear
                return pass;

            byte[] bIn = Encoding.Unicode.GetBytes(pass);
            byte[] bSalt = Convert.FromBase64String(salt);
            byte[] bRet = null;

            if (passwordFormat == 1)
            { // MembershipPasswordFormat.Hashed 
                HashAlgorithm hm = HashAlgorithm.Create("SHA1");
                if (hm is KeyedHashAlgorithm)
                {
                    KeyedHashAlgorithm kha = (KeyedHashAlgorithm)hm;
                    if (kha.Key.Length == bSalt.Length)
                    {
                        kha.Key = bSalt;
                    }
                    else if (kha.Key.Length < bSalt.Length)
                    {
                        byte[] bKey = new byte[kha.Key.Length];
                        Buffer.BlockCopy(bSalt, 0, bKey, 0, bKey.Length);
                        kha.Key = bKey;
                    }
                    else
                    {
                        byte[] bKey = new byte[kha.Key.Length];
                        for (int iter = 0; iter < bKey.Length;)
                        {
                            int len = Math.Min(bSalt.Length, bKey.Length - iter);
                            Buffer.BlockCopy(bSalt, 0, bKey, iter, len);
                            iter += len;
                        }
                        kha.Key = bKey;
                    }
                    bRet = kha.ComputeHash(bIn);
                }
                else
                {
                    byte[] bAll = new byte[bSalt.Length + bIn.Length];
                    Buffer.BlockCopy(bSalt, 0, bAll, 0, bSalt.Length);
                    Buffer.BlockCopy(bIn, 0, bAll, bSalt.Length, bIn.Length);
                    bRet = hm.ComputeHash(bAll);
                }
            }

            return Convert.ToBase64String(bRet);
        }
    }
}
 

Issues – PasswordHarsher doesn’t work well for legacy password.

I have been spending almost one day to investigate why I could not get the same hashed password by using same clear password, salt and encryption algorithm.

Until, I have not found any issues with the code provided. A new post will be published to address this issue. Stay tuned.

Add comment

Comments (2)

R about 3 years ago Re:Migrate from Universal Membership Provider to ASP.NET Identity 2.1.0 - PasswordHarsher

HashPassword method can be constomed based on your previous settings.

Raymond about 3 years ago Re:Migrate from Universal Membership Provider to ASP.NET Identity 2.1.0 - PasswordHarsher

This issue has now been resolved, please refer to the following post: http://kosmisch.net/Blog/DotNetEssential/Archive/2015/2/1/aspnet-membership-default-password-hash-algorithms-in-net-4x-and-previous-versions.html Root cause: I was migrating from .NET 4.0 and the default hash algorithm is HMAC256 instead of SHA1.
In this Page