Spring BootBCryptPasswordEncoder
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 101,024 iterations → ~100ms
// Strength 112,048 iterations → ~200ms
// Strength 124,096 iterations → ~400ms
// Strength 138,192 iterations → ~800ms
// Strength 1416,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()));
    }
}