The default password hasher for ASP.NET Core Identity uses PBKDF2 for password hashing. While PBKDF2 is not the worst choice, there are certainly better password hashing algorithms available to you, such as bcrypt, scrypt, and Argon2.
In this article, you’re going to see how password hashing works in ASP.NET Core Identity, how to write your own, and how you can take advantage of newer algorithms such as bcrypt and Argon2 by using some IPasswordHasher
implementations available on nuget.
ASP.NET Core Identity: IPasswordHasher
When creating passwords or validating passwords, the ASP.NET Core Identity user manager will call your implementation of IPasswordHasher
.
IPasswordHasher
is responsible for the hashing of passwords, including any salt generation.
public interface IPasswordHasher<TUser> where TUser : class
{
string HashPassword(TUser user, string password);
PasswordVerificationResult VerifyHashedPassword(
TUser user, string hashedPassword, string providedPassword);
}
To hash a password, IPasswordHasher
takes your user object and the plaintext password that needs to be hashed and returns the resulting password hash for you to store in your database.
It’s expected that this password hash contains everything required to validate the password.
In other words, it should include any versioning information, the salt, and, of course, the password hash itself.
To validate the password, IPasswordHasher
takes your user object, the hashed password for that user, and the plaintext password.
It will then run the provided plaintext password through the same hashing algorithm to see if it matches the provided hash.
If the password is correct, the password hasher will return PasswordVerificationResult.Success
.
Otherwise, it will return PasswordVerificationResult.Failure
.
Typically the user object passed into IPasswordHasher
is not used, but it’s there if you need it.
VerifyHashedPassword
can also return PasswordVerificationResult.SuccessRehashNeeded
.
This means that the password was valid, but the hash itself needs to be updated.
Maybe you upped the iteration count or migrated password hashing algorithms since the password hash was created.
Returning this status code will cause the user manager to call HashPassword
and update the user record.
Bcrypt Password Hasher
Let’s take a quick look at an example implementation of IPasswordHasher
that uses bcrypt:
using Microsoft.AspNetCore.Identity;
// Adapted from ScottBrady91.AspNetCore.Identity.BCryptPasswordHasher
// https://github.com/scottbrady91/ScottBrady91.AspNetCore.Identity.BCryptPasswordHasher
public class BCryptPasswordHasher<TUser> : IPasswordHasher<TUser> where TUser : class
{
public string HashPassword(TUser user, string password)
{
return BCrypt.Net.BCrypt.HashPassword(password, 12);
}
public PasswordVerificationResult VerifyHashedPassword(
TUser user, string hashedPassword, string providedPassword)
{
var isValid = BCrypt.Net.BCrypt.Verify(providedPassword, hashedPassword);
if (isValid && BCrypt.Net.BCrypt.PasswordNeedsRehash(hashedPassword, 12))
{
return PasswordVerificationResult.SuccessRehashNeeded;
}
return isValid ? PasswordVerificationResult.Success : PasswordVerificationResult.Failed;
}
}
You would then register this implementation using the scoped lifetime before or after the call to AddIdentity
:
services.AddScoped<IPasswordHasher<IdentityUser>, BCryptPasswordHasher<IdentityUser>>();
Calling HashPassword
on this bcrypt implementation creates a typical bcrypt password hash that looks something like the following.
Bcrypt hashes contain a version, work factor, salt, and the password hash itself.
$2a$12$fCFnLYqwvj3vR72SdqEbWOcUdr5AaYkKmaNt2M8CImpemCyrTqtT2
This implementation used the BCrypt.Net-Next library to hash and validate passwords using bcrypt. When validating passwords, it will check if a successful validation used a lower work factor than the current default (in this case, 12), and it will flag the password as valid but ready to rehash.
Default Password-Based Key Derivation Function (PBKDF2) Password Hasher
The default ASP.NET Core Identity password hasher uses PBKDF2 with HMAC-SHA256, a 128-bit salt, a 256-bit subkey, and 10,000 iterations.
Unlike the ASP.NET Identity 2 password hasher, this iteration count is now configurable, and realistically you’ll be looking at adding at least another zero to that iteration count. 10,000 iterations are so 2012.
PBKDF2 has generally been considered “good enough”, assuming you use a high number of iterations and a SHA2 family hash function. It is also FIPS compliant and recommended by NIST (you’ll be able to find FIPS-140 validated implementations). However, it is not so secure against newer attack vectors, such as GPU-based attacks, and as a result, it is often considered weak compared to alternatives such as bcrypt and Argon2.
In fact, to defend against modern attacks in 2021, cryptographers suggest that you need to use 310,000 iterations for PBKDF2-HMAC-SHA256. This would make PBKDF2 comparable to bcrypt work factor 8, and it is a little different from ASP.NET Identity’s default 10,000.
Alternatives to PBKDF2
Around the internet, you’ll typically find the following list of password hashing algorithm strength:
- Argon2 (winner of the Password Hashing Competition)
- bcrypt
- scrypt
- Catena, Lyra2, Makwa, or yescrypt (honourable mentions in the Password Hashing Competition)
- PBKDF2
With Argon2 leading the list but with the caveat that it’s still relatively “new”, and bcrypt & scrypt fighting for the number 2 spot (with bcrypt typically winning).
PBKDF2 still gets a mention if FIPS compliance is top of your priorities; however, it is starting to be retired from lists of recommended password hashing algorithms.
For up-to-date details and recommendations on password hashing, check out the OWASP Password Storage Cheat Sheet. At time of writing, this cheat sheet has just seen some excellent revisions and should be your go-to place for password hashing recommendations.
Password Hashing Algorithms in ASP.NET Core Identity
To improve the story for password hashing in ASP.NET Core, I’ve created some IPasswordHasher
implementations that use open source libraries to hash and verify passwords.
At the moment, these have been designed for new projects only, with no migration/rehash functionality for older algorithms.
If you’re keen on seeing this, let me know.
You can find installation instructions for each on their respective GitHub repositories. All options default to those used by their underlying crypto libraries. If some of these ever get outdated, feel free to create a pull request.
Bcrypt Password Hasher
NuGet | Source code & docs | OWASP recommendations
dotnet add package ScottBrady91.AspNetCore.Identity.BCryptPasswordHasher
The bcrypt password hasher uses Chris McKee’s BCrypt.Net, an updated and maintained version of the original BCrypt.Net port of jBCrypt.
This was the easiest password hasher to implement since the API makes sense, and the library has been kept up to date with .NET Standard. This currently defaults to a work factor of 11.
Bcrypt does come with a recommended character limit of 64-characters.
You can work around this using the EnhancedEntropy
, which will pre-hash passwords using SHA-384, before running the password through bcrypt; however, this is no longer recommended due to issues such as password shucking.
Using the default options, the bcrypt password hasher will result in a password hash that looks like this:
// plaintext: KingGeedorah
$2a$12$fCFnLYqwvj3vR72SdqEbWOcUdr5AaYkKmaNt2M8CImpemCyrTqtT2
Argon2 Password Hasher
NuGet | Source code & docs | OWASP recommendations
dotnet add package ScottBrady91.AspNetCore.Identity.Argon2PasswordHasher
The Argon2 password hasher uses libsodium-core, a .NET Standard port of libsodium-net, a C# wrapper around libsodium.
As of version 1.0.15, libsodium defaults to Argon2id. Argon2id is the recommended version of Argon2, based on the upcoming RFC. I currently do not have any plans to support Argon2i or Argon2d.
This implementation uses the recommended Argon2 strengths from libsodium. These don’t quite match up to the OWASP recommendations; however, the default “interactive” settings do roughly match:
- OWASP: m= 37MB, t=1,p=1 or m= 15MB, t=2, p=1
- libsodium: m= 33MB, t=4, p=1
Using the default options, the Argon2 password hasher will result in a password hash that looks like this:
// plaintext: KingGeedorah
$argon2id$v=19$m=32768,t=4,p=1$8G7bZn5h85dqZjBnFNWmlQ$Uh71LAwCel46jjWJdf5HhEORnv8Gh95iF7EsOE3cROw
Scrypt Password Hasher
NuGet | Source code & docs | OWASP recommendations
dotnet add package ScottBrady91.AspNetCore.Identity.ScryptPasswordHasher
The scrypt password hasher uses scrypt.net, a .NET port of the original implementation in C.
Scrypt is by far the least popular of these three algorithms, so I won’t bore you too much with this one 🙂.
// plaintext: KingGeedorah
$s2$16384$8$1$JV9fOUq7C4xnzavMBjrT0VNUe2FG5dtylt8iyrg0wGA=$AR767pjdpLKeEnNFmGZcNP5IgT1NGVx+C382y1HQN3w=
PBKDF2 Password Hasher
NuGet | Source code | OWASP recommendations
dotnet add package Microsoft.Extensions.Identity.Core
This is the default ASP.NET Core Identity password hasher that has a default iteration count of 10,000.
If you’re going to use the default password hasher, then you need to improve the iteration count. The current minimum recommendation is 310,000, but I suggest testing your production hardware to see what it can handle. Just don’t go lower.
// 310,000 is the minimum iteration count at time of writing
services.Configure<PasswordHasherOptions>(options => options.IterationCount = 310000);
Password migration
If you have existing password hashes from an old system and you want to upgrade your password hashing algorithm without resetting everyone’s password, I recommend either rehashing them all upfront or using a rolling migration approach. Both approaches will involve your ASP.NET website supporting the old password hashing algorithm for an amount of time.
Rolling migration
For a rolling migration, you would import your existing password hashes into your new ASP.NET Identity user store.
The first time a user logs into your system, you will have to validate their password using your old password hashing algorithm. If the password is valid, you can now update their stored password hash using your new password hashing algorithm. After all, in that brief window, you know the user’s plaintext password and have proven that it is valid.
This approach relies heavily on you timeboxing the use of the old hashing algorithm, as it leaves your infrequent users exposed in the event of a breach.
Rehashing password
Rehashing involves running all of your existing, insecure password hashes through your new password hashing algorithm. This will protect all passwords from day one.
Let’s say you have password hashes from the ASP.NET Core Identity 2 (PBKD2-HMAC-SHA1 using 1,000 iterations) and want to move to bcrypt. As part of an external process, you would rehash all of your existing password hashes using bcrypt before importing them into your ASP.NET Core Identity user store.
To verify these hashes, this means that your IPasswordHasher
implementation would now need to implement the following:
// bcrypt(pbkdf2($password, $legacySalt))
public PasswordVerificationResult VerifyHashedPassword(
TUser user, string hashedPassword, string providedPassword)
{
var legacyHash = Convert.ToBase64String(
VerifyLegacyHashedPassword(providedPassword, user.LegacySalt));
var isValid = BCrypt.Net.BCrypt.Verify(legacyHash, hashedPassword);
return isValid ? PasswordVerificationResult.Success : PasswordVerificationResult.Failed;
}
However, this wouldn’t become your main password hashing algorithm. You would still update the hash when the user next successfully logs in using just bcrypt.
Beware of password shucking
Let’s say you are in the unenviable position of having passwords stored using unsalted SHA-1 and wanted to move to bcrypt. You rehash all of your password hashes using bcrypt, leaving you with the password validation process of:
// bcrypt(sha1($password))
using (var legacyHasher = SHA1.Create())
{
byte[] legacyHashBytes = legacyHasher.ComputeHash(Encoding.UTF8.GetBytes(providedPassword));
string legacyHash = Convert.ToBase64String(legacyHashBytes);
bool isValid = BCrypt.Net.BCrypt.Verify(legacyHash, hashedPassword);
}
Unfortunately, this would leave you vulnerable to password shucking.
Password shucking is when the attacker declines attacking the bcrypt (your strong algorithm) and instead attacks the SHA-1 (your weak algorithm).
The attacker does this by searching past database breaches that contain password hashes that used the same weak hashing algorithm (SHA-1) as you. They will then take the SHA-1 password hash for a user that exists in both systems and validate the SHA-1 hash against the bcrypt hash. If it successfully validates, then it means that the user re-used passwords between systems.
Once the attacker knows the SHA-1 hash, they can start attacking that, which will be much faster than attacking the bcrypt hash. By finding the hash for the re-used password, the attack has effectively bypassed (shucked) the use of bcrypt.
If your old password hashing algorithm used a salt, then you do not need to worry about this attack vector. To rehash this type of insecure password hash, it is recommended that you use a pepper.
Check out my detailed explanation on password shucking to learn more or to watch the original DEFCON presentation on the attack.
Implementing password migration
To implement the rehashing functionality in your password hasher, you’ll need to know what hash type you currently have stored for the user so that you can run it against the correct algorithm.
If your password hashes are remarkably different, then it could be as simple as checking the first character. For example, a bcrypt password hash will always start with a dollar sign ($), whereas a SHA-1 hash will not.
A future-proof approach can be to use a flag. ASP.NET Identity already uses this approach, with ASP.NET Identity 2 hashes starting with 0x00
and ASP.NET Core Identity hashes starting with 0x01
.
Check out this example for extending an IPasswordHasher
implementation with custom flags.
Whichever approach you choose, just make sure you only support the old password hashes for a limited amount of time. The older hashes will be vulnerable until their corresponding user re-authenticates in your system. How at risk they are depends on the migration approach you take, the strength of your previous password hashing policy, and if your old password hashes were unsalted and therefore vulnerable to password shucking (assuming you go for the big-bang approach).
I recommend giving your users a few months to log in. After that, secure your system by deleting their old password hash and requiring the user to reset their password.
You can read more about password hashing upgrade approaches in “Upgrading existing password hashes” by Michal Špaček.
Future
Like these packages or have a feature request? Then let me know! I come back to these libraries with each major .NET release, and they’re seeing a decent amount of usage, so I am open to adding new features.