Spring Security Password Encoding: BCrypt, Argon2 & DelegatingPasswordEncoder
Master Spring Security password encoding with BCryptPasswordEncoder, DelegatingPasswordEncoder migration from MD5/SHA, Argon2, and timing attack prevention..
N
NarenFounder & Principal Engineer
20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.
Use BCryptPasswordEncoder(strength) (strength 12 for production) to hash passwords before storing
Never store or compare raw passwords — always call passwordEncoder.matches(raw, encoded)
Use DelegatingPasswordEncoder to migrate from legacy MD5/SHA hashes without forcing password resets
Prefer Argon2 or BCrypt over SHA-based encoders — they are deliberately slow and resistant to GPU cracking
Timing-safe comparison is built into Spring's matches() via MessageDigest.isEqual — never implement it yourself
✦ Definition~90s read
What is Password Encoding and BCrypt in Spring Security?
A PasswordEncoder in Spring Security is an interface with two methods: encode(rawPassword) which produces a one-way hash, and matches(rawPassword, encodedPassword) which verifies a candidate password against a stored hash without revealing the original. The interface deliberately separates encoding from matching to prevent naive implementations that decode instead of re-encode for comparison.
★
Storing passwords is like storing house keys.
BCryptPasswordEncoder is the most widely used implementation. BCrypt incorporates a random 128-bit salt into every hash output, making each stored hash unique even for identical passwords. The output is a 60-character string containing the version, cost factor, salt, and hash.
The strength parameter (4-31) sets 2^strength iterations — higher values exponentially increase computation time for both the application and an attacker.
DelegatingPasswordEncoder wraps multiple PasswordEncoder implementations and identifies them by string prefix. The default instance (created via PasswordEncoderFactories.createDelegatingPasswordEncoder()) encodes with BCrypt but can verify MD5, SHA-1, SHA-256, SCrypt, Argon2, and plaintext ({noop}) hashes.
This makes it the right default for every new Spring Boot application — you get modern encoding today with the flexibility to migrate later.
Plain-English First
Storing passwords is like storing house keys. BCrypt is like converting your key into a unique wax mold that can verify your key fits, but you can not copy the key back from the mold. MD5/SHA is like writing the key number on a sticky note — trivially reversible. DelegatingPasswordEncoder is a key cabinet that supports old mold formats while gradually upgrading them to the modern standard.
Your legacy application stores passwords as MD5 hashes. You know it is wrong, but you cannot force a million users to reset their passwords. A competitor gets breached; the MD5 hashes appear on HaveIBeenPwned within hours. Rainbow tables crack 95% of them in minutes. Now your incident response team is writing the breach notification email at 2 AM.
This scenario plays out repeatedly because password encoding is treated as a solved problem once a hash function is chosen. It is not. The threat model evolves: SHA-1 was considered strong, then MD5 became suspect, then SHA-256 without salt became crackable with GPU farms costing $10/hour on spot instances. The only lasting solution is using adaptive, deliberately slow algorithms and building a migration path into your codebase from day one.
Spring Security's PasswordEncoder interface and its implementations handle this correctly. BCryptPasswordEncoder applies a salt automatically, making rainbow tables useless. Its strength parameter controls the work factor — 12 iterations means 2^12 rounds of hashing, taking ~300ms on modern hardware. Attackers attempting brute force hit a wall; legitimate users barely notice.
DelegatingPasswordEncoder is the production migration tool. It stores the encoder id as a prefix ({bcrypt}, {sha256}, {noop}) alongside the hash. On verification, it routes to the correct encoder. On the next successful login, you upgrade the hash transparently — no password reset required. This is how you migrate from MD5 to BCrypt with zero user disruption.
Argon2 and scrypt take the arms race further. Designed specifically to resist GPU and ASIC attacks by requiring large amounts of memory, they are the recommended choice for new systems. Spring Security 5+ includes Argon2PasswordEncoder out of the box via Bouncy Castle.
This guide covers the full spectrum: BCrypt strength tuning for your hardware, DelegatingPasswordEncoder migration strategies, Argon2 and scrypt configuration, timing attack anatomy, and testing password encoding correctly.
BCryptPasswordEncoder: Strength, Salting, and Configuration
BCrypt was designed in 1999 specifically as a password hashing function. Unlike general-purpose hashes (SHA-256, MD5), BCrypt is slow by design. The strength parameter (also called cost or log rounds) determines how slow: each increment doubles the computation time. Strength 10 = 2^10 = 1024 iterations; strength 12 = 4096 iterations; strength 14 = 16,384 iterations.
On modern hardware, strength 12 takes approximately 250-350ms. This means an attacker testing passwords can attempt roughly 3 per second per CPU core — versus billions per second with raw SHA-256. GPU farms that crack SHA-256 at terahashes per second are throttled to thousands of attempts per second on BCrypt.
Every BCrypt hash includes a randomly generated 128-bit salt embedded in the output. This makes rainbow tables and precomputed attack tables useless — every hash is unique even for identical passwords. The full 60-character BCrypt output encodes the version (2a or 2b), the cost, the 22-character base64 salt, and the 31-character base64 hash.
Choose your strength based on benchmarking on the weakest server in your production fleet. The target: hashing should take 250-500ms. Too fast means attackers can brute-force efficiently; too slow degrades user experience during login. Strength 10 is the historical default; strength 12 is the current recommended minimum. Revisit annually as hardware gets faster.
Do not configure BCryptPasswordEncoder with a fixed SecureRandom unless you have a specific reason — the default uses SecureRandom correctly with system entropy. Passing a seeded SecureRandom reduces salt entropy and weakens the protection.
Never use strength < 10 in production
Strength 4-8 is suitable only for tests. Production minimum is strength 10; strength 12 is recommended. Re-evaluate annually as hardware improves — increment strength during off-peak hours with a rolling hash upgrade.
Production Insight
Store the BCrypt strength in configuration and plan for annual strength increments — implement UserDetailsPasswordService so strength upgrades happen transparently on next login.
Key Takeaway
BCrypt's strength parameter is a tuning dial, not a set-and-forget value — benchmark on your hardware, target 250-500ms, and implement automatic hash upgrade via UserDetailsPasswordService.
thecodeforge.io
Spring Security Password Encoding
DelegatingPasswordEncoder for Zero-Downtime Migration
Legacy applications often store passwords as unsalted MD5, SHA-1, or SHA-256 hashes. Forcing a mass password reset is disruptive and drives user churn. DelegatingPasswordEncoder enables a gradual, transparent migration without any user interaction.
The delegation mechanism works via encoding prefixes stored in the database alongside the hash. The format is {encoderId}hash. When matches(raw, encoded) is called, the encoder extracts the id, finds the matching encoder, and delegates verification. When encoding new passwords (or upgrading on login), it always uses the current default encoder.
Migration path: first, update all existing MD5/SHA hashes in the database to add the appropriate prefix. MD5 becomes {MD5}d8578edf..., SHA-1 becomes {SHA-1}aaf4c61d.... No passwords change — only the prefix is added. Then deploy with DelegatingPasswordEncoder as the PasswordEncoder bean. Existing users continue to log in. On each successful login, Spring's DaoAuthenticationProvider detects the non-current encoding and calls UserDetailsPasswordService.updatePassword() with the new BCrypt hash. Over days or weeks, all active users migrate without any forced reset.
For inactive users who never log in again, consider a proactive migration job. Query for hashes with legacy prefixes, prompt users to reset their password, or after an extended period, lock accounts with legacy encodings.
The {noop} prefix supports plaintext passwords in development and tests. Never allow {noop} in production — add a Spring profile check or @PostConstruct validator that throws if {noop} encoding is active in non-development environments.
Prefix existing hashes before deploying DelegatingPasswordEncoder
Always run the SQL prefix migration BEFORE deploying the new encoder. If hashes have no prefix and you deploy DelegatingPasswordEncoder, all logins fail with IllegalArgumentException: no PasswordEncoder mapped for id null.
Production Insight
Track the distribution of password encoding prefixes in a Grafana dashboard — plot the daily count of {bcrypt} vs legacy prefixes to visualize migration progress and set a deadline for legacy account lockout.
Key Takeaway
DelegatingPasswordEncoder enables transparent, zero-downtime password migration — prefix existing hashes in the DB, implement UserDetailsPasswordService, and let Spring upgrade hashes on each login.
Argon2 and SCrypt: Memory-Hard Password Hashing
BCrypt was revolutionary in 1999, but modern GPU farms and ASICs challenge its assumptions. BCrypt requires minimal memory, making it parallelizable on GPU hardware. An NVIDIA RTX 4090 can attempt approximately 100,000 BCrypt-10 hashes per second — still slow, but ASIC farms can do much better.
Argon2 won the Password Hashing Competition in 2015 and is designed to resist GPU and ASIC attacks by requiring large amounts of memory. Its parameters control time (iterations), memory (KB), and parallelism (threads). To crack a single Argon2 hash, an attacker needs to allocate the configured memory per attempt — 64MB of memory per attempt makes massively parallel cracking economically unviable.
Argon2 has three variants: Argon2d (data-dependent memory access, resistant to GPU cracking), Argon2i (data-independent, resistant to side-channel attacks), and Argon2id (hybrid, recommended by RFC 9106 for password hashing). Spring Security's Argon2PasswordEncoder uses Argon2id by default.
SCrypt (implemented as SCryptPasswordEncoder in Spring Security) is another memory-hard function. It is an older alternative to Argon2 with similar goals but a more complex parameter space. Prefer Argon2 for new systems; SCrypt for compatibility with existing systems.
Choose parameters carefully: the OWASP recommendation for Argon2id is m=19456 (19 MB), t=2 iterations, p=1 thread for interactive authentication. Increase memory to m=65536 (64MB) for high-security applications where login latency can be 1-2 seconds.
Argon2 requires Bouncy Castle on the classpath
Add org.bouncycastle:bcprov-jdk18on to your pom.xml or build.gradle. Spring Boot's spring-security-crypto includes Argon2 support but delegates to Bouncy Castle at runtime.
Production Insight
Argon2's memory requirement is per-hash-operation — with m=65536 and 100 concurrent login requests, your JVM needs at least 6.5GB headroom. Set memory parameters based on your peak concurrency and available heap.
Key Takeaway
Argon2id with OWASP parameters is the modern recommendation for new systems; use DelegatingPasswordEncoder to support Argon2 for new hashes while BCrypt validates legacy ones.
thecodeforge.io
Spring Security Password Encoding
Timing Attack Prevention: MessageDigest.isEqual and Spring's Mitigations
A timing attack exploits the fact that string comparison in most languages short-circuits on the first differing character. An attacker measuring response times can determine how many leading characters of their guess match the stored hash, iterating until the full hash is found. This is particularly relevant for HMAC signature verification and password comparison.
Java's String.equals() is not constant-time. MessageDigest.isEqual(byte[], byte[]) is — it always compares all bytes regardless of where the first mismatch occurs. Spring Security uses this internally in BCryptPasswordEncoder.matches() and other PasswordEncoder.matches() implementations.
However, timing attacks at the password comparison level are largely irrelevant for BCrypt because the attacker cannot observe which part of the BCrypt output matches. The more relevant timing vulnerability is the one described in the production incident above: the difference in time between the fast path (username not found → throw exception immediately) and the slow path (username found → run BCrypt).
Spring Security's DaoAuthenticationProvider mitigates this with a dummy password check when the user is not found. It calls passwordEncoder.matches(presentedPassword, userNotFoundEncodedPassword) where userNotFoundEncodedPassword is a pre-computed BCrypt hash of a random string. This ensures the authentication response takes approximately the same time whether the username exists or not.
For custom authentication code, always implement the same pattern: run the full password check before returning, use MessageDigest.isEqual for any HMAC or token comparison, and add random jitter (5-20ms) to authentication responses to prevent statistical timing analysis.
Never implement custom authentication bypassing DaoAuthenticationProvider
Custom authentication logic often omits the dummy BCrypt check for missing users. Always extend or configure DaoAuthenticationProvider rather than writing authentication from scratch — it has years of security hardening built in.
Production Insight
Add a @ControllerAdvice that introduces 5-50ms random jitter to all 401 responses — statistical timing analysis requires consistent timing, and jitter breaks the measurement.
Key Takeaway
Spring Security's DaoAuthenticationProvider already prevents username enumeration via timing — the risk is when custom authentication code bypasses it and returns early for missing users.
Password Policy Enforcement and Validation
A strong PasswordEncoder is necessary but not sufficient — weak passwords that are correctly BCrypt-hashed are still vulnerable to dictionary attacks. Password policy enforcement at the application layer prevents users from choosing easily guessable passwords.
Spring Security does not include a password policy engine, but Passay is the standard Java library for this. It validates passwords against configurable rules: minimum length, character class requirements, no common dictionary words, no username substring, no sequential characters. Integrate Passay in your UserService validation layer before calling passwordEncoder.encode().
For checking whether a password appears in known breach databases, integrate with the HaveIBeenPwned Passwords API. The API uses k-anonymity: you send the first 5 characters of the SHA-1 hash and receive all matching suffixes. This reveals nothing about the full password. Check this API asynchronously on password change and warn users whose passwords appear in breaches.
Password expiration is increasingly discouraged by NIST SP 800-63B (2017). Forced periodic expiration leads to weak increment patterns (Spring2023! → Summer2023!). Instead, enforce expiration only when a breach is detected. Store the password encoding timestamp and flag accounts where the hash scheme is too old.
Store only the hash — never store the plaintext password even temporarily. Avoid logging request bodies for password-change endpoints. Mask sensitive fields in debug logs by configuring logging.level.org.springframework.security=DEBUG only in controlled environments with log scrubbing.
NIST recommends checking breach databases, not complexity rules
NIST SP 800-63B recommends checking passwords against known breach lists rather than enforcing character complexity rules. A long passphrase of dictionary words is stronger than a short P@ssw0rd that appears in breach databases.
Production Insight
Cache HaveIBeenPwned responses locally for the 5-character prefix — the same prefix may appear in many password change requests, and local caching reduces external API calls significantly.
Key Takeaway
Combine Passay policy validation with HaveIBeenPwned k-anonymity checks for production-grade password policy — enforce breach checks over character complexity rules per NIST SP 800-63B.
Why Your Old Passwords Suddenly Fail After a Spring Boot Upgrade
You upgraded from Spring Security 4 or 5.1 to 5.8+. Now users can't log in. The error says 'There is no PasswordEncoder mapped for the id null'. This is not a bug. It's Spring Security enforcing a policy you ignored. In older versions, you could store passwords in plaintext or use NoOpPasswordEncoder. That's gone. Spring Security 5.0+ requires a prefix in the stored password hash that identifies the encoder used. If your database has 'secret' instead of '{bcrypt}$2a$10$...', authentication fails immediately. The fix is not to roll back. The fix is to migrate your password storage format while keeping users logged in. You need DelegatingPasswordEncoder with a default encoder for new passwords and a list of legacy encoders for verification. This is the zero-downtime path. Do not patch logins with NoOpPasswordEncoder. That trades a warning for a security hole.
On first login with old password, DelegatingPasswordEncoder detects '{noop}secret' and rehashes to '{bcrypt}$2a$10$...' via custom UserDetailsService.onAuthenticationSuccess.
Production Trap:
If you store password without prefix (e.g. plain 'secret'), DelegatingPasswordEncoder fails. You must either wrap that old hash in '{noop}secret' in DB or run a migration script. I've seen teams spend 3 days debugging this because they forgot to add the prefix.
Key Takeaway
Always store passwords with the encoder prefix, or authentication breaks on upgrade. DelegatingPasswordEncoder is your migration lifeline, not a permanent solution.
The Real Cost of Choosing the Wrong Salt Strategy
BCrypt generates a random salt per hash. That's good. SCrypt and Argon2 do too. But if you re-encode the same password, the hash changes. That's by design. Salt prevents rainbow table attacks and hides identical passwords. But some developers get clever: they reuse a static salt across all users to make password verification faster. Do not do this. Static salt means one rainbow table cracks every user. The memory-hard algorithms (Argon2, SCrypt) already cost 100ms+ per hash. The salt generation is not the bottleneck. The bottleneck is your database query. Spring Security 3.x removed the setSalt() method for a reason. If you must use a legacy SHA-256 with salt, encode the salt into the hash format: '{salt}hash'. But honestly, just use BCrypt. Default strength 10. That's 2^10 rounds. For most apps, strength 10-12 is sufficient. Strength 14+ is for government secrets. If your users hate slow logins, drop strength, not salt randomness.
SaltAntiPattern.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// io.thecodeforge — java tutorial// DON'T: shared static saltpublicclassBadSalt {
privatestaticfinalString FIXED_SALT = "supersecret";
publicStringhash(String password) {
return BCrypt.hashpw(password, BCrypt.gensalt()); // NO! uses random salt
}
}
// DO: let BCrypt handle salt
@BeanpublicBCryptPasswordEncoderencoder() {
return new BCryptPasswordEncoder(10); // strength only
}
Output
BCryptPasswordEncoder with strength 10 generates unique salt per password automatically. No need to manage salt storage or rotation.
CVE Waiting to Happen:
I audited a fintech app using static salt across all users. They stored salt in code. One Git leak, and every password was cracked in 2 hours with a precomputed rainbow table. Salt must be per-user, random, and stored in the hash.
Key Takeaway
Trust the encoder to generate salt. Never hardcode or share salt across users. If you're manually managing salt, you're doing it wrong.
● Production incidentPOST-MORTEMseverity: high
Timing Attack Exposed Admin Password via Login Response Time Differential
Symptom
Security audit revealed that requests for non-existent usernames returned 401 in ~1ms while requests for valid usernames returned 401 in ~280ms — the BCrypt computation time was leaking existence of accounts.
Assumption
The team assumed BCrypt was sufficient protection. The response time differential was assumed negligible.
Root cause
The UserDetailsService threw UsernameNotFoundException immediately for unknown users, bypassing BCrypt entirely. For valid users, BCrypt ran its 2^12 iterations. The 280ms difference was measurable and consistent, enabling username enumeration via timing.
Fix
Added a dummy BCryptPasswordEncoder.matches() call in the exception handler for unknown users, ensuring constant-time response regardless of username validity. Spring Security actually handles this via DaoAuthenticationProvider.mitigateAgainstTimingAttack — enabled by configuring setUserDetailsPasswordService.
Key lesson
Timing attacks are real and exploitable remotely over HTTPS.
Always ensure authentication paths take the same time for valid and invalid credentials.
Spring Security's DaoAuthenticationProvider already mitigates this if configured correctly — do not bypass it with custom authentication logic.
Production debug guideSymptom → root cause → fix5 entries
Symptom · 01
Login fails with correct password after migration
→
Fix
Verify the stored hash has the correct {bcrypt} prefix if using DelegatingPasswordEncoder. Check that the hash was encoded with passwordEncoder.encode() not a raw BCrypt library call without prefix. Print the stored hash and verify it starts with {bcrypt}$2a$ or {bcrypt}$2b$. If missing prefix, update the stored value or switch to the bare BCryptPasswordEncoder (not DelegatingPasswordEncoder).
Symptom · 02
Password encoding is extremely slow in tests
→
Fix
BCrypt strength 12 takes ~300ms. In tests, use new BCryptPasswordEncoder(4) or new BCryptPasswordEncoder() with strength 4 for dramatically faster test execution. Never use strength 4 in production. Configure the encoder as a bean and mock or override it in test contexts using @TestConfiguration.
Symptom · 03
IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
→
Fix
This means the stored password has no {id} prefix but you are using DelegatingPasswordEncoder. Either prefix existing hashes with {bcrypt} in the database, or switch to a bare BCryptPasswordEncoder bean. If migrating, wrap the default encoder: PasswordEncoderFactories.createDelegatingPasswordEncoder() and update all existing hashes to have prefixes.
Symptom · 04
BCrypt hash does not match despite correct password
→
Fix
Confirm you are not double-encoding. If the UserDetailsService already returns the stored encoded password and Spring's DaoAuthenticationProvider calls matches(rawPassword, storedEncoded), do not call encode() again in your service. Also check character encoding — a password with non-ASCII characters may be encoded differently depending on the charset used in the HTTP request vs. the database.
Symptom · 05
Password upgrade (re-encoding) not happening on login
→
Fix
Implement UserDetailsPasswordService in your UserDetailsService. Spring Security calls updatePassword(user, newEncodedPassword) automatically when DelegatingPasswordEncoder detects an outdated encoding scheme. Without this implementation, hash upgrades never persist to the database.
★ Debug Cheat SheetFast commands for diagnosing password encoding issues.
Need to generate BCrypt hash for testing−
Immediate action
Generate hash via Spring Boot CLI or curl
Commands
curl -s -X POST http://localhost:8080/actuator/env | grep -i password
Choose the highest strength that keeps login under 500ms on your weakest production node
Password Hashing Algorithm Comparison
Algorithm
GPU Resistance
Memory Hard
Spring Support
Rec. for New Systems
MD5
None (terahash/s)
No
Legacy via Delegating
Never
SHA-256
Poor (gigahash/s)
No
Legacy via Delegating
Never
BCrypt-10
Moderate
No
BCryptPasswordEncoder
Minimum
BCrypt-12
Good
No
BCryptPasswordEncoder
Yes
SCrypt
Good
Yes
SCryptPasswordEncoder
Yes
Argon2id
Excellent
Yes
Argon2PasswordEncoder
Preferred
PBKDF2
Moderate
No
Pbkdf2PasswordEncoder
Acceptable
⚙ Quick Reference
2 commands from this guide
File
Command / Code
Purpose
PasswordMigrationConfig.java
@Bean
Why Your Old Passwords Suddenly Fail After a Spring Boot Upg
SaltAntiPattern.java
public class BadSalt {
The Real Cost of Choosing the Wrong Salt Strategy
Key takeaways
1
BCrypt (strength ≥ 12) and Argon2id are the only acceptable password hashing algorithms for production
never use MD5, SHA-1, or SHA-256 alone.
2
DelegatingPasswordEncoder enables zero-downtime migration from legacy hash algorithms; prefix existing hashes before deploying and implement UserDetailsPasswordService.
3
Spring Security's DaoAuthenticationProvider already prevents timing-based username enumeration
never bypass it with custom authentication logic.
4
Use MessageDigest.isEqual() for any security-sensitive string comparison; String.equals() leaks timing information.
5
Validate passwords against HaveIBeenPwned (k-anonymity) on registration and password change
breach exposure is a stronger security signal than character complexity rules.
Common mistakes to avoid
6 patterns
×
Comparing raw passwords with `equals()` instead of `passwordEncoder.matches()`
Symptom
Authentication works only when passwords are stored as plaintext; fails after introducing hashing
Fix
Always use passwordEncoder.matches(rawPassword, storedEncodedPassword). Never compare raw passwords. Never decode stored hashes.
×
Using `NoOpPasswordEncoder` in production (forgotten from a tutorial)
Symptom
Passwords stored as plaintext; any DB read reveals all credentials
Fix
Add a @PostConstruct validator that throws IllegalStateException if the active PasswordEncoder is NoOpPasswordEncoder and the profile is not dev or test.
×
Setting BCrypt strength too low (4-8) for performance in production
Symptom
Hashing is fast but provides minimal protection against brute force attacks
Fix
Use strength 12 as the production minimum. Benchmark on your hardware targeting 250-500ms per hash. Never sacrifice security for sub-100ms hashing.
×
Not implementing `UserDetailsPasswordService` when using `DelegatingPasswordEncoder`
Symptom
Hash upgrades detected on login but never persisted — old encoding remains in DB forever
Fix
Implement UserDetailsPasswordService.updatePassword() in your UserDetailsService and persist the new encoded password to the database.
×
Logging the raw password or the encoded password in debug output
Symptom
Passwords or password hashes appear in log files, increasing breach impact
Fix
Never log password-related fields. Mask sensitive fields in Spring Security debug logs. Add a custom LoggingFilter that sanitizes request bodies for /login and /password endpoints.
×
Deploying `DelegatingPasswordEncoder` without first prefixing existing hashes
Symptom
IllegalArgumentException: There is no PasswordEncoder mapped for the id "null" — all logins fail
Fix
Run the SQL migration to add {bcrypt} or {MD5} prefixes to existing hashes BEFORE deploying the new encoder. Test in staging with a copy of production data.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01JUNIOR
Why should you never store passwords as MD5 or SHA-256 hashes?
Q02JUNIOR
What does the BCrypt strength parameter control, and how do you choose i...
Q03SENIOR
How does DelegatingPasswordEncoder enable zero-downtime migration from M...
Q04SENIOR
Explain a timing attack on a login endpoint and how Spring Security prev...
Q05SENIOR
What is the difference between BCrypt and Argon2 in terms of attack resi...
Q06SENIOR
How would you use the HaveIBeenPwned API without revealing the user's pa...
Q07SENIOR
Why is `MessageDigest.isEqual()` preferred over `String.equals()` for se...
Q08SENIOR
How do you handle the Argon2 memory requirement in a high-concurrency AP...
Q01 of 08JUNIOR
Why should you never store passwords as MD5 or SHA-256 hashes?
ANSWER
MD5 and SHA-256 are general-purpose hashes designed to be fast. An attacker with a GPU farm can compute billions of SHA-256 hashes per second, enabling exhaustive dictionary and brute-force attacks. They also lack salting by default, making precomputed rainbow tables effective. BCrypt and Argon2 are intentionally slow and include automatic salting.
Q02 of 08JUNIOR
What does the BCrypt strength parameter control, and how do you choose it?
ANSWER
The strength (or cost) parameter sets 2^strength iterations of the BCrypt algorithm. Strength 12 means 4096 rounds, taking ~300ms on modern hardware. Choose the highest value that keeps login latency under 500ms on your weakest production server. Benchmark annually — as hardware speeds up, increment the strength and upgrade hashes on next login via UserDetailsPasswordService.
Q03 of 08SENIOR
How does DelegatingPasswordEncoder enable zero-downtime migration from MD5 to BCrypt?
ANSWER
DelegatingPasswordEncoder stores the encoder ID as a prefix ({MD5}, {bcrypt}) alongside the hash. When validating, it routes to the matching encoder by prefix. Spring's DaoAuthenticationProvider detects when the stored encoding differs from the default and calls UserDetailsPasswordService.updatePassword() with the new BCrypt hash after successful login. Existing users migrate silently; no password reset required.
Q04 of 08SENIOR
Explain a timing attack on a login endpoint and how Spring Security prevents it.
ANSWER
A timing attack exploits the fact that UserDetailsService throws UsernameNotFoundException immediately for unknown users (fast path, ~1ms), while valid users undergo BCrypt verification (~300ms). An attacker can enumerate valid usernames by measuring response times. Spring's DaoAuthenticationProvider prevents this by running BCrypt verification even for missing users using a pre-computed dummy hash, ensuring constant response time regardless of username validity.
Q05 of 08SENIOR
What is the difference between BCrypt and Argon2 in terms of attack resistance?
ANSWER
BCrypt requires minimal memory, so GPU farms can run many parallel BCrypt computations simultaneously. Argon2 is memory-hard — each computation requires a configurable amount of RAM (e.g., 64MB). An attacker running 1000 parallel Argon2 computations needs 64GB of RAM, making GPU/ASIC attacks economically unviable. Argon2id is the current NIST and OWASP recommendation for new systems.
Q06 of 08SENIOR
How would you use the HaveIBeenPwned API without revealing the user's password?
ANSWER
Use the k-anonymity API. SHA-1 hash the password, send only the first 5 characters of the hex hash to api.pwnedpasswords.com/range/{prefix}. The API returns all hash suffixes matching that prefix. Compare the remaining suffix locally. The API never sees the full hash or the original password.
Q07 of 08SENIOR
Why is `MessageDigest.isEqual()` preferred over `String.equals()` for security comparisons?
ANSWER
String.equals() short-circuits on the first differing character, leaking information about how many characters match via response time differences. MessageDigest.isEqual() always compares all bytes in constant time, preventing timing-based side-channel attacks. Use it for HMAC verification, token comparison, and API key validation.
Q08 of 08SENIOR
How do you handle the Argon2 memory requirement in a high-concurrency API?
ANSWER
Calculate peak memory: concurrent_logins × argon2_memory_per_hash. With m=65536 (64MB) and 100 concurrent logins, you need 6.4GB just for Argon2 operations. Mitigation strategies: use a lower memory setting for interactive APIs (m=19456, 19MB per OWASP), apply a rate limiter to login endpoints limiting concurrent BCrypt/Argon2 operations, and use a fixed-size ExecutorService for hashing to cap parallelism.
01
Why should you never store passwords as MD5 or SHA-256 hashes?
JUNIOR
02
What does the BCrypt strength parameter control, and how do you choose it?
JUNIOR
03
How does DelegatingPasswordEncoder enable zero-downtime migration from MD5 to BCrypt?
SENIOR
04
Explain a timing attack on a login endpoint and how Spring Security prevents it.
SENIOR
05
What is the difference between BCrypt and Argon2 in terms of attack resistance?
SENIOR
06
How would you use the HaveIBeenPwned API without revealing the user's password?
SENIOR
07
Why is `MessageDigest.isEqual()` preferred over `String.equals()` for security comparisons?
SENIOR
08
How do you handle the Argon2 memory requirement in a high-concurrency API?
SENIOR
FAQ · 6 QUESTIONS
Frequently Asked Questions
01
Can I use the same PasswordEncoder bean for all services in a microservice architecture?
Yes — the PasswordEncoder bean is stateless and can be shared. Configure it once in a shared library. However, each service that performs password verification should have the encoder on its classpath; for services that only validate JWTs, the encoder is unnecessary overhead.
Was this helpful?
02
How do I handle password encoding in a multi-tenant application where tenants may require different policies?
Implement a TenantAwarePasswordEncoder that wraps DelegatingPasswordEncoder but selects the default encoder based on the current tenant context. Store tenant configuration in a database and inject it via a TenantContextHolder. Each tenant's hashes are prefixed with both the tenant ID and the encoder ID.
Was this helpful?
03
Is it safe to use BCrypt for hashing API keys or tokens?
For API keys, use a fast hash (SHA-256) with a constant salt rather than BCrypt. BCrypt's slowness is designed for interactive human-speed logins — API keys are validated per request at high frequency and 300ms per check is unacceptable. Store SHA-256(prefix + api_key) where the prefix is user-visible for key identification.
Was this helpful?
04
How do I migrate passwords when users are offline for months and I need to deprecate old encodings?
After a grace period (e.g., 6 months), query users whose password hash still has legacy prefixes. Send them a password reset email. After another grace period, lock accounts with legacy encodings. Monitor the prefix distribution in your database and set a sunset date for legacy encoder support.
Was this helpful?
05
Should I pepper passwords in addition to salting (BCrypt)?
BCrypt's automatic per-password salt already prevents rainbow tables and precomputed attacks. A pepper (secret key known only to the application, stored separately from the DB) adds an additional layer — even if the database is compromised, hashes cannot be cracked without the pepper. Implement peppering by HMAC-ing the raw password with the pepper before BCrypt encoding. Store the pepper in a secrets manager (AWS Secrets Manager, HashiCorp Vault), not in the database.
Was this helpful?
06
What is the risk of using {noop} prefix in a shared configuration file?
If {noop} passwords appear in a shared configuration (like Spring Boot test fixtures committed to Git), they expose plaintext passwords in source control. Always use {bcrypt} hashes even in test fixtures. Use new BCryptPasswordEncoder(4).encode("testpassword") and commit the resulting hash.