Migrating Legacy Password Hashes

The Fallback Password Hasher Pattern

Posted by Stuart Greig on March 10, 2026.net 

The Problem: Legacy Password Migration

When migrating an existing application to ASP.NET Core Identity, you often face a critical challenge: your users' passwords are hashed using a legacy algorithm that differs from Identity's default password hasher.

You can't simply re-hash the passwords because:

• You don't have access to the plaintext passwords (and shouldn't!)

• Forcing all users to reset their passwords creates a poor user experience

• Mass password resets can trigger security concerns among users

The Solution: A Fallback Password Hasher

The FallbackPasswordHasher provides an elegant solution by supporting both legacy and modern password hashing schemes simultaneously.

How It Works

The hasher follows a simple but powerful pattern:

public override PasswordVerificationResult VerifyHashedPassword(
    TUser user, string hashedPassword, string providedPassword)
{
    // 1. Try to detect legacy format (pipe-delimited: hash|?|salt)
    string[] passwordProperties = hashedPassword.Split('|');
    if (passwordProperties.Length != 3)
    {
        // Modern format - use standard ASP.NET Core Identity hashing
        return base.VerifyHashedPassword(user, hashedPassword, providedPassword);
    }

    // 2. Legacy format detected - verify using old algorithm
    string passwordHash = passwordProperties[0];
    string salt = passwordProperties[2];
    
    if (String.Equals(EncryptPassword(providedPassword, salt), passwordHash, 
        StringComparison.CurrentCultureIgnoreCase))
    {
        // Success! Signal that password should be rehashed
        return PasswordVerificationResult.SuccessRehashNeeded;
    }

    return PasswordVerificationResult.Failed;
}

PasswordVerificationResult

The Magic: SuccessRehashNeeded

The key to seamless migration is returning PasswordVerificationResult.SuccessRehashNeeded. This tells ASP.NET Core Identity:

  • The password is correct - let the user in

  • The password needs to be rehashed using the modern algorithm

  • Save the new hash on the next password update

This means users are automatically migrated from legacy to modern hashing without any action required on their part.

Understanding the Legacy Algorithm

Many older ASP.NET systems used HMACSHA512 with custom salt handling. The EncryptPassword(string, string) method reconstructs this legacy process:

private string EncryptPassword(string pass, string salt)
{
    var bPassword = Encoding.Unicode.GetBytes(pass);
    var bSalt = Convert.FromBase64String(salt);

    using var hashAlgorithm = HashAlgorithm.Create("HMACSHA512");
    
    if (hashAlgorithm is KeyedHashAlgorithm keyedHashAlgorithm)
    {
        // Adjust the key to match the salt length
        keyedHashAlgorithm.Key = AdjustKeyToSaltLength(bSalt, 
            keyedHashAlgorithm.Key.Length);
        hash = keyedHashAlgorithm.ComputeHash(bPassword);
    }
    
    return Convert.ToBase64String(hash);
}

EncryptPassword

The Key Adjustment Challenge

One subtle complexity is that HMACSHA512 has a fixed key length (128 bytes), but legacy salts might be any length. The AdjustKeyToSaltLength(byte[], int) method handles three scenarios:

  • Exact match: Use the salt as-is

  • Salt too long: Truncate to the required length

  • Salt too short: Repeat the salt until it fills the key

private static byte[] AdjustKeyToSaltLength(byte[] salt, int requiredKeyLength)
{
    if (salt.Length == requiredKeyLength)
        return salt;

    var adjustedKey = new byte[requiredKeyLength];

    if (requiredKeyLength < salt.Length)
    {
        Buffer.BlockCopy(salt, 0, adjustedKey, 0, requiredKeyLength);
    }
    else
    {
        // Repeat salt to fill the required key length
        for (var i = 0; i < requiredKeyLength;)
        {
            var len = Math.Min(salt.Length, requiredKeyLength - i);
            Buffer.BlockCopy(salt, 0, adjustedKey, i, len);
            i += len;
        }
    }

    return adjustedKey;
}

AdjustKeyToSaltLength

Implementation Benefits

Zero Downtime Migration

Users continue logging in normally during and after the migration. No maintenance windows required.

Gradual Security Improvement

As users log in, they're automatically upgraded to the more secure ASP.NET Core Identity password hashing algorithm (PBKDF2 with HMAC-SHA256 by default).

Backward Compatibility

The system continues to support legacy hashes indefinitely for users who don't log in frequently.

Fail-Safe Design

If the legacy format isn't detected, the hasher falls back to the standard Identity implementation, maintaining forward compatibility.

When Should You Use This Pattern?

Consider implementing a fallback password hasher when:

  • Migrating from legacy .NET Framework applications to .NET Core

  • Adopting ASP.NET Core Identity in an existing system

  • Upgrading from custom authentication to Identity

  • Consolidating multiple systems with different hashing schemes

Security Considerations

While this pattern is practical, keep in mind:

  • Legacy algorithms may be weaker than modern standards. Monitor and eventually deprecate support for the legacy format.

  • Document the migration timeline. Consider forcing password resets for users who haven't logged in after a reasonable period (e.g., 6-12 months).

  • The modern Identity hasher uses PBKDF2 with adaptive iteration counts, making it much more resistant to brute-force attacks.

Conclusion

The Fallback Password Hasher pattern demonstrates how thoughtful software design can bridge the gap between legacy systems and modern security practices. By supporting both old and new hashing schemes simultaneously, you can migrate users seamlessly while improving security over time.

This approach respects your users' experience while maintaining the highest security standards for new authentications a win-win for everyone involved.

Want to see my full CV and portfolio?