// LOADING
// LOADING
// LOADING_ARTICLE
Password storage is one of those topics where "almost right" is catastrophically wrong. MD5, SHA-1, even plain SHA-256 — all inadequate. Plaintext storage — a direct path to user harm and regulatory liability. This post covers why generic hash functions fail, how bcrypt and Argon2 fix the problem, and how to implement each correctly in Node.js.
A cryptographic hash function like SHA-256 is designed to be fast. On modern hardware, billions of SHA-256 hashes per second are achievable using GPUs. An attacker who obtains a database of SHA-256 password hashes can run a brute-force or dictionary attack at enormous speed.
Password hashing algorithms are intentionally slow. They are designed to make each comparison expensive — on the order of milliseconds — while still being fast enough for legitimate authentication. The attacker's billion-guesses-per-second advantage collapses when each guess costs 100ms of computation.
Before hashing, a salt — a unique random value per user — is concatenated with the password. This means:
Bcrypt and Argon2 both handle salting internally. You do not generate or store the salt separately — it is embedded in the output hash string.
bcrypt has been the industry standard since 1999. Its cost factor (rounds) is a base-2 exponent: cost 10 means 2^10 = 1024 iterations. Incrementing by 1 doubles the work. This lets you ratchet up security as hardware improves.
tsimport bcrypt from 'bcrypt'; const SALT_ROUNDS = 12; // Hash a password before storing export async function hashPassword(plaintext: string): Promise<string> { return bcrypt.hash(plaintext, SALT_ROUNDS); } // Verify at login export async function verifyPassword( plaintext: string, stored: string ): Promise<boolean> { return bcrypt.compare(plaintext, stored); }
The bcrypt.compare function uses constant-time comparison internally, preventing timing side-channel attacks where an attacker infers information from how long the comparison takes.
A cost of 10 is the historical default. In 2024, a cost of 12 is a reasonable baseline for web applications — it produces a hash in roughly 250–400ms on a modern server, which is imperceptible to users but enormously costly for an attacker. Test with your server hardware and choose the highest cost that keeps login latency acceptable (under 500ms).
Bcrypt also has a 72-byte input limit. Passwords longer than 72 bytes are silently truncated. For applications allowing very long passphrases, either enforce a 72-character maximum or pre-hash with SHA-256 before bcrypt (though the latter adds complexity — enforcing the limit is simpler).
Argon2 won the Password Hashing Competition in 2015 and is the current recommendation from security standards bodies including OWASP. It improves on bcrypt in two important ways:
There are three variants: argon2d (GPU resistance), argon2i (side-channel resistance), and argon2id (both). Use argon2id for password storage — it is the OWASP-recommended default.
tsimport argon2 from 'argon2'; const ARGON2_OPTIONS = { type: argon2.argon2id, memoryCost: 65536, // 64 MB timeCost: 3, // 3 iterations parallelism: 4, }; export async function hashPassword(plaintext: string): Promise<string> { return argon2.hash(plaintext, ARGON2_OPTIONS); } export async function verifyPassword( plaintext: string, stored: string ): Promise<boolean> { return argon2.verify(stored, plaintext); }
OWASP's minimum parameters for Argon2id: memoryCost 46 MB, timeCost 1, parallelism 1. The values above are more conservative. Start with OWASP minimums and increase if latency allows.
| | bcrypt | Argon2id | |---|---|---| | Standard | 1999 | 2015 | | Memory hard | No | Yes | | Max input length | 72 bytes | Unlimited | | OWASP recommendation | Acceptable | Preferred | | Node.js library maturity | High | High |
For new projects, use Argon2id. For existing projects using bcrypt, migrate opportunistically: rehash the password using Argon2id the next time the user logs in (you have access to the plaintext at login). Store both old and new hash during transition; once a user rehashes, delete the bcrypt version.
Password hashing is one piece of the authentication picture. Hashed passwords must also be transmitted over TLS (HTTPS), and the session tokens issued after login require their own security properties. See how modern authentication works for how session cookies, JWTs, and refresh tokens connect to the login flow. For why JWTs in localStorage are dangerous regardless of how the password is hashed, see why you should never store JWT in localStorage.
Secure password storage protects you when your database is breached. Additional controls reduce the likelihood and impact of breach:
For a broader security posture, see common web security threats and how to prevent them. If you are preparing for security-focused interviews, the interview prep section covers these topics in depth.