This is not secure.
First off, as Sjoerd already pointed out, your question title about storing a password hash is detached from the question text which deals with a password-based authentication scheme. Those are two entirely different aspects.
The authentication scheme you've proposed is not secure for many reasons. It actually seems to be a reinvention of the old Digest Access Authentication scheme and has many of the same problems.
- The server has to store the password or password-equivalent data as plaintext. Since the server must be able to calculate
hash(hash(password + salt) + nonce)
itself and then compare the result with the client-provided value, they either have to know password
or hash(password + salt)
. In the first case, your server is keeping the plaintext passwords of all users, which is a security nightmare. In the second case, the passwords are at least hashed, but knowing the password hash alone is sufficient for an attacker to impersonate the user in your scheme. So with regards to your application, there's no improvement. You're merely doing your users a service by protecting the original passwords in case they've reused them somewhere else (which of course they shouldn't do in the first place).
- In your scheme, calculating the password hash has to happen client-side. Not only does this make JavaScript a strict requirement for your application. It also means you have to find a production-ready JavaScript implementation of a strong password hashing algorithm, you cannot fully control the hash calculations, and you have to be careful not to overload weak client devices like smartphones or tablets – while at the same time making sure the password hashes are strong enough to withstand brute-force attacks.
- If you use the username as the salt, then an attacker can reuse calculated hashes, potentially allowing them to perform a brute-force attack much more efficiently. For example, if the same username is present in a different application that uses the same hash algorithm, then it's possible to attack both applications at once – the attacker can guess a password, calculate the hash with the username as the salt and then compare the result with the stored hashes in both applications. If the username is very common, the attacker may also have a precomputed table of hashes which lets them search for a match with a simple lookup.
- Since the entire scheme is home-grown, there's a huge risk of making other mistakes that drastically reduce security.
The proper approach is to transmit the password over a TLS-encrypted connection and use a strong password hash algorithm like Argon2 on the server to calculate the stored hash. While Argon2 is state of the art, it's also fairly new and not yet implemented in the standard libraries of all major programming languages. The are alternatives like bcrypt, though.
Argon2 has different parameters which let you fine-tune the strength of the hashes. You can find recommended values in RFC 9106. Note that Argon2 has built-in support for salts, so you just need to make sure you're proving a sufficiently long random value. You should use at least 128 bits obtained from a secure random number generator (most programming languages have a built-in feature for this).
Another interesting feature of Argon2 is that you can mix in a secret value (through the parameter K
). This way, an attacker can only perform a brute-force attack if they've obtained both the hash and the secret K
.