Spring Security: Custom UserDetailsService & AuthenticationProvider
Spring Security's authentication system is built around two extension points: UserDetailsService for loading user data by username, and AuthenticationProvider for complete control over how a credential is verified. Understanding both lets you connect any identity store (database, LDAP, API, or external system) to Spring Security.
Contents
Custom UserDetails Implementation
Implement UserDetailsSecurityContextauthentication.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; }
}
Custom UserDetailsService
Implement UserDetailsServiceloadUserByUsername(String username)
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);
}
}
UsernameNotFoundExceptionPasswordEncoder Configuration
Never store plain-text passwords. Expose a PasswordEncoder
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);
}
}
Wiring into SecurityFilterChain
Spring Security auto-detects your UserDetailsServicePasswordEncoderDaoAuthenticationProvider
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));
}
}
Custom AuthenticationProvider
Use a custom AuthenticationProvider
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);
}
}
Multiple Authentication Sources
Register multiple AuthenticationProvider
@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;
}
}