Spring Boot
BCryptPasswordEncoder
BCryptPasswordEncoder is Spring Security's recommended PasswordEncoder. It applies the bcrypt adaptive hashing algorithm, automatically generates and embeds a unique salt in every hash, and uses a configurable work factor to control computational cost. This entry covers configuration, strength tuning, usage patterns, migration, and testing.
Configuration and Bean Setup
Declare BCryptPasswordEncoder as a @Bean in a @Configuration class. Inject it wherever passwords need to be encoded or verified. Define it once and share it across the application — instantiating a new BCryptPasswordEncoder per request is wasteful and loses any warm-up benefits. The default strength is 10; production systems should use 12 or higher.
Java
// ── Standard bean declaration ─────────────────────────────────────────
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // strength 12
}
// ── With explicit SecureRandom ────────────────────────────────────
@Bean
public PasswordEncoder secureBcrypt() {
return new BCryptPasswordEncoder(
12,
new SecureRandom() // explicit CSPRNG — default is fine
);
}
}
// ── Strength selection ────────────────────────────────────────────────
// BCrypt computes 2^strength iterations.
// Strength 10 → 1,024 iterations → ~100ms
// Strength 11 → 2,048 iterations → ~200ms
// Strength 12 → 4,096 iterations → ~400ms
// Strength 13 → 8,192 iterations → ~800ms
// Strength 14 → 16,384 iterations → ~1,600ms
//
// Rule: highest strength where login latency stays under 500ms.
// Benchmark on your target hardware before choosing.
// ── Benchmarking helper ───────────────────────────────────────────────
@Component
@Slf4j
public class BcryptBenchmark {
public void benchmark() {
String sample = "SamplePassword123!";
for (int strength = 10; strength <= 14; strength++) {
BCryptPasswordEncoder encoder =
new BCryptPasswordEncoder(strength);
long start = System.currentTimeMillis();
encoder.encode(sample);
long elapsed = System.currentTimeMillis() - start;
log.info("Strength {}: {}ms", strength, elapsed);
}
}
}Encoding and Verifying Passwords
Encode a password once at registration or password-change time and store the hash. Never store the raw password. Verify with matches() — bcrypt extracts the embedded salt from the stored hash and re-hashes the candidate, so every encode() call produces a different hash for the same input.
Java
@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {
private final UserRepository userRepo;
private final PasswordEncoder passwordEncoder;
// ── Registration — encode once, store hash ────────────────────────
@Transactional
public UserResponse register(RegisterRequest request) {
if (userRepo.existsByEmail(request.email())) {
throw new DuplicateEmailException(request.email());
}
User user = new User();
user.setEmail(request.email().toLowerCase().trim());
user.setPassword(passwordEncoder.encode(request.password()));
user.setRoles(Set.of("USER"));
return UserResponse.from(userRepo.save(user));
}
// ── Password change ────────────────────────────────────────────────
@Transactional
public void changePassword(Long userId,
ChangePasswordRequest request) {
User user = userRepo.findById(userId).orElseThrow();
// Verify current password before allowing change
if (!passwordEncoder.matches(
request.currentPassword(), user.getPassword())) {
throw new InvalidPasswordException(
"Current password is incorrect");
}
user.setPassword(
passwordEncoder.encode(request.newPassword()));
userRepo.save(user);
}
// ── Password reset (token-based) ──────────────────────────────────
@Transactional
public void resetPassword(String resetToken, String newPassword) {
PasswordResetToken token = tokenRepo
.findByToken(resetToken)
.filter(t -> t.getExpiresAt().isAfter(Instant.now()))
.orElseThrow(() ->
new InvalidTokenException("Reset token invalid or expired"));
User user = token.getUser();
user.setPassword(passwordEncoder.encode(newPassword));
userRepo.save(user);
tokenRepo.delete(token); // one-time use
}
// ── Why encode() ≠ encode() for the same input ────────────────────
@Autowired
void demonstrateSalt(PasswordEncoder enc) {
String hash1 = enc.encode("myPassword");
String hash2 = enc.encode("myPassword");
// hash1 != hash2 — different salts embedded
// but enc.matches("myPassword", hash1) == true
// enc.matches("myPassword", hash2) == true
}
}Upgrading Weak Hashes
Applications migrating from MD5, SHA-1, or a lower bcrypt strength can upgrade hashes transparently at login. Implement UserDetailsPasswordService — Spring Security calls updatePassword() when upgradeEncoding() returns true, re-encoding with the current encoder and saving the updated hash without requiring a password reset.
Java
// ── UserDetailsService + UserDetailsPasswordService ──────────────────
@Service
@RequiredArgsConstructor
@Slf4j
public class UpgradingUserDetailsService
implements UserDetailsService, UserDetailsPasswordService {
private final UserRepository userRepo;
private final PasswordEncoder passwordEncoder; // strength-12 bcrypt
@Override
@Transactional(readOnly = true)
public UserDetails loadUserByUsername(String email)
throws UsernameNotFoundException {
User user = userRepo.findByEmail(email).orElseThrow(() ->
new UsernameNotFoundException("Invalid credentials"));
return AuthenticatedUser.from(user);
}
// ── Called by DaoAuthenticationProvider when upgradeEncoding()
// returns true for the stored hash ───────────────────────────────
@Override
@Transactional
public UserDetails updatePassword(UserDetails details,
String newEncodedPassword) {
User user = userRepo.findByEmail(details.getUsername())
.orElseThrow();
user.setPassword(newEncodedPassword);
userRepo.save(user);
log.info("Password hash upgraded for: {}",
details.getUsername());
return AuthenticatedUser.from(user);
}
}
// ── upgradeEncoding() — detect weak hashes ───────────────────────────
// BCryptPasswordEncoder.upgradeEncoding() returns true when the stored
// hash has a lower work factor than the configured encoder:
BCryptPasswordEncoder enc = new BCryptPasswordEncoder(12);
String weakHash = new BCryptPasswordEncoder(10).encode("pass");
enc.upgradeEncoding(weakHash); // true — strength 10 < 12
// ── Migration from MD5 via DelegatingPasswordEncoder ─────────────────
@Bean
public PasswordEncoder migrationEncoder() {
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put("bcrypt", new BCryptPasswordEncoder(12));
encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
// New passwords encoded with bcrypt
// Existing {MD5}... hashes still verified
// On next login upgradeEncoding() triggers re-encode to bcrypt
return new DelegatingPasswordEncoder("bcrypt", encoders);
}Testing with BCrypt
BCrypt's intentional slowness makes tests that encode passwords on every run noticeably slow. Use NoOpPasswordEncoder in test profiles, or pre-compute test hashes at class load time and reuse them. Never use NoOpPasswordEncoder in any non-test configuration.
Java
// ── Test configuration — fast encoder ────────────────────────────────
@TestConfiguration
public class TestSecurityConfig {
@Bean
@Primary
public PasswordEncoder testPasswordEncoder() {
// Plain text — tests only, never production
return NoOpPasswordEncoder.getInstance();
}
}
// ── Pre-computed hash — encode once per test class ────────────────────
@SpringBootTest
class UserServiceTest {
@Autowired
private UserService userService;
@Autowired
private UserRepository userRepo;
// Encode once at class level — not per test
private static final BCryptPasswordEncoder ENCODER =
new BCryptPasswordEncoder(4); // strength 4 — fast for tests
private static final String HASH =
ENCODER.encode("TestPassword123!");
@BeforeEach
void setUp() {
userRepo.deleteAll();
User user = new User();
user.setEmail("test@example.com");
user.setPassword(HASH); // pre-computed
user.setRoles(Set.of("USER"));
user.setEnabled(true);
userRepo.save(user);
}
@Test
void passwordMatchesStoredHash() {
assertTrue(ENCODER.matches(
"TestPassword123!", HASH));
}
@Test
void wrongPasswordDoesNotMatch() {
assertFalse(ENCODER.matches(
"WrongPassword", HASH));
}
@Test
void changePasswordUpdatesHash() {
userService.changePassword(
userRepo.findByEmail("test@example.com")
.orElseThrow().getId(),
new ChangePasswordRequest(
"TestPassword123!", "NewPassword456!"));
User updated = userRepo
.findByEmail("test@example.com").orElseThrow();
assertTrue(ENCODER.matches(
"NewPassword456!", updated.getPassword()));
assertFalse(ENCODER.matches(
"TestPassword123!", updated.getPassword()));
}
}