AuthenticationManager, AuthenticationProvider, and UserDetailsService
The Authentication Delegation Chain
Spring Security separates requesting authentication from performing authentication. A filter receives a username and password — but it does not check them itself. It passes the request to AuthenticationManager, which delegates to one or more AuthenticationProvider instances:
flowchart LR
F[Authentication Filter\nForm Login / Basic / JWT] -->|UsernamePasswordToken| AM[AuthenticationManager\nProviderManager]
AM -->|try each provider| AP1[DaoAuthenticationProvider\nfor DB users]
AM -->|try each provider| AP2[LdapAuthenticationProvider\nfor LDAP users]
AM -->|try each provider| AP3[Custom Provider\nfor API key]
AP1 -->|if supported| UDS[UserDetailsService\nloadUserByUsername]
UDS --> DB[(Database)]
AP1 -->|verify password| PE[PasswordEncoder]
AP1 -->|success| Token[Authenticated Token]
Token --> F
This chain is the core of Spring Security’s extensibility — you can plug in any number of authentication mechanisms without touching the filter or the rest of the framework.
AuthenticationManager
AuthenticationManager has a single method:
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
Input: an unauthenticated Authentication (e.g., UsernamePasswordAuthenticationToken with username and plaintext password).
Output: an authenticated Authentication (same type, but with isAuthenticated() = true and populated authorities), or an AuthenticationException if authentication fails.
The standard implementation is ProviderManager.
ProviderManager: The Standard AuthenticationManager
ProviderManager holds a list of AuthenticationProvider instances. When authenticate() is called, it iterates through the list and tries each provider:
flowchart TD
PM[ProviderManager] -->|for each provider| Check{supports this\nAuthentication type?}
Check -->|no| Next[try next provider]
Check -->|yes| Try[provider.authenticate]
Try -->|success| Return[return authenticated token]
Try -->|AuthenticationException| Next
Next --> Check
Next -->|no more providers| Parent{parent\nProviderManager?}
Parent -->|yes| PM2[parent.authenticate]
Parent -->|no| Throw[throw ProviderNotFoundException]
// ProviderManager selects providers by type
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
Parent AuthenticationManager
ProviderManager can have a parent. If none of its own providers can handle the token, it delegates to the parent. This supports hierarchical authentication — e.g., per-HttpSecurity managers that fall back to a global one.
AuthenticationProvider
AuthenticationProvider does the actual credential verification:
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
boolean supports(Class<?> authentication); // which token types can this handle?
}
DaoAuthenticationProvider
The most common provider — handles UsernamePasswordAuthenticationToken by loading the user from a UserDetailsService and verifying the password with a PasswordEncoder:
sequenceDiagram
participant PM as ProviderManager
participant DAP as DaoAuthenticationProvider
participant UDS as UserDetailsService
participant PE as PasswordEncoder
PM->>DAP: authenticate(token{username="alice", password="secret"})
DAP->>UDS: loadUserByUsername("alice")
UDS-->>DAP: UserDetails{username="alice", password="$2a$...", roles=[ROLE_USER]}
DAP->>PE: matches("secret", "$2a$...")
PE-->>DAP: true
DAP->>DAP: check account non-expired, non-locked, credentials valid
DAP-->>PM: UsernamePasswordAuthToken{principal=UserDetails, authorities=[ROLE_USER]}
The password is matched against the stored (hashed) password. If they match and the account is in good standing, an authenticated token is returned with the user’s authorities.
UserDetailsService
UserDetailsService is the bridge between Spring Security and your database:
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
It has one job: given a username, return a UserDetails object (or throw UsernameNotFoundException). Spring Security handles everything else — password checking, account status validation, authority population.
Database-Backed Implementation
@Service
@RequiredArgsConstructor
public class AppUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
@Transactional(readOnly = true)
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsernameIgnoreCase(username)
.orElseThrow(() -> new UsernameNotFoundException(
"User not found: " + username
));
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPasswordHash())
.roles(user.getRoles().stream()
.map(Role::getName)
.toArray(String[]::new))
.accountExpired(!user.isActive())
.accountLocked(user.isLocked())
.credentialsExpired(user.isPasswordExpired())
.disabled(!user.isEnabled())
.build();
}
}
Using a Custom UserDetails
For richer user data (like the user ID needed for authorization), use a custom UserDetails:
@Service
@RequiredArgsConstructor
public class AppUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
@Transactional(readOnly = true)
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByEmailIgnoreCase(username) // email as username
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
return new AppUserDetails(user); // custom UserDetails with getId(), getEmail(), etc.
}
}
UserDetailsPasswordService
An optional extension for upgrading stored password hashes without requiring users to re-authenticate:
public interface UserDetailsPasswordService {
UserDetails updatePassword(UserDetails user, String newPassword);
}
When DaoAuthenticationProvider detects that the stored password is encoded with an outdated algorithm (via PasswordEncoder.upgradeEncoding()), it calls updatePassword() with the re-encoded hash. Implement this on your UserDetailsService to transparently migrate password hashes.
Wiring It Together: The Security Configuration
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final AppUserDetailsService userDetailsService;
private final PasswordEncoder passwordEncoder;
// Expose AuthenticationManager as a bean (needed for manual authenticate() calls)
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
// Configure DaoAuthenticationProvider explicitly
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder);
return provider;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authenticationProvider(authenticationProvider())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
)
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
}
Why Expose AuthenticationManager as a Bean?
Spring Boot auto-configures the AuthenticationManager internally. But if you need to call authenticationManager.authenticate(token) manually — for example, in a login endpoint that generates a JWT — you need it as an injectable bean:
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtService jwtService;
@PostMapping("/login")
public JwtResponse login(@RequestBody @Valid LoginRequest request) {
// Manually trigger authentication
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getEmail(),
request.getPassword()
)
);
// Authentication succeeded — generate JWT
AppUserDetails user = (AppUserDetails) authentication.getPrincipal();
String token = jwtService.generateToken(user);
return new JwtResponse(token);
}
}
Custom AuthenticationProvider
Build a custom provider for non-standard authentication — API keys, hardware tokens, custom protocols:
@Component
public class ApiKeyAuthenticationProvider implements AuthenticationProvider {
private final ApiKeyRepository apiKeyRepository;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
ApiKeyAuthenticationToken token = (ApiKeyAuthenticationToken) authentication;
String apiKey = (String) token.getCredentials();
ApiKeyEntity entity = apiKeyRepository.findByKeyHash(hashKey(apiKey))
.orElseThrow(() -> new BadCredentialsException("Invalid API key"));
if (!entity.isActive()) {
throw new DisabledException("API key is disabled");
}
return new ApiKeyAuthenticationToken(
entity, // principal
apiKey, // credentials
entity.getAuthorities() // authorities from the API key's associated roles
);
}
@Override
public boolean supports(Class<?> authentication) {
return ApiKeyAuthenticationToken.class.isAssignableFrom(authentication);
}
private String hashKey(String key) {
// SHA-256 hash of the key — never store raw API keys
return DigestUtils.sha256Hex(key);
}
}
// Custom token type
public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal;
private final String credentials;
// Pre-authentication (unauthenticated)
public ApiKeyAuthenticationToken(String apiKey) {
super(null);
this.principal = null;
this.credentials = apiKey;
setAuthenticated(false);
}
// Post-authentication (authenticated)
public ApiKeyAuthenticationToken(Object principal, String credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(true);
}
@Override public Object getPrincipal() { return principal; }
@Override public Object getCredentials() { return credentials; }
}
Multiple AuthenticationProviders
Register multiple providers to support multiple authentication mechanisms in one chain:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authenticationProvider(daoAuthenticationProvider()) // username/password
.authenticationProvider(apiKeyAuthenticationProvider()) // API keys
.authenticationProvider(ldapAuthenticationProvider()) // LDAP
...
return http.build();
}
ProviderManager tries each provider in order until one succeeds or all fail.
AuthenticationException Types
Different failures throw different exceptions — all extend AuthenticationException:
classDiagram
AuthenticationException <|-- BadCredentialsException
AuthenticationException <|-- UsernameNotFoundException
AuthenticationException <|-- DisabledException
AuthenticationException <|-- LockedException
AuthenticationException <|-- AccountExpiredException
AuthenticationException <|-- CredentialsExpiredException
AuthenticationException <|-- ProviderNotFoundException
class BadCredentialsException {
Wrong password
}
class UsernameNotFoundException {
User not found
}
class DisabledException {
Account disabled
}
class LockedException {
Account locked
}
Security note: UsernameNotFoundException is internally converted to BadCredentialsException by DaoAuthenticationProvider by default (controlled by hideUserNotFoundExceptions). This prevents username enumeration — an attacker can’t tell whether a username doesn’t exist or the password is wrong.
// Keep username enumeration protection (default = true, recommended)
provider.setHideUserNotFoundExceptions(true);
UserDetails Account Status Checks
DaoAuthenticationProvider checks all four boolean flags on UserDetails before authentication succeeds:
public interface UserDetails {
boolean isAccountNonExpired(); // false → AccountExpiredException
boolean isAccountNonLocked(); // false → LockedException
boolean isCredentialsNonExpired(); // false → CredentialsExpiredException
boolean isEnabled(); // false → DisabledException
}
Use these for: account suspension (disabled), security lockout after failed attempts (locked), forced password rotation (credentialsExpired), account expiry for trial users (accountExpired).
Summary
flowchart TD
Filter[Authentication Filter] -->|unauthenticated token| PM[ProviderManager\nAuthenticationManager]
PM -->|iterate providers| P1[DaoAuthenticationProvider]
PM -->|iterate providers| P2[Custom Provider]
P1 -->|loadUserByUsername| UDS[UserDetailsService]
UDS --> DB[(Database)]
P1 -->|verify password| PE[PasswordEncoder]
P1 -->|check account flags| Flags[isEnabled, isLocked...]
P1 -->|success| Auth[Authenticated Token\nwith UserDetails + Authorities]
Auth --> Filter
Filter -->|setAuthentication| SCH[SecurityContextHolder]
AuthenticationManager(implemented byProviderManager) coordinates — it doesn’t authenticate itself.AuthenticationProviderdoes the actual verification —supports()determines which token types it handles.UserDetailsService.loadUserByUsername()is the bridge to your database.DaoAuthenticationProviderchainsUserDetailsService+PasswordEncoder+ account status checks.- Expose
AuthenticationManageras a bean when you need to callauthenticate()manually (e.g., JWT login endpoint). hideUserNotFoundExceptions = true(default) prevents username enumeration attacks.
Next: Article 5 covers the modern SecurityFilterChain configuration API — every HttpSecurity option explained with real examples.