Password Encoding: BCrypt, Argon2, and DelegatingPasswordEncoder
Why Passwords Must Be Hashed
Storing plaintext passwords is a critical security failure. When a database is breached, attackers immediately have every user’s password — and because people reuse passwords, those credentials work on other sites too.
Password hashing is not encryption. Encryption is reversible. Hashing is one-way: you can verify a password by hashing it and comparing to the stored hash, but you cannot recover the original password from the hash. A good password hashing algorithm is also deliberately slow, making brute-force attacks computationally expensive.
PasswordEncoder Interface
Every encoder in Spring Security implements PasswordEncoder:
public interface PasswordEncoder {
String encode(CharSequence rawPassword);
boolean matches(CharSequence rawPassword, String encodedPassword);
default boolean upgradeEncoding(String encodedPassword) { return false; }
}
encode— produces the hash (includes a random salt by default in modern encoders)matches— compares a raw password against a stored hashupgradeEncoding— returnstrueif the encoded form should be re-hashed (e.g. cost factor increased)
BCrypt
BCrypt is the default in Spring Security and the correct choice for most applications. It incorporates a cost factor (work factor) that makes it slower as hardware improves.
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // default cost = 10
}
Set the cost factor explicitly:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // 2^12 rounds — slower, more secure
}
Cost factor 10 is the default; each increment doubles the work. Benchmark on your hardware: aim for 100–500 ms per hash, which is imperceptible to users but expensive for attackers.
// Benchmarking helper
void benchmarkBCrypt() {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);
long start = System.currentTimeMillis();
encoder.encode("test-password");
long elapsed = System.currentTimeMillis() - start;
System.out.println("BCrypt (cost 12): " + elapsed + " ms");
}
BCrypt output looks like this:
$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
^^ ^^
| cost factor (10)
algorithm version (2a)
Argon2
Argon2 won the Password Hashing Competition in 2015 and is considered the strongest password hashing algorithm available. It is memory-hard, meaning attackers cannot parallelize attacks using GPUs as effectively as against BCrypt.
@Bean
public PasswordEncoder passwordEncoder() {
return new Argon2PasswordEncoder(
16, // salt length in bytes
32, // hash length in bytes
1, // parallelism
65536, // memory in KB (64 MB)
3 // iterations
);
}
Argon2 requires the Bouncy Castle library:
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
</dependency>
Argon2 is the better technical choice; BCrypt has broader operational familiarity. Either is acceptable for new applications.
SCrypt and PBKDF2
Both are solid alternatives, included for completeness:
// SCrypt — memory-hard like Argon2, older
@Bean
public PasswordEncoder passwordEncoder() {
return new SCryptPasswordEncoder(
16384, // CPU cost
8, // memory cost
1, // parallelization
32, // key length
64 // salt length
);
}
// PBKDF2 — NIST-approved, used in government/compliance contexts
@Bean
public PasswordEncoder passwordEncoder() {
return new Pbkdf2PasswordEncoder(
"", // secret (empty = none)
16, // salt length
310000, // iterations (OWASP recommended minimum)
Pbkdf2PasswordEncoder.SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA256
);
}
DelegatingPasswordEncoder
Real applications must handle password algorithm upgrades. Users registered two years ago may have BCrypt(cost=10) hashes; you want new registrations to use BCrypt(cost=12) or Argon2. DelegatingPasswordEncoder manages this by prefixing each stored hash with an identifier:
{bcrypt}$2a$10$...
{argon2}$argon2id$...
{pbkdf2}$...
{noop}plaintext
The prefix is stored in the database alongside the hash. On authentication, DelegatingPasswordEncoder reads the prefix, selects the right encoder, and delegates to it.
The Default DelegatingPasswordEncoder
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
This registers BCrypt as the default encoder and includes decoders for all supported algorithms. New passwords are encoded as {bcrypt}...; existing passwords with any registered prefix continue to work.
Custom DelegatingPasswordEncoder
@Bean
public PasswordEncoder passwordEncoder() {
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put("bcrypt", new BCryptPasswordEncoder(10));
encoders.put("bcrypt12", new BCryptPasswordEncoder(12));
encoders.put("argon2", new Argon2PasswordEncoder(16, 32, 1, 65536, 3));
encoders.put("noop", NoOpPasswordEncoder.getInstance()); // legacy
DelegatingPasswordEncoder delegating = new DelegatingPasswordEncoder("argon2", encoders);
delegating.setDefaultPasswordEncoderForMatches(new BCryptPasswordEncoder(10));
return delegating;
}
New passwords encode as {argon2}.... Existing {bcrypt}... and {noop}... hashes continue to authenticate.
Upgrading Passwords on Login
When a user authenticates successfully, check upgradeEncoding. If true, re-hash the password and persist the new hash:
@Service
public class AuthService {
private final PasswordEncoder passwordEncoder;
private final UserRepository userRepository;
public void upgradePasswordIfNeeded(User user, String rawPassword) {
if (passwordEncoder.upgradeEncoding(user.getPassword())) {
String upgraded = passwordEncoder.encode(rawPassword);
user.setPassword(upgraded);
userRepository.save(user);
}
}
}
Wire this into your AuthenticationSuccessHandler or a custom UserDetailsService:
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException(username));
// UserDetailsPasswordService handles upgrade automatically
return buildUserDetails(user);
}
// Implement UserDetailsPasswordService to get automatic upgrade from Spring Security
@Service
public class UserDetailsServiceImpl implements UserDetailsService, UserDetailsPasswordService {
@Override
public UserDetails updatePassword(UserDetails user, String newEncodedPassword) {
userRepository.updatePassword(user.getUsername(), newEncodedPassword);
return User.withUserDetails(user)
.password(newEncodedPassword)
.build();
}
}
When you implement UserDetailsPasswordService, Spring Security automatically calls updatePassword whenever upgradeEncoding returns true — no extra code needed in the authentication flow.
Migrating Legacy Plaintext Passwords
Never store plaintext passwords — but if you inherited a database that does, migration must happen without locking out users.
Option 1: Prefix with {noop} — Migrate on Login
Set stored passwords as {noop}actualpassword. Add noop to your DelegatingPasswordEncoder. On the next login, upgradeEncoding returns true and the password is re-hashed transparently.
UPDATE users SET password = CONCAT('{noop}', password)
WHERE password NOT LIKE '{%}%';
Option 2: Double Hash — Hash the Existing Hash
If you have MD5/SHA1 hashes and cannot expose the original plaintext, wrap the existing hash: store bcrypt(md5hash) and re-hash on first login:
// Temporary encoder during migration window
public class LegacyMd5ToBcryptEncoder implements PasswordEncoder {
private final BCryptPasswordEncoder bcrypt = new BCryptPasswordEncoder();
@Override
public String encode(CharSequence raw) {
return bcrypt.encode(md5(raw));
}
@Override
public boolean matches(CharSequence raw, String encoded) {
return bcrypt.matches(md5(raw), encoded);
}
private String md5(CharSequence input) {
// MD5 of the input — used only for matching legacy hashes
try {
MessageDigest md = MessageDigest.getInstance("MD5");
return HexFormat.of().formatHex(md.digest(input.toString().getBytes()));
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}
}
Register this as a prefixed encoder and migrate on login.
Security Configuration
Wire the encoder into the AuthenticationManager:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
UserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder);
return new ProviderManager(provider);
}
}
Do not inject PasswordEncoder into the same @Configuration class that defines SecurityFilterChain if UserDetailsService is also defined there — circular bean dependencies arise. Define encoder, UserDetailsService, and SecurityFilterChain in separate classes or be careful about dependency order.
Common Mistakes
Storing Encoded Passwords Without a Prefix
If you encode with BCryptPasswordEncoder directly (not through DelegatingPasswordEncoder) and store the result without a {bcrypt} prefix, you cannot migrate to DelegatingPasswordEncoder later without a database migration. Start with the delegating encoder from day one.
Re-Encoding on Every Request
encode is slow by design. Call it only at registration, password change, and password upgrade — never on every authentication request. matches is what you call during authentication.
Using MD5 or SHA1
These are fast hashes — trivially brute-forced with modern GPUs. Use BCrypt, Argon2, SCrypt, or PBKDF2. Nothing else.
Ignoring upgradeEncoding
If you increase the BCrypt cost factor from 10 to 12, existing hashes are not automatically re-hashed. Implement UserDetailsPasswordService so upgrades happen automatically on next login.
Key Takeaways
PasswordEncoderhas three methods:encode,matches, andupgradeEncoding- BCrypt is the default and correct choice for most applications; Argon2 is stronger but requires Bouncy Castle
DelegatingPasswordEncoderprefixes stored hashes with the algorithm identifier, enabling safe algorithm upgrades- Implement
UserDetailsPasswordServiceto trigger automatic re-hashing whenupgradeEncodingreturns true - Never use MD5 or SHA1 for passwords; never store plaintext
- Start with
PasswordEncoderFactories.createDelegatingPasswordEncoder()from day one
Next: Password Management: Registration, Reset, and Migration — building secure registration flows, email-based password reset, and bulk migration strategies.