In this article, we will delve into the default ASP.NET Core Identity Password Hasher.

We will focus on understanding how it transforms plain text passwords into secure hashes and highlighting its security features.

To download the source code for this article, you can visit our GitHub repository.

Let’s dive in.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

How the Password Hashing Process Works

When discussing password hashing, we typically consider two scenarios.

Firstly, when a user sets up a new password during registration or updates an existing one, we must hash it before storing it in a database.

Let’s take a look at how such a process typically works:

password-hasher-hash-password-flow

The user enters their details into the form, and the service responsible for user management processes the registration request.

At this point, the service sends the password to the password hasher to hash it before storing it in the database.

Secondly, to authenticate the user, we need to ensure that the hash of the password entered at login matches the hashed password stored in the database:

password-hasher-verify-password-flow

The user enters their credentials into the form, and the user management service processes the login request.

In this case, the service retrieves the user’s hashed password from the database and uses the password hasher to compare the stored hashed password with the hash of the password entered by the user.

Fortunately, ASP.NET Core Identity provides a solution for managing user authentication, with password security at its core. The framework’s default password hashing mechanism is implemented through the PasswordHasher<TUser> class, which uses the Password-Based Key Derivation Function 2, also known as PBKDF2.

Hashing Passwords With ASP.NET Core Identity Password Hasher

Now, let’s see how the PasswordHasher<TUser> class can secure passwords in our applications.

IPasswordHasher<TUser> Interface

First, let’s have a look at the IPasswordHasher<TUser> interface:

public interface IPasswordHasher<TUser> where TUser : class
{
    string HashPassword(TUser user, string password);
    PasswordVerificationResult VerifyHashedPassword(TUser user, string hashedPassword,
        string providedPassword);
}

This is a generic interface that takes a class representing the user in our system as a generic TUser parameter. It provides two methods that address the previously mentioned scenarios.

We use the HashPassword() method to secure a password. It requires two parameters: the user parameter of type TUser and the password we want to hash.

Next, we have the VerifyHashedPassword() method that is used for password validation. It takes three parameters: the user parameter of TUser type, along with two strings representing the hashed and tested passwords.

The PasswordHasher<TUser> Constructor

Now, let’s have a look at the PasswordHasher<TUser> class constructor:

public PasswordHasher(IOptions<PasswordHasherOptions>? optionsAccessor = null)
{
    var options = optionsAccessor?.Value ?? DefaultOptions;

    _compatibilityMode = options.CompatibilityMode;
    switch (_compatibilityMode)
    {
        case PasswordHasherCompatibilityMode.IdentityV2:
            // nothing else to do
            break;

        case PasswordHasherCompatibilityMode.IdentityV3:
            _iterCount = options.IterationCount;
            if (_iterCount < 1)
            {
                throw new InvalidOperationException(Resources.InvalidPasswordHasherIterationCount);
            }
            break;

        default:
            throw new InvalidOperationException(Resources.InvalidPasswordHasherCompatibilityMode);
    }

    _rng = options.Rng;
}

To start, the PasswordHasher<TUser> class loads the configuration from the provided PasswordHasherOptions instance.

However, the most interesting part is the switch statement which shows us that the PasswordHasher<TUser> class can work in two modes.

First, IdentityV2 mode, which is compatible with ASP.NET Identity versions 1 and 2. According to documentation, this mode supports PBKDF2 with 128-bit salt, 256-bit subkey, 1000 iterations, and HMAC-SHA1.

Second, IdentityV3 mode, which is compatible with ASP.NET Identity version 3. Likewise, this mode supports PBKDF2 with 128-bit salt and 256-bit subkey. However, this mode supports 100000 iterations and HMAC-SHA512.

Hashing Passwords With the HashPassword Method

Next, let’s have a look at a HashPassword method:

public virtual string HashPassword(TUser user, string password)
{
    ArgumentNullThrowHelper.ThrowIfNull(password);

    if (_compatibilityMode == PasswordHasherCompatibilityMode.IdentityV2)
    {
        return Convert.ToBase64String(HashPasswordV2(password, _rng));
    }
    else
    {
        return Convert.ToBase64String(HashPasswordV3(password, _rng));
    }
}

The HashPassword() method takes two parameters: a user of the generic TUser type and a password as a string.

It’s important to note that the default implementation does not use the user parameter. That is because users can be represented differently in various systems. However, in custom implementations of the IPasswordHasher<TUser> interface, user details could be incorporated into the hashing process.

Furthermore, depending on the compatibility mode set, the method then calls either HashPasswordV2 or HashPasswordV3, and returns the result from the appropriate version coded using Base64.

Hashing Passwords With the HashPasswordV2 Method

Firstly, we will start with HashPasswordV2() method:

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

    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;
    Buffer.BlockCopy(salt, 0, outputBytes, 1, SaltSize);
    Buffer.BlockCopy(subkey, 0, outputBytes, 1 + SaltSize, Pbkdf2SubkeyLength);
    return outputBytes;
}

The HashPasswordV2() method begins by setting constants critical in the hashing process.

It utilizes the HMACSHA1 pseudo-random function, as specified by the Pbkdf2Prf constant.

It also defaults to 1000 iterations as defined by Pbkdf2IterCount constant. In short, this iteration count represents the number of times the algorithm applies the key derivation, making brute-force attacks increasingly computationally expensive as the count increases.

Additionally, the method initializes a byte array for the salt with a length determined by the SaltSize constant. Then it fills this array with random bytes to ensure salt uniqueness for each hashing operation.

Then, the KeyDerivation.Pbkdf2 function generates a cryptographic key by applying the HMACSHA1 function to the password and salt over the specified number of iterations.

Finally, it prepares the output byte array to store the final hash. The first byte of the array is set as a format marker (0x00) to identify this as a Version 2 hash. Then, it copies the salt and subkey into the array.

HashPasswordV3 Method to Hash Passwords

Next, let’s analyze the HashPasswordV3() method:

private byte[] HashPasswordV3(string password, RandomNumberGenerator rng) 
{ 
    return HashPasswordV3(
        password, rng,
        prf: KeyDerivationPrf.HMACSHA512,
        iterCount: _iterCount,
        saltSize: 128 / 8,
        numBytesRequested: 256 / 8);
}

private static byte[] HashPasswordV3(string password, RandomNumberGenerator rng, 
    KeyDerivationPrf prf, int iterCount, int saltSize, int numBytesRequested)
{
    byte[] salt = new byte[saltSize];
    rng.GetBytes(salt);
    byte[] subkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, numBytesRequested);

    var outputBytes = new byte[13 + salt.Length + subkey.Length];
    outputBytes[0] = 0x01;
    WriteNetworkByteOrder(outputBytes, 1, (uint)prf);
    WriteNetworkByteOrder(outputBytes, 5, (uint)iterCount);
    WriteNetworkByteOrder(outputBytes, 9, (uint)saltSize);
    Buffer.BlockCopy(salt, 0, outputBytes, 13, salt.Length);
    Buffer.BlockCopy(subkey, 0, outputBytes, 13 + saltSize, subkey.Length);
    return outputBytes;
}

We can see that the HashPasswordV3() implementation is a set of two methods.

The first method acts as an interface that accepts a password and a random number generator instance. It then delegates to a more detailed implementation, specifying several additional parameters.

In short, it sets the pseudo-random function to HMACSHA512, the number of iterations the hash function will cycle through, the size of the salt in bytes, and the number of bytes requested for the resulting hash.

It is worth pointing out that this approach allows us to customize the hashing process, as parameters such as salt size or key length are not hardcoded.

The second method begins by generating a salt using the specified saltSize in a similar fashion as HashPasswordV2() method.

Next, the KeyDerivation.Pbkdf2 function generates a cryptographic key by applying the HMACSHA512 function to the password and salt over the specified number of iterations.

The method then prepares an output byte array to store the complete hash. It sets the first byte of the array as a format marker (0x01) to mark this as a Version 3 hash.

Lastly, it stores the pseudo-random function, iteration count, and salt size in the array in network byte order followed by the salt and subkey.

Verifying Passwords With the ASP.NET Core Identity Password Hasher

Now that we know how the hashing process works, let’s see how to validate the password:

public virtual PasswordVerificationResult VerifyHashedPassword(TUser user, string hashedPassword, 
    string providedPassword)
{
    ArgumentNullThrowHelper.ThrowIfNull(hashedPassword);
    ArgumentNullThrowHelper.ThrowIfNull(providedPassword);

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

    if (decodedHashedPassword.Length == 0)
    {
        return PasswordVerificationResult.Failed;
    }
    switch (decodedHashedPassword[0])
    {
        case 0x00:
            if (VerifyHashedPasswordV2(decodedHashedPassword, providedPassword))
            {
                return (_compatibilityMode == PasswordHasherCompatibilityMode.IdentityV3)
                    ? PasswordVerificationResult.SuccessRehashNeeded
                    : PasswordVerificationResult.Success;
            }
            else
            {
                return PasswordVerificationResult.Failed;
            }
        case 0x01:
            if (VerifyHashedPasswordV3(decodedHashedPassword, providedPassword,
                out int embeddedIterCount, out KeyDerivationPrf prf))
            {
                if (embeddedIterCount < _iterCount)
                {
                    return PasswordVerificationResult.SuccessRehashNeeded;
                }

                if (prf == KeyDerivationPrf.HMACSHA1 || prf == KeyDerivationPrf.HMACSHA256)
                {
                    return PasswordVerificationResult.SuccessRehashNeeded;
                }

                return PasswordVerificationResult.Success;
            }
            else
            {
                return PasswordVerificationResult.Failed;
            }
        default:
            return PasswordVerificationResult.Failed;
    }
}

As we can see, the method starts by validating that neither the hashed password nor the provided password is null.

Then, it decodes the hashed password from a Base64 string into a byte array and checks if the decoded hashed password is empty returning a PasswordVerificationResult.Failed if true, indicating an invalid or corrupted hash.

Next, it reads the format marker from the beginning of the hashed password to determine the version of the hashing algorithm.

Finally, depending on the format marker one of the flows is executed:

  • If the password is hashed using IdentityV2 mode, it calls VerifyHashedPasswordV2() 
  • If it is hashed using IdentityV3, it calls VerifyHashedPasswordV3()
  • In the case of unknown format markers, it indicates an unrecognized or corrupted hash format

In general, both VerifyHashedPasswordV2() and VerifyHashedPasswordV3() operate on the same principle. They hash the provided password using the original hashed password’s salt and hashing parameters and then compare the resulting strings.

Additionally, if the password is correct it checks if the system is running in a compatibility mode that requires rehashing.

When verifying the password, the hasher returns one of three values:

  • PasswordVerificationResult.Success if the password is correct
  • PasswordVerificationResult.SuccessRehashNeeded if the password is correct but we should update the hash to a new algorithm
  • PasswordVerificationResult.Failed if the password is incorrect

Security Features of the ASP.NET Core Identity Password Hasher

The PasswordHasher<TUser> offers a few security features contributing to the security of our applications.

Salting

Salting involves adding a random string to each password before it undergoes the hashing process.

This means that even if two users choose the same password, their stored password hashes will be distinct due to the unique salts used. By doing so, salting prevents attackers from successfully using precomputed hash tables known as rainbow tables.

Cryptographic Hash Function

The default configuration of PasswordHasher<TUser> uses the Password-Based Key Derivation Function 2 with HMACSHA256.

This hash function extends the time and computational resources required to generate a hash, making it difficult and time-consuming for attackers to crack passwords.

Iterative Hashing

As mentioned earlier, while the number of iterations enhances security against brute-force attacks, it also impacts the performance of the hashing operation.

Let’s verify this using the BenchmarkDotNet library:

| Method                       | IterationCount | Mean         | Error        | StdDev      | Median       |
|----------------------------- |--------------- |-------------:|-------------:|------------:|-------------:|
| PasswordHasherWithIdentityV2 | 1000           |     546.6 us |     22.54 us |    62.45 us |     514.5 us |
| PasswordHasherWithIdentityV3 | 1000           |     688.8 us |     24.06 us |    68.25 us |     654.5 us |
| PasswordHasherWithIdentityV3 | 10000          |   6,426.1 us |    140.89 us |   390.39 us |   6,417.1 us |
| PasswordHasherWithIdentityV3 | 100000         |  60,038.0 us |  1,162.89 us | 1,087.77 us |  59,638.7 us |
| PasswordHasherWithIdentityV3 | 1000000        | 598,615.3 us | 11,186.67 us | 9,916.69 us | 596,388.8 us |

From our results, the PasswordHasherV2() executes faster than the PasswordHasherV3() when using the same number of iterations, but PasswordHasherV3() employs a more secure cryptographic hashing function.

Furthermore, raising the number of iterations proportionally increases the execution time of PasswordHasherV3() method. It’s important to note that the default setting for iterations in PasswordHasherV3() is 100,000.

Best Practices for Password Security in ASP.NET Core

Finally, it’s worth pointing out the critical importance of following recognized best practices when dealing with passwords, some of which we covered in another article.

Firstly, we should use the latest version of ASP.NET Core Identity, as it includes the most up-to-date security features and fixes. Regularly updating our framework ensures that we benefit from the latest improvements and security patches.

Secondly, while ASP.NET Core Identity defaults to strong password hashing algorithms like PBKDF2 with HMACSHA256 or HMACSHA512, we should avoid reducing the default number of iterations used for hashing. These iterations make the hashing process slower, significantly increasing the difficulty for attackers attempting to crack the passwords using brute-force methods.

Moreover, we should enforce strong password policies. This includes requiring passwords that combine upper and lowercase letters, numbers, and special characters, as well as setting a minimum password length. These measures help prevent common password attacks such as dictionary attacks.

Incorporating multi-factor authentication requires users to provide two or more verification factors to gain access, significantly reducing the risk of unauthorized access, even if someone compromises a password.

Lastly, regularly educating users about safe password practices such as avoiding the reuse of passwords across different sites and recognizing phishing attempts can help maintain the system’s security.

Conclusion

In conclusion, understanding the internal workings of ASP.NET Core Identity’s default password hasher is beneficial for maintaining secured web applications.

Through our detailed exploration of PasswordHasherV2 and PasswordHasherV3, we’ve seen how different versions apply cryptographic methods to enhance password protection.

PasswordHasherV3, using a more secure cryptographic function and higher default iteration count, offers superior security, albeit with a longer execution time than its predecessor.

As developers, we must stay informed and proactive about implementing the most secure authentication methods, ensuring our applications remain robust against threats and safeguard user data effectively.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!