Storing passwords correctly
Storing passwords is the security decision most likely to end up in a news headline if you get it wrong. The rules are not complicated, but they are absolute, and Spring Security makes doing it right easy — so there is no excuse for doing it wrong. This lesson is how to store passwords correctly with Spring Security, verified against a running Dakiya, and why each rule exists.
The one rule: never store the password
The foundational rule: you never store the password itself. You store a hash of it — a one-way transformation from which the original cannot be recovered. When a user logs in, you hash what they typed and compare it to the stored hash; if they match, the password was correct. You never need, and must never keep, the plaintext.
Why one-way hashing and not encryption: encryption is reversible (with the key), so an attacker who gets the database and the key gets every password. A hash is not reversible — there is no key that turns the hash back into the password. So even if your entire database is stolen, the passwords are not directly exposed. The absolute rules that follow:
- Never store plaintext passwords. A database breach then hands the attacker every user's actual password — which they will try on those users' other accounts (people reuse passwords).
- Never encrypt passwords (reversible) when you can hash them (one-way). Hashing is strictly safer here.
- Never log a password, put it in a URL, or send it in a response.
Use a slow, salted hash — BCrypt
Not any hash will do. A fast general-purpose hash (MD5, SHA-256) is wrong for passwords, because its speed lets an attacker try billions of guesses per second against a stolen hash. Password hashing needs a deliberately slow, salted, adaptive algorithm. Spring Security's default and recommended choice is BCrypt:
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // slow, salted, adaptive
}
You then hash on registration and verify on login:
String hash = encoder.encode("staff-pass-123"); // store this
boolean ok = encoder.matches("staff-pass-123", hash); // true; matches("wrong", hash) is false
Verified against the running app: encode produced a hash prefixed $2a$10$ (BCrypt, work factor 10), the
hash did not contain the plaintext, matches returned true for the correct password and false
for a wrong one, and — crucially — hashing the same password twice produced different hashes. Three
properties, each essential:
- Slow (adaptive). BCrypt is deliberately expensive, and the cost factor (the
10in$2a$10$) can be raised as hardware gets faster — so brute-forcing stays impractical over time. - Salted. Each hash includes a random salt, so identical passwords hash differently (verified: two hashes of the same password differed). This defeats precomputed "rainbow table" attacks and hides the fact that two users share a password.
- Self-describing. The hash string encodes the algorithm, cost and salt, so
matchesknows how to verify it without extra storage.
The takeaway: use BCrypt (or Argon2/scrypt) via Spring's PasswordEncoder — never a fast hash, never
plaintext, never encryption.
Spring Security wires it in for you
You do not hash and compare by hand at login — Spring Security does it. Register the PasswordEncoder bean
and store hashed passwords, and the authentication mechanism uses the encoder's matches automatically:
UserDetails staff = User.withUsername("staff")
.password(encoder.encode("staff-pass-123")) // store the HASH, not the plaintext
.roles("STAFF").build();
When "staff" logs in, Spring Security loads that user, and its DaoAuthenticationProvider calls
encoder.matches(typedPassword, storedHash) — you write no comparison code. Verified: authenticating with
the correct password succeeded (200) and a wrong password failed (401), driven entirely by BCrypt matching.
In production the users come from your database (a users table with a password_hash column), loaded by a
UserDetailsService; the only rule is that the stored value is always the BCrypt hash, never the
plaintext.
DelegatingPasswordEncoder and upgrading
Spring Boot's default PasswordEncoder is actually a DelegatingPasswordEncoder, which stores a prefix
identifying the algorithm — {bcrypt}$2a$10$.... This is a thoughtful detail: it lets an application
upgrade its hashing over time (support old hashes while writing new ones with a stronger algorithm) and
verify hashes made by different encoders. You rarely configure it directly, but it is why you may see a
{bcrypt} prefix, and it means "the format records which algorithm made this hash" — so migrating to a
stronger one later does not lock out existing users. The principle for you: store hashes through Spring's
PasswordEncoder, and the framework handles verification and future upgrades — your job is simply never to
let a plaintext password touch storage, a log, or a response.
Check your work
The one rule. Never store the password — store a one-way hash; verify by hashing the input and comparing. Hashing (irreversible) beats encryption (reversible) for passwords.
Absolute rules. No plaintext, no encryption-when-you-could-hash, no logging/URL/response of a password.
Use BCrypt. Slow/adaptive (cost factor, raisable), salted (same password → different hashes — verified),
self-describing. A fast hash (MD5/SHA-256) is wrong — too fast to resist brute force. Verified: $2a$10$,
not plaintext, matches correct/wrong true/false, salted differ.
Spring wires it. Register a PasswordEncoder bean, store encoder.encode(...) hashes; the auth provider
calls matches for you (verified 200 correct / 401 wrong) — no hand comparison. Production users from a DB
UserDetailsService.
DelegatingPasswordEncoder. Stores an algorithm prefix ({bcrypt}) so hashing can be upgraded over time
without locking out existing users; store hashes through PasswordEncoder and the framework handles the
rest.
Practice
- Create a
BCryptPasswordEncoderbean;encodea password and confirm the$2a$10$prefix, that it is not the plaintext, and thatmatchesis true/false for correct/wrong (reproduce the verified results). - Encode the same password twice and confirm the hashes differ — the salt.
- Store a user with
encoder.encode(...)and authenticate with correct and wrong passwords; confirm 200 vs 401, with no comparison code of your own. - Reason about why a stolen database of BCrypt hashes is far less dangerous than one of plaintext (or MD5-hashed) passwords.
- Try (in a throwaway) storing a plaintext password and authenticating; observe it fails to match a BCrypt check — and reflect on why plaintext storage is the real disaster.
- Look up Argon2 and explain when you might choose it over BCrypt.
Official documentation
- Spring Security — Password storage —
PasswordEncoder, BCrypt,DelegatingPasswordEncoder. - OWASP — Password Storage Cheat Sheet — The authoritative rules.
- Spring Security — DaoAuthenticationProvider — How matching happens at login.
Next: JWT authentication end to end.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship