Storing passwords safely is one of the most important security responsibilities of any application. This lesson covers why plain hashing isn't enough, and how to hash passwords correctly using bcrypt.
Why Plain Hashing Isn't Enough
Simply running a password through crypto.createHash('sha256') is fast, which is exactly the problem: attackers with a leaked password database can try billions of guesses per second against a fast hash. Password-hashing algorithms like bcrypt and argon2 are deliberately slow and include a random "salt" automatically, making large-scale guessing attacks impractical.
bcrypt also protects against "rainbow table" attacks, precomputed hash lookups, because each password is hashed together with a unique, randomly generated salt, so identical passwords produce completely different hashes.
The cost factor (12 here) controls how slow the hashing is, higher values are more secure but take longer to compute, both at signup and at login.
bcrypt API
bcrypt.hash(password, saltRounds) // returns a hash (includes the salt)
bcrypt.compare(password, hash) // returns true/false
bcrypt.hashSync(password, saltRounds) // synchronous variant
bcrypt.hash() generates a unique salt automatically and embeds it in the returned hash string.
bcrypt.compare() extracts the salt from the stored hash and re-hashes the input to compare safely.
Never store or compare plain-text passwords, only ever store the resulting hash.
A cost factor between 10 and 12 is a common, reasonable starting point as of current hardware.
Password Hashing Cheatsheet
The core do's and don'ts of storing passwords.
Do
Don't
Use bcrypt/argon2 to hash passwords
Use crypto.createHash('sha256') alone
Let the library generate the salt
Reuse the same salt for every password
Store only the resulting hash
Store plain-text passwords anywhere, ever
Use bcrypt.compare() to check logins
Compare hashes with === manually
Increase cost factor as hardware improves
Leave the cost factor fixed forever
Hashing at Signup, Comparing at Login
The password itself is only ever needed briefly, hash it immediately at signup and discard the plain-text value; at login, compare the submitted password against the stored hash without ever decrypting anything (hashing is one-way, there's nothing to decrypt).
Returning the exact same generic "Invalid credentials" message whether the email or the password was wrong avoids leaking which emails are registered.
Choosing a Cost Factor
bcrypt's cost factor exponentially increases how long hashing takes: each increment roughly doubles the computation time. Choose a value that takes on the order of 100-300ms on your production hardware, slow enough to deter brute-force attempts, fast enough not to hurt legitimate login latency noticeably.
Common Mistakes
Hashing passwords with a fast, general-purpose hash function instead of bcrypt/argon2.
Storing or logging plain-text passwords anywhere, even temporarily, during debugging.
Comparing password hashes with === instead of the library's dedicated .compare() function.
Revealing whether an email exists in the system through different error messages for bad email vs bad password.
Key Takeaways
Passwords must be hashed with a slow, salted algorithm like bcrypt, never a fast general-purpose hash.
bcrypt.hash() embeds a unique salt automatically; bcrypt.compare() handles comparison safely.
Plain-text passwords should exist only transiently, at signup and login, never stored anywhere.
A cost factor around 10-12 is a reasonable default, tuned to your hardware and security requirements.
Pro Tip
Never build your own password-hashing scheme, even a seemingly clever one. Stick to bcrypt or argon2, both are battle-tested, peer-reviewed, and handle salting correctly by default, which is far more valuable than any custom approach.
You now know how to store passwords securely. Next, explore session-based authentication and cookies as an alternative to tokens.