Migrating passwords in ASP.NET Core Identity with a custom PasswordHasher

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

As I discussed in my last post, the IPasswordHasher<> interface has two responsibilities:

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.

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.

Note that the PasswordHasher<> we derive from takes an optional IOptions<PasswordHasherOptions> 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:

Note, even if RehashPasswords 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 the HashPassword 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.:

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.

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.