blog post image
Andrew Lock avatar

Andrew Lock

~7 min read

Exploring the ASP.NET Core Identity PasswordHasher

In this post I'll look at some of the source code that makes up the ASP.NET Core Identity framework. In particular, I'm going to look at the PasswordHasher<T> implementation, and how it handles hashing user passwords for verification and storage. You'll also see how it handles updating the hashing algorithm used by your app, while maintaining backwards compatibility with existing hash functions.

I'll start by describing where password hashing fits into ASP.NET Core Identity overall, and the functionality provided by the IPasswordHasher<TUser> interface. Then I'll provide a high-level overview of the PasswordHasher<T> implementation, before finally digging into a few details.

In the next post, I'll show how to create a custom IPasswordHasher<TUser> implementation, so you can integrate an existing user database into ASP.NET Core Identity. This will let you use your existing password hashes without having to reset every user's password, and optionally allow you to migrate them to the suggested ASP.NET Core Identity hash format.

ASP.NET Core Identity and password hashing

You're no doubt familiar with the "username and password" authentication flow used by the vast majority of web apps. ASP.NET Core Identity uses this flow by default (I'm going to ignore third-party login providers for the purposes of this article).

When a user registers with the app, they provide a username and password (and any other required information). The app will create a hash of the password, and store it in the database along with the user's details.

Storing a hashed password in the database

A hash is a one way function, so given the password you can work out the hash, but given the hash you can't get the original password back. For security reasons, the characteristics of the hash function are important; in particular, the hash function should be relatively costly to compute, so that if your database of password hashes were to be compromised, it would take a long time to crack them.

Important You should never store a user's password directly in a database (or anywhere else). Also, you should never store the password in an encrypted format, in which you can recover the password. Instead, passwords should only ever be stored as a hash of the original, using a strong cryptographic hash function designed for this purpose.

When it comes to logging in, users POST their username and password to the app. The app will take the identifier and attempt to find an existing account in its database. If it finds the account, it retrieves the stored password hash associated with the account.

The app then hashes the password that was submitted, and compares the two hashes. If the hashes match, then the password is correct, and the user can be authenticated. If the hashes don't match, the user provided the wrong password, and should be rejected.

Logging in to a web app with username and password

The IPasswordHasher<TUser> interface

With this typical flow, there are two different scenarios in which we need to hash a password:

  • When the user registers - to create the password hash that will be stored in the database
  • When the user logs in - to hash the provided password and compare it to the stored hash

These two scenarios are closely related, and are encapsulated in the IPasswordHasher<TUser> interface in ASP.NET Core Identity. The Identity framework is designed to be highly extensible, so most of the key parts of infrastructure are exposed as interfaces, with default implementations that are registered by default.

The IPasswordHasher<TUser> is one such component. It's used in the two scenarios described above and exposes a method for each, as shown below.

Note: In this post I'm going to show the source code as it exists in the ASP.NET Core 2.0 release, by using the rel/2.0.0 tag in the Identity Github repo. You can view the full source for the IPasswordHasher<TUser> here.

public interface IPasswordHasher<TUser> where TUser : class
{
    string HashPassword(TUser user, string password);

    PasswordVerificationResult VerifyHashedPassword(
        TUser user, string hashedPassword, string providedPassword);
}

The IPasswordHasher<TUser> interface is a generic interface, where the generic parameter is the type representing a User in the system - often a class deriving from IdentityUser.

When a new user registers, the Identity framework calls HashPashword() to hash the provided password, before storing it in the database. When a user logs in, the framework calls VerifyHashedPassword() with the user account, the stored password hash, and the password provided by the user.

Pretty self explanatory right? Let's take a look at the default implementation of this interface.

The default PasswordHasher<TUser> implementation

The default implementation in the Identity framework is the PasswordHasher<TUser> class (source code). This clas is designed to work with two different hashing formats:

  • ASP.NET Identity Version 2: PBKDF2 with HMAC-SHA1, 128-bit salt, 256-bit subkey, 1000 iterations
  • ASP.NET Core Identity Version 3: PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations

The PasswordHasher<TUser> class can hash passwords in both of these formats, as well as verify passwords stored in either one.

Verifying hashed passwords

When a password is provided that you need to compare against a hashed version, the PasswordHasher<TUser> needs to know which format was used to hash the password. To do this, it preppends a single byte to the hash before storing it in the database (Base64 encoded).

When a password needs to be verified, the hasher checks the first byte, and uses the appropriate algorithm to hash the provided password.

public virtual PasswordVerificationResult VerifyHashedPassword(TUser user, string hashedPassword, string providedPassword)
{
    // Convert the stored Base64 password to bytes
    byte[] decodedHashedPassword = Convert.FromBase64String(hashedPassword);

    // The first byte indicates the format of the stored hash
    switch (decodedHashedPassword[0])
    {
        case 0x00:
            if (VerifyHashedPasswordV2(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 (_compatibilityMode == PasswordHasherCompatibilityMode.IdentityV3)
                    ? PasswordVerificationResult.SuccessRehashNeeded
                    : PasswordVerificationResult.Success;
            }
            else
            {
                return PasswordVerificationResult.Failed;
            }

        case 0x01:
            if (VerifyHashedPasswordV3(decodedHashedPassword, providedPassword))
            {
                return PasswordVerificationResult.Success;
            }
            else
            {
                return PasswordVerificationResult.Failed;
            }

        default:
            return PasswordVerificationResult.Failed; // unknown format marker
    }
}

When the password is verified, the hasher returns one of three results:

  • PasswordVerificationResult.Failed - the provided password was incorrect
  • PasswordVerificationResult.Success - the provided password was correct
  • PasswordVerificationResult.SuccessRehashNeeded - the provided password was correct, but the stored hash should be updated

The switch statement in VerifyHashedPassword() has two main cases - one for Identity v2 hashing, and one for Identity v3 hashing. If the password has been stored using the older v2 hashing algorithm, and the provided password is correct, then the hasher will either return Success or SuccessRehashNeeded.

Which result it chooses is based on the PasswordHasherCompatibilityMode which is passed in via an IOptions<PasswordHasherOptions> object. This lets you choose whether or not to rehash the older passwords; if you need the password hashes to remain compatible with Identity v2, then you might want to keep the older hash format.

As well as verifying hashed passwords, the PasswordHasher<TUser> is used to create new hashes.

Hashing new passwords

The HashPassword() function is called when a new user registers, and the password needs hashing before it's stored in the database. It's also called after an old v2 format password hash is verified, and needs rehashing.

private readonly RandomNumberGenerator _rng;

public virtual string HashPassword(TUser user, string password)
{
    if (_compatibilityMode == PasswordHasherCompatibilityMode.IdentityV2)
    {
        return Convert.ToBase64String(HashPasswordV2(password, _rng));
    }
    else
    {
        return Convert.ToBase64String(HashPasswordV3(password, _rng));
    }
}

The hashes are generated in the correct format, depending on the PasswordHasherCompatibilityMode set in the options, which is then Base64 encoded before it's stored in the database.

I won't dwell on the hashing algorithms themselves too much, but as an example, the HashPasswordV2 function is shown below. Of particular note is the line 4th from the bottom, where the "first byte" format marker is set to 0x00:

private static byte[] HashPasswordV2(string password, RandomNumberGenerator rng)
{
    const KeyDerivationPrf Pbkdf2Prf = KeyDerivationPrf.HMACSHA1; // default for Rfc2898DeriveBytes
    const int Pbkdf2IterCount = 1000; // default for Rfc2898DeriveBytes
    const int Pbkdf2SubkeyLength = 256 / 8; // 256 bits
    const int SaltSize = 128 / 8; // 128 bits

    // Produce a version 2 text hash.
    byte[] salt = new byte[SaltSize];
    rng.GetBytes(salt);
    byte[] subkey = KeyDerivation.Pbkdf2(password, salt, Pbkdf2Prf, Pbkdf2IterCount, Pbkdf2SubkeyLength);

    var outputBytes = new byte[1 + SaltSize + Pbkdf2SubkeyLength];
    outputBytes[0] = 0x00; // format marker
    Buffer.BlockCopy(salt, 0, outputBytes, 1, SaltSize);
    Buffer.BlockCopy(subkey, 0, outputBytes, 1 + SaltSize, Pbkdf2SubkeyLength);
    return outputBytes;
}

The "first byte" format marker is the byte that is used by the VerifyHashedPassword() function to identify the format of the stored password. A format marker of 0x00 indicates that the password is stored in the v2 format; a value of 0x01 indicates the password is stored in the v3 format. In the next post, we'll use this to extend the class and support other formats too.

That's pretty much all there is to the PasswordHasher<TUser> class. If you'd like to see more details of the hashing algorithms themselves, I suggest checking out the source code.

Summary

The IPasswordHasher<TUser> is used by the ASP.NET Core Identity framework to both hash passwords for storage, and to verify that a provided password matches a stored hash. The default implementation PasswordHasher<TUser> supports two different formats of hash function: one used by Identity v2, and a stronger version used by ASP.NET Core Identity v3.

If you need to keep the passwords in the v2 format you can set the PasswordHasherCompatibilityMode on the IOptions<PasswordHasherOptions> object in the constructor to IdentityV2. If you use IdentityV3 instead, new passwords will be hashed with the stronger algorithm, and when old passwords are verified, they will be rehashed with the newer, stronger algorithm.

Andrew Lock | .Net Escapades
Want an email when
there's new posts?