Chaturmind
LearnDSASystem DesignDevOpsEngineering GrowthBlog
Start learning
Chaturmind

Structured learning paths for engineers who want to go deep. Written by practitioners.

Learn

  • Java
  • DSA
  • System Design
  • Spring Boot
  • AI / ML
  • DevOps
  • Engineering Growth

Company

  • Blog
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Chaturmind. All rights reserved.

Built for engineers who want to go deep.


← Spring Security & JWT Auth

Spring Security Basics

  • Spring Security Overview
  • JWT Authentication
  • Authentication Mechanics

Authorization

  • Role-Based Access Control
  • Password Encoding
  • OAuth2 & Social Login Basics
Chaturmind
← Spring Security & JWT Auth

Spring Security Basics

  • Spring Security Overview
  • JWT Authentication
  • Authentication Mechanics

Authorization

  • Role-Based Access Control
  • Password Encoding
  • OAuth2 & Social Login Basics
HomeLearnSpring BootSpring Security & JWT AuthAuthorization
✓ FreeBeginner· 7 min read

Password Encoding

BCryptPasswordEncoder, strength tuning, and why you never store plaintext passwords.

Published September 21, 2026


Password Encoding

Databases get breached, through SQL injection, a leaked backup or an insider. When that happens, what's stored in the password column decides whether attackers get nothing useful or every user's password. And because people reuse passwords, those passwords then unlock their email and bank accounts too.

The rule is simple: never store passwords, store a slow, salted hash of them. Spring Security's PasswordEncoder does this correctly, as long as you know which algorithm to pick and why.

Hashing, not encryption

  • Encryption is reversible with a key. If the key leaks (and it's usually stored near the data), every password is exposed. Passwords must never be encrypted.
  • Hashing is one-way. To check a login, hash the password the user typed and compare the result with the stored hash. You never need the original.

Why fast hashes like SHA-256 are wrong for passwords

SHA-256 and MD5 are designed to be fast. A modern GPU computes billions of them per second. An attacker with your hash table simply tries every common password and every variation, and most human-chosen passwords fall quickly.

Two defences are needed:

  1. A salt: a random value generated per password and stored alongside the hash. Two users with the same password get different hashes, and precomputed "rainbow tables" become useless, because the attacker must attack each hash separately.
  2. Deliberate slowness (a work factor): make each hash cost around a tenth of a second or more. That's unnoticeable for one login, but it multiplies the attacker's cost by millions.

Password-hashing algorithms build both in: bcrypt, scrypt, Argon2 and PBKDF2. Argon2id is the current top recommendation (OWASP); bcrypt remains a solid, widely supported choice.

BCrypt in Spring

@Bean
PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(12);   // cost 12 → 2^12 = 4,096 rounds of the expensive key setup
}
String hash = encoder.encode("correct horse battery staple");
// $2a$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW
//  │   │  └── 22-character salt, then 31-character hash
//  │   └── cost factor
//  └── algorithm version

encoder.matches("correct horse battery staple", hash);   // true

Everything needed to verify, including the algorithm, cost and salt, is inside the stored string. You don't store the salt separately. matches re-hashes the attempt using the same salt and cost, then compares.

The cost factor is exponential: each +1 doubles the time. Tune it so a hash takes roughly 100–500 ms on your production hardware, and raise it over the years as hardware gets faster. Remember that login endpoints become CPU-heavy under attack, so rate-limit them (see below).

BCrypt's 72-byte limit: bcrypt only uses the first 72 bytes of input. For typical passwords that's irrelevant, but it's a reason to cap password length sensibly (for example at 128 characters) and to prefer Argon2 for new systems where that matters.

DelegatingPasswordEncoder: upgrading algorithms over time

Algorithms and cost factors change over a system's lifetime, and you can't rehash old passwords without the plaintext. Spring's default encoder solves this by prefixing each hash with its algorithm:

PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
encoder.encode("secret");                 // "{bcrypt}$2a$10$..."
encoder.matches("secret", "{sha256}...");  // verifies old hashes using the algorithm named in the prefix

New passwords use the current default, and old ones still verify. Implement UserDetailsPasswordService and Spring Security will re-hash a user's password with the current algorithm at their next successful login, which is the only moment you have the plaintext. That's how you migrate from SHA-256 to bcrypt, or from bcrypt to Argon2, without forcing resets.

Around the hash: the rest of a safe password flow

Registration

public void register(RegisterRequest req) {
    if (breachedPasswords.contains(req.password())) {             // e.g. a check against known-breached lists
        throw new WeakPasswordException("This password appears in known data breaches");
    }
    users.save(new User(req.email().toLowerCase(), encoder.encode(req.password())));
    // a UNIQUE index on email is the real duplicate guard — an existsByEmail() check alone can race
}

Current guidance (NIST SP 800-63B) favours length (at least 8, ideally 12+) and blocking known-breached or common passwords over complex composition rules and forced periodic changes.

Login

  • Return the same error ("invalid email or password") whether the email exists or not, so attackers can't enumerate accounts.
  • Rate-limit attempts per account and per IP, and add increasing delays or a CAPTCHA after repeated failures. The slow hash alone doesn't stop online guessing.
  • matches() compares in constant time. Never compare hashes with String.equals yourself.

Password reset

String token = generateSecureToken();                           // e.g. 32 random bytes from SecureRandom, Base64url
resetTokens.save(new ResetToken(user.getId(), sha256(token), Instant.now().plus(Duration.ofMinutes(30))));
emailService.sendResetLink(user.getEmail(), "https://app.example.com/reset?token=" + token);
  • Tokens must be unguessable (a CSPRNG), short-lived and single-use.
  • Store only a hash of the token. A database leak then doesn't hand out working reset links.
  • After a reset, invalidate existing sessions and refresh tokens.
  • Respond "if that email exists, we've sent a link", which avoids account enumeration again.

Common mistakes

  • Using MessageDigest SHA-256 (even salted) instead of a password-hashing algorithm.
  • A single global salt instead of a random salt per password.
  • Logging request bodies that contain passwords, which makes the hashing pointless.
  • Setting the bcrypt cost so high that login becomes a denial-of-service vector, or so low (4–6) that hashing is nearly free for attackers.
  • "Encrypting" passwords so that they can be emailed back to users. If you can recover a password, so can an attacker.

Follow-up questions this topic invites — and their answers

Q: Why is bcrypt better than SHA-256 with a salt? A: The salt only defeats precomputed tables. SHA-256 is still extremely fast, so an attacker can try billions of guesses per second against each salted hash. Bcrypt is deliberately slow and tunable (the cost factor), which multiplies the attacker's work by orders of magnitude while one login stays fast enough for users.

Q: Where is the salt stored? A: Inside the bcrypt output string itself, together with the version and cost. The salt doesn't need to be secret, only unique and random per password. Its job is to force attackers to crack each hash individually.

Q: How would you migrate millions of users from an old SHA-1 scheme? A: Two common approaches. First, verify with the old scheme at the next login, then re-hash with bcrypt (Spring's DelegatingPasswordEncoder plus UserDetailsPasswordService automate this). Second, to protect dormant accounts immediately, wrap the existing hashes: store bcrypt(sha1Hash) now, and verify with bcrypt(sha1(input)) until the user logs in and can be upgraded.

Q: What is a pepper? A: A secret value added to every password before hashing, stored outside the database (for example in a secrets manager or HSM). If only the database leaks, the hashes can't be attacked without it. It complements salting and a slow hash, and doesn't replace them.

Q: Argon2 or bcrypt? A: Argon2id is the modern recommendation because it's memory-hard: it forces attackers to spend RAM as well as CPU, which blunts GPU and ASIC attacks. Bcrypt is still considered secure with an adequate cost factor and has wider support. Spring Security supports both (Argon2PasswordEncoder, BCryptPasswordEncoder).

Previous

Role-Based Access Control

Next

OAuth2 & Social Login Basics

AI Tutor

Lesson: Password Encoding

Quick actions

AI responses can be inaccurate. Verify critical information.