Migrate from Universal Membership Provider to ASP.NET Identity 2.1.0 - PasswordHarsher
In my previous post, I demonstrated how to migrate from ASP.NET Universal Membership Provider to ASP.NET Identity 2.1.0.
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.
info Last modified by Raymond 6 years ago
copyright
This page is subject to Site terms.
comment Comments
hide_source
Anonymous
R
access_time
10 years ago
link
more_vert
HashPassword method can be constomed based on your previous settings.
hide_source
Anonymous
Raymond
access_time
10 years ago
link
more_vert
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.