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.
Let’s dive in.
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:
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:
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 callsVerifyHashedPasswordV2()
- If it is hashed using
IdentityV3
, it callsVerifyHashedPasswordV3()
- 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 correctPasswordVerificationResult.SuccessRehashNeeded
if the password is correct but we should update the hash to a new algorithmPasswordVerificationResult.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.