How authentication actually resolves
AuthenticationManager, ProviderManager and the delegation chain behind a login.
Open this lesson in the learning hubKey points
- A filter does not authenticate. It builds an unauthenticated
Authenticationtoken and hands it to theAuthenticationManager- the filter knows how to read a request, nothing more. - The usual manager is a
ProviderManager, which holds a list ofAuthenticationProviders and asks each whether itsupportsthe token type, stopping at the first that can handle it. - That is the extension point. A custom provider - API keys, a legacy password store, a hardware token - plugs in without touching any filter.
- A provider returns a fully authenticated token with authorities, or throws. Returning null means "not mine", and the manager moves on to the next provider.
- The result is placed in the
SecurityContextHolder, which is aThreadLocalby default - so it does not follow work onto an @Async thread or an executor without explicit propagation. - Erase credentials after authenticating.
ProviderManagerdoes this by default, and turning it off leaves passwords sitting in the security context for the life of the request.
Example
/*
* THE CHAIN, top to bottom:
*
* UsernamePasswordAuthenticationFilter
* -> builds UsernamePasswordAuthenticationToken (unauthenticated)
* -> AuthenticationManager.authenticate(token)
* -> ProviderManager loops its providers
* -> DaoAuthenticationProvider.supports(token)? yes
* -> UserDetailsService.loadUserByUsername()
* -> PasswordEncoder.matches()
* -> returns an AUTHENTICATED token with authorities
* -> SecurityContextHolder.setContext(...)
*/
// A custom provider - no filter changes needed.
@Component
class ApiKeyAuthenticationProvider implements AuthenticationProvider {
private final ApiKeyService keys;
ApiKeyAuthenticationProvider(ApiKeyService keys) { this.keys = keys; }
@Override
public Authentication authenticate(Authentication authentication) {
String presented = (String) authentication.getCredentials();
// Constant-time compare inside: a length-sensitive equals leaks
// the key one character at a time under timing analysis.
ApiKey key = keys.findValid(presented)
.orElseThrow(() -> new BadCredentialsException("Unknown API key"));
return new ApiKeyAuthenticationToken(key.owner(), key.authorities()); // authenticated
}
@Override
public boolean supports(Class<?> type) {
return ApiKeyAuthenticationToken.class.isAssignableFrom(type);
}
}
// The context does NOT follow work onto another thread.
@Bean
DelegatingSecurityContextAsyncTaskExecutor securityAwareExecutor(ThreadPoolTaskExecutor d) {
// Without this, an @Async method sees an empty SecurityContext and every
// authorization check inside it fails or silently sees an anonymous user.
return new DelegatingSecurityContextAsyncTaskExecutor(d);
}
Filters read requests, providers authenticate - add a provider rather than a filter, and remember the context is a ThreadLocal.
This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Spring Security course, and every lesson in it is listed on the Spring Security contents page.