Password Storage Done Right: Argon2, bcrypt, and the Hashing Parameters That Matter
Password Storage Done Right
Password storage is not a feature. It is an attack surface that determines how many of your users get compromised when your database leaks. Not if. When.
Every credential database eventually leaks. Through SQL injection, backup exposure, insider access, or a misconfigured cloud storage bucket. The question is not whether an attacker gets your hashed passwords. The question is how long those hashes resist offline cracking once the attacker has them on their own hardware.
The answer depends entirely on two things: the algorithm you chose and the parameters you configured. Most Spring Security applications use bcrypt with default parameters. This chapter explains why that default is no longer sufficient for high-value targets and how to configure password hashing that gives your users months of protection instead of hours.
The Cost Function
Password hashing is intentionally slow. Unlike SHA-256 (designed for speed), password hashing functions are designed to consume computational resources proportional to configurable parameters. The attacker’s cost to crack one hash equals:
cost_per_hash × number_of_candidates
You control cost_per_hash. The attacker controls number_of_candidates (determined by the password’s entropy). For a password from a breach dictionary (low entropy, high probability), the only defense is making each hash evaluation expensive enough that testing millions of candidates takes months on dedicated GPU hardware.
Three algorithms meet this requirement: bcrypt, scrypt, and Argon2id. All three are implemented in Spring Security. The choice between them is not about correctness (all three are cryptographically sound) but about which resource they make expensive:
- bcrypt: CPU-hard. Each evaluation consumes CPU cycles proportional to the work factor. Does not require significant memory. Attackers can parallelize on GPUs.
- scrypt: Memory-hard and CPU-hard. Requires configurable memory per evaluation, making GPU attacks more expensive because GPU memory is limited.
- Argon2id: Memory-hard, CPU-hard, and parallelism-aware. The winner of the Password Hashing Competition. Configures memory, CPU iterations, and parallelism independently. The strongest option against modern GPU hardware.
The decision rule: if you are starting fresh or migrating, use Argon2id. If you have existing bcrypt hashes and cannot afford a full migration cycle, keep bcrypt but increase the work factor and plan migration to Argon2id using DelegatingPasswordEncoder.
Hashing Is Rate Limiting
An underappreciated property of expensive hashing: it rate-limits your own login endpoint. If each password verification takes 250ms, your login endpoint can process at most 4 logins per second per thread. This is a feature, not a bug. An attacker sending 1,000 login attempts per second against your endpoint cannot overwhelm the hash computation because each attempt blocks a thread for 250ms.
This property complements but does not replace explicit rate limiting (covered in Chapter 12). Hash cost gives you inherent protection against brute force at the computation layer. Rate limiting gives you protection at the network layer. Both are necessary.
What This Chapter Covers
The two sections that follow cover Argon2id configuration in depth (the recommended algorithm) and bcrypt limitations with the migration path via DelegatingPasswordEncoder. Both sections follow the five-part structure: the assumption that most implementations make, the attack that exploits it, the mechanism that prevents it, the Spring Security implementation, and the verification that proves it works.