In my last post I provided an overview of the ASP.NET Core Identity
PasswordHasher<>
implementation, and how it enables backwards compatibility between password hashing algorithms. In this post, I'll create a custom implementation of IPasswordHasher<>
that we can use to support other password formats. We'll use this to migrate existing password hashes created using BCrypt to the default ASP.NET Core Identity hashing format.Disclaimer: You should always think carefully before replacing security-related components, as a lot of effort goes into making the default components secure by default. This article solves a specific problem, but you should only use it if you need it!
As in the last post, the code I'm going to show is based on the ASP.NET Core 2.0 release. You can view the full source code for ASP.NET Core Identity on GitHub here.
Background
- Hash a password so it can be stored in a database
- Verify a provided plain-text password matches a previously stored hash
In this post I'm focusing on the scenario where you want to add ASP.NET Core Identity to an existing app, or you already have a database that contains usernames and password hashes.
The problem is that your password hashes are stored using a hash format that isn't compatible with ASP.NET Core Identity. In this example, I'm going to assume your passwords are hashed using BCrypt, using the excellent BCrypt.Net library, but you could easily apply it to any other hashing algorithm. The
BCryptPasswordHasher<>
we create will allow you to verify existing password hashes created using BCrypt, as well as hashes created by ASP.NET Core Identity
Note that I won't be implementing the algorithms to hash new passwords with BCrypt. Instead, the hasher will create new hashes using the default ASP.NET Core Identity hash function, by deriving from the default
PasswordHasher<>
implementation. Also, when a user logs in and verifies their password, the hasher will optionally re-hash the password using the ASP.NET Core Identity default hash function. That way, hashes will slowly migrate from the legacy hash function to the default hash function.As a reminder, the ASP.NET Core Identity (v3) password hasher mode uses the PBKDF2 algorithm with HMAC-SHA256, 128-bit salt, 256-bit subkey, and 10,000 iterations.
If you want to keep all your passwords using BCrypt, then you could implement
IPasswordHasher<>
directly. That would actually make the code simpler, as you won't need to handle multiple hash formats, but I specifically wanted to migrate our passwords to the NIST-recommended PBKDF2 algorithm, hence this hybrid-solution.The implementation
As discussed in my last post, the default
PasswordHasher<>
implementation already handles multiple hashing formats, namely two different versions of PBKDF2. It does this by storing a single-byte "format-marker" along with the password hash. The whole combination is then base64 encoded and stored in the database as a string.
When a password needs to be verified compared to a stored hash, the hash is read from the database, decoded from base64 to bytes, and the first byte is inspected. If the byte is a
0x00
, the password hash was created using v2 of the hashing algorithm. If the byte is a 0x01
, then v3 was used.
We maintain compatibility with the
PasswordHasher
algorithm by storing our own custom format marker in the first byte of the password hash, in a similar fashion. 0x00
and 0x01
are already taken, so I chose 0xFF
as it seems like it should be safe for a while!
When a password hash and plain-text password are provided for verification, we follow a similar approach to the default
PasswordHasher<>
. We convert the password from Base64 into bytes, and examine the first byte. If the hash starts with 0xFF
then we have a BCrypyt hash. If it starts with something else, then we just delegate the call to the base PasswordHasher<>
implementation we derive from.///
/// 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
///
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);
}
}
Note that the
PasswordHasher<>
we derive from takes an optional IOptions
object in its constructor. If you want to provide a custom PasswordHasherOptions
object to the base implementation then you could add that to the BCryptPasswordHasher<>
constructor. If you don't, the default options will be used instead.
Instead, I provide a
BCryptPasswordSettings
parameter in the constructor. This controls whether existing BCrypt passwords should be re-hashed with the ASP.NET Core Identity hashing algorithm, or whether they should be left as BCrypt passwords, based on the RehashPasswords
property:public class BCryptPasswordSettings
{
public bool RehashPasswords {get; set; }
}
Note, even ifRehashPasswords
is false, new passwords will be created using the identity v3 PBKDF2 format. If you want to ensure all your passwords are kept in the BCrypt format, you will need to override theHashPassword
method too.
You can replace the default
PasswordHasher<>
implementation by registering the BCryptPasswordHasher
in Startup.ConfigureServices()
. Just make sure you register it before the call to AddIdentity
, e.g.:public void ConfigureServices(IServiceCollection services)
{
// must be added before AddIdentity()
services.AddScoped<IPasswordHasher<ApplicationUser>, BCryptPasswordHasher<ApplicationUser>>();
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddMvc();
}
By registering your custom implementation of
IPasswordHasher<>
first, the default implementation PasswordHasher<>
will be skipped in the call to AddIdentity()
, avoiding any issues due to multiple registered instances.
Converting stored BCrypt passwords to support the BCryptPasswordHasher
The mechanism of extending the default
PasswordHasher<>
implementation I've shown in this post hinges on the ability to detect the hashing algorithm by checking the first byte of the stored password hash. That means you'll need to update your stored BCrypt hashes to include the first-byte format marker and to be Base64 encoded.
Exactly how you choose to do this is highly dependent on how and where your passwords are stored, but I've provided a basic function below that takes an existing password hash stored as a string, a
byte
format-marker, and produces a string in a format compatible with the BCryptPasswordHasher
.public static class HashHelper
{
public static string ConvertPasswordFormat(string passwordHash, byte formatMarker)
{
var bytes = Encoding.UTF8.GetBytes(passwordHash);
var bytesWithMarker = new byte[bytes.Length + 1];
bytesWithMarker[0] = formatMarker;
bytes.CopyTo(bytesWithMarker, 1);
return Convert.ToBase64String(bytesWithMarker);
}
}
For our
BCryptPasswordHasher
, we would add the format marker to an existing BCrypt hash using:var newHash = HashHelper.ConvertPasswordFormat(bcryptHash, 0xFF);
Summary
In this post I showed how you could extend the default ASP.NET Core Identity
PasswordHasher<>
implementation to support additional password formats. This lets you verify hashes created using a legacy format (BCrypt in this example), and update them to use the default Identity password hashing algorithm.