RizTech Academy logo
RizTech Academy
SecurityLesson 2 of 525 min

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 10 in $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 matches knows 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

  1. Create a BCryptPasswordEncoder bean; encode a password and confirm the $2a$10$ prefix, that it is not the plaintext, and that matches is true/false for correct/wrong (reproduce the verified results).
  2. Encode the same password twice and confirm the hashes differ — the salt.
  3. Store a user with encoder.encode(...) and authenticate with correct and wrong passwords; confirm 200 vs 401, with no comparison code of your own.
  4. Reason about why a stolen database of BCrypt hashes is far less dangerous than one of plaintext (or MD5-hashed) passwords.
  5. 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.
  6. Look up Argon2 and explain when you might choose it over BCrypt.

Official documentation

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