I wanted to add authorization to a SPA web api backend (using asp.net core). The build in solutions seem to either be IdentityServer4 (which will only be supported unitl Nov 2022?) (wait, maybe they’ll have a Community Edition for companies with less that $1M revenue?) or Microsoft.AspNetCore.Identity which seems to want to work more with MVC/Razor and SPAs (though I’m sure it could be hacked?).
Seems like it shouldn’t be the end of the world to create a user table and store passwords with some sort of best practice. The first part of that would be how to hash the password. Unsurprisingly there seem to be a few opinions around the inter-webs. This is what I ended up with.
public static class Pbkdf2PasswordHasher
{
const int PasswordHashSize = 512 / 8;
const int SaltSize = 256 / 8;
static readonly HashAlgorithmName HashAlgorithm =
HashAlgorithmName.SHA512;
public static string Generate(
string password,
int iterations = 175_000)
{
//generate a random salt for hashing
var salt = new byte[SaltSize];
new RNGCryptoServiceProvider().GetBytes(salt);
//hash password given salt and iterations
//iterations provide difficulty when cracking
var pbkdf2 =
new Rfc2898DeriveBytes(
password, salt, iterations, HashAlgorithm);
var hash = pbkdf2.GetBytes(PasswordHashSize);
//return delimited string with salt | #iterations | hash
return Convert.ToBase64String(salt) + "|"
+ iterations + "|"
+ Convert.ToBase64String(hash);
}
public static bool IsValid(string password, string encodedHash)
{
//extract original values from delimited hash text
var parts = encodedHash.Split('|');
var salt = Convert.FromBase64String(parts[0]);
var iterations = int.Parse(parts[1]);
var hash = parts[2];
//generate hash from test password
// and original salt and iterations
var pbkdf2 = new Rfc2898DeriveBytes(
password, salt, iterations, HashAlgorithm);
var testHash = pbkdf2.GetBytes(PasswordHashSize);
//if hash values match then return success
if (Convert.ToBase64String(testHash) == hash)
return true;
//no match return false
return false;
}
}Notes
-
interationCountwas used by running a perf test on my local machine to find hashing that took ~100ms[Fact(Skip = "not really a unit test, should probably move to some other mechanism.")] public void Perf() { const int maxRuntimeMs = 200; var iterations = 10_000; var lastRuntime = 0L; while (lastRuntime < maxRuntimeMs) { var timer = Stopwatch.StartNew(); var hashResult = Pbkdf2PasswordHasher.Generate( PasswordPlainTextDefault, iterations); timer.Stop(); var runTime = timer.ElapsedMilliseconds; _testOutputHelper.WriteLine($"hash with iterations: {iterations}, ms: {runTime}, result: {hashResult}"); lastRuntime = runTime; iterations += 5000; } } - I used
HashAlgorithmName.SHA512instead ofSHA256because larger is better?
bcrypt
There is a dotnet bcrypt implementation which seems to more widely respected on the internet (as it is slower to calculate, and slower is better?). But PBKDF2 still seems to be “officially” recommended, and it’s built into dotnet which seemed a little safer than OSS.