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.