Authentication and Password Questions

What is UserDetailsService and how do you load users from a database?

@Service
public class ShopUserDetailsService implements UserDetailsService {
    private final UserRepository users;
    public ShopUserDetailsService(UserRepository users) { this.users = users; }

    @Override
    public UserDetails loadUserByUsername(String email) {
        User u = users.findByEmail(email)
            .orElseThrow(() -> new UsernameNotFoundException("No user " + email));
        return org.springframework.security.core.userdetails.User
            .withUsername(u.getEmail())
            .password(u.getPasswordHash())          // already BCrypt-hashed
            .roles(u.getRole().name())              // becomes ROLE_ADMIN etc.
            .build();
    }
}

DaoAuthenticationProvider calls it during login. It only loads the user; the password comparison is done by the PasswordEncoder.

Why should passwords be hashed and not encrypted?

Encryption is reversible: anyone with the key gets every password. Hashing is one-way: the server stores only the hash and compares hashes at login. Passwords must use a slow, salted algorithm (BCrypt, Argon2, SCrypt, PBKDF2), not a fast hash like MD5 or plain SHA-256, so that stolen hashes cannot be brute-forced cheaply and identical passwords do not produce identical hashes.

How does BCrypt work, and why do two hashes of the same password differ?

$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
 |  |  |---------------------||------------------------------|
 |  |  22-char salt            31-char hash
 |  cost factor 10 (2^10 rounds)
 algorithm version

Every call to encode() generates a new random salt, so the output differs each time. matches(raw, stored) reads the salt and cost from the stored string, hashes the raw password the same way and compares. The cost factor makes hashing deliberately slow (about 100 ms at 10–12). BCrypt uses only the first 72 bytes of a password.

What is DelegatingPasswordEncoder?

@Bean
PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
// encode("secret") -> {bcrypt}$2a$10$...
// can still match old hashes stored as {noop}..., {pbkdf2}..., {sha256}...

It prefixes each hash with an id like {bcrypt}, so one application can verify hashes from several algorithms. This lets you migrate to a stronger algorithm gradually: new and changed passwords use the default (bcrypt), old ones still work.

How do you implement a custom login endpoint (for JWT) instead of form login?

@PostMapping("/api/auth/login")
public TokenResponse login(@RequestBody LoginRequest req) {
    Authentication auth = authenticationManager.authenticate(
        UsernamePasswordAuthenticationToken.unauthenticated(req.email(), req.password()));
    return new TokenResponse(jwtService.issue(auth));   // throws BadCredentialsException -> 401
}

@Bean
AuthenticationManager authenticationManager(UserDetailsService uds, PasswordEncoder enc) {
    DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
    provider.setUserDetailsService(uds);
    provider.setPasswordEncoder(enc);
    return new ProviderManager(provider);
}

How do you protect login against brute-force attacks?

  • Lock the account (or add a growing delay) after N failed attempts, using AuthenticationFailureBadCredentialsEvent / AuthenticationSuccessEvent listeners.
  • Rate-limit /login per IP and per username (Bucket4j, Redis, or at the API gateway).
  • Return the same message for "unknown user" and "wrong password" to avoid user enumeration.
  • Add CAPTCHA after a few failures and offer MFA for sensitive accounts.