Skip to main content

A drop-in replacement for the standard Identity hasher to be backwards compatible with existing bcrypt hashes. New passwords will be hashed with Identity V3.

/// <summary>
/// A drop-in replacement for the standard Identity hasher to be backwards compatible with existing bcrypt hashes
/// New passwords will be hashed with Identity V3
/// </summary>
public class BCryptPasswordHasher<TUser> : PasswordHasher<TUser> where TUser : class  
{
    readonly BCryptPasswordSettings _settings;
    public BCryptPasswordHasher(BCryptPasswordSettings settings)
    {
        _settings = settings;
    }

    public override PasswordVerificationResult VerifyHashedPassword(TUser user, string hashedPassword, string providedPassword)
    {
        if (hashedPassword == null) { throw new ArgumentNullException(nameof(hashedPassword)); }
        if (providedPassword == null) { throw new ArgumentNullException(nameof(providedPassword)); }

        byte[] decodedHashedPassword = Convert.FromBase64String(hashedPassword);

        // read the format marker from the hashed password
        if (decodedHashedPassword.Length == 0)
        {
            return PasswordVerificationResult.Failed;
        }

        // ASP.NET Core uses 0x00 and 0x01, so we start at the other end
        if (decodedHashedPassword[0] == 0xFF)
        {
            if (VerifyHashedPasswordBcrypt(decodedHashedPassword, providedPassword))
            {
                // This is an old password hash format - the caller needs to rehash if we're not running in an older compat mode.
                return _settings.RehashPasswords
                    ? PasswordVerificationResult.SuccessRehashNeeded
                    : PasswordVerificationResult.Success;
            }
            else
            {
                return PasswordVerificationResult.Failed;
            }
        }

        return base.VerifyHashedPassword(user, hashedPassword, providedPassword);
    }

    private static bool VerifyHashedPasswordBcrypt(byte[] hashedPassword, string password)
    {
        if (hashedPassword.Length < 2)
        {
            return false; // bad size
        }

        //convert back to string for BCrypt, ignoring first byte
        var storedHash = Encoding.UTF8.GetString(hashedPassword, 1, hashedPassword.Length - 1);

        return BCrypt.Verify(password, storedHash);
    }
}