Contents
- Custom UserDetails Implementation
- Custom UserDetailsService
- PasswordEncoder Configuration
- Wiring into SecurityFilterChain
- Custom AuthenticationProvider
- Multiple Authentication Sources
Implement UserDetails to carry not just the username and password but also your domain-specific fields (user ID, tenant ID, display name, etc.). This is the object placed in the SecurityContext and accessible via authentication.principal.
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
public class AppUserDetails implements UserDetails {
private final Long id;
private final String username;
private final String password;
private final String email;
private final boolean enabled;
private final List<GrantedAuthority> authorities;
public AppUserDetails(AppUser user) {
this.id = user.getId();
this.username = user.getUsername();
this.password = user.getPasswordHash();
this.email = user.getEmail();
this.enabled = user.isEnabled();
this.authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.name()))
.collect(java.util.stream.Collectors.toList());
}
// Custom getters for your domain fields
public Long getId() { return id; }
public String getEmail(){ return email; }
// UserDetails contract
@Override public String getUsername() { return username; }
@Override public String getPassword() { return password; }
@Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; }
@Override public boolean isEnabled() { return enabled; }
@Override public boolean isAccountNonExpired() { return true; }
@Override public boolean isAccountNonLocked() { return true; }
@Override public boolean isCredentialsNonExpired(){ return true; }
}
Implement UserDetailsService with a single method: loadUserByUsername(String username). Spring Security calls this during form-login and HTTP Basic authentication to load the user for credential verification.
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class AppUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public AppUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
AppUser user = userRepository.findByUsernameIgnoreCase(username)
.orElseThrow(() -> new UsernameNotFoundException(
"User not found: " + username));
if (!user.isEnabled()) {
throw new DisabledException("Account is disabled: " + username);
}
return new AppUserDetails(user);
}
}
Always throw UsernameNotFoundException (not a generic exception) when the user is not found. Spring Security catches this and maps it to an authentication failure, preventing information leakage about whether a username exists.
Never store plain-text passwords. Expose a PasswordEncoder bean — Spring Security uses it automatically to verify passwords during authentication and for encoding when you register users.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class PasswordConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // strength 12 — ~300ms per hash on modern hardware
}
}
// Use DelegatingPasswordEncoder to support multiple algorithms during migration
@Bean
public PasswordEncoder passwordEncoder() {
// Stores id prefix: {bcrypt}$2a$10$..., {noop}plaintext, {pbkdf2}...
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
// Encoding when saving a new user
@Service
public class UserRegistrationService {
private final UserRepository repo;
private final PasswordEncoder encoder;
public void register(RegisterRequest req) {
AppUser user = new AppUser();
user.setUsername(req.username());
user.setPasswordHash(encoder.encode(req.password())); // always encode before saving
user.setRoles(Set.of(Role.USER));
repo.save(user);
}
}
Spring Security auto-detects your UserDetailsService and PasswordEncoder beans and wires them into a DaoAuthenticationProvider automatically. You only need explicit wiring if you have multiple providers or want to customise the provider.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
// Expose AuthenticationManager for use in login endpoints
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated());
return http.build();
}
}
// Typical login endpoint — uses the auto-configured AuthenticationManager
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AuthenticationManager authManager;
private final JwtService jwtService;
@PostMapping("/login")
public ResponseEntity<TokenResponse> login(@RequestBody LoginRequest req) {
Authentication auth = authManager.authenticate(
new UsernamePasswordAuthenticationToken(req.username(), req.password())
);
AppUserDetails principal = (AppUserDetails) auth.getPrincipal();
String token = jwtService.generateToken(principal);
return ResponseEntity.ok(new TokenResponse(token));
}
}
Use a custom AuthenticationProvider when you need authentication logic that goes beyond username/password lookup — for example, OTP verification, API key validation, or multi-factor checks.
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
private final AppUserDetailsService userDetailsService;
private final PasswordEncoder passwordEncoder;
private final OtpService otpService;
public CustomAuthenticationProvider(AppUserDetailsService uds,
PasswordEncoder pe,
OtpService otp) {
this.userDetailsService = uds;
this.passwordEncoder = pe;
this.otpService = otp;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
AppUserDetails user = (AppUserDetails) userDetailsService.loadUserByUsername(username);
if (!passwordEncoder.matches(password, user.getPassword())) {
throw new BadCredentialsException("Invalid password for: " + username);
}
// Extra step: verify OTP if the user has MFA enabled
String otp = ((MfaAuthenticationToken) authentication).getOtp();
if (!otpService.verify(user.getId(), otp)) {
throw new BadCredentialsException("Invalid OTP");
}
return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
// Only handle MfaAuthenticationToken, not plain UsernamePasswordAuthenticationToken
return MfaAuthenticationToken.class.isAssignableFrom(authentication);
}
}
Register multiple AuthenticationProvider beans to support different authentication schemes (e.g., username/password for the UI, API key for machine-to-machine calls). Spring Security tries each provider in order until one succeeds.
@Configuration
public class MultiAuthConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http,
DaoAuthenticationProvider daoProvider,
ApiKeyAuthenticationProvider apiKeyProvider) throws Exception {
http
.authenticationProvider(daoProvider) // tried first
.authenticationProvider(apiKeyProvider) // tried second
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
return http.build();
}
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider(
AppUserDetailsService uds, PasswordEncoder pe) {
DaoAuthenticationProvider p = new DaoAuthenticationProvider();
p.setUserDetailsService(uds);
p.setPasswordEncoder(pe);
return p;
}
}