I was digging into Spring Security 7.1 internals while rebuilding a custom AuthenticationProvider example, and I found a behavior that I think is easy to miss:
With a single AuthenticationProvider bean, Spring Security can automatically wire it into the shared AuthenticationManager. Add a second provider bean, and that automatic wiring backs off.
That can leave you with an application that starts normally but later fails authentication with ProviderNotFoundException.
The simple case
Suppose you have:
@Bean
public CustomAuthProvider customAuthProvider() {
return new CustomAuthProvider();
}
Your provider implements:
public class CustomAuthProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) {
// custom authentication logic
}
@Override
public boolean supports(Class<?> authenticationType) {
return authenticationType.equals(
UsernamePasswordAuthenticationToken.class
);
}
}
You might expect that you need to manually create a ProviderManager and expose it as an AuthenticationManager.
You don't.
With one AuthenticationProvider bean, Spring Security's authentication configuration can discover it and add it to the shared authentication manager.
So a simple configuration can be:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authenticationProvider(customAuthProvider())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
return http.build();
}
The explicit .authenticationProvider(...) is important when you want to make the provider-to-chain relationship obvious and deterministic.
Where things get interesting
The problem appears when the application context contains more than one AuthenticationProvider.
Conceptually, Spring's authentication configuration does something like:
String[] beanNames =
context.getBeanNamesForType(AuthenticationProvider.class);
if (beanNames.length == 1) {
// automatically register the provider
}
If there are multiple provider beans, automatic discovery backs off.
That means this:
@Bean
AuthenticationProvider providerOne() {
return new CustomAuthProvider();
}
@Bean
AuthenticationProvider providerTwo() {
return new AnotherAuthProvider();
}
should not be treated as:
"Spring will automatically put both into my authentication manager."
Instead, configure the providers explicitly where you need them.
For example:
@Bean
SecurityFilterChain webSecurity(HttpSecurity http) throws Exception {
http
.authenticationProvider(providerOne())
.authenticationProvider(providerTwo())
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
return http.build();
}
Why this matters with JWT
This becomes even more interesting when the same application has:
- a custom username/password authentication mechanism
- a JWT-secured REST API
For example:
/api/** -> JWT bearer authentication
/admin/** -> custom username/password authentication
I would use separate SecurityFilterChains:
@Bean
@Order(1)
SecurityFilterChain apiSecurity(
HttpSecurity http,
JwtDecoder jwtDecoder) throws Exception {
http
.securityMatcher("/api/**")
.authorizeHttpRequests(auth ->
auth.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 ->
oauth2.jwt(jwt -> jwt.decoder(jwtDecoder))
)
.csrf(csrf -> csrf.disable());
return http.build();
}
And then:
@Bean
@Order(2)
SecurityFilterChain webSecurity(HttpSecurity http) throws Exception {
http
.authenticationProvider(customAuthProvider())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
return http.build();
}
The important concept is that the security chains are independent request-processing pipelines.
securityMatcher("/api/**") determines which chain handles the request, while @Order determines which matching chain gets the opportunity first.
That gives you a clean separation:
HTTP Request
|
v
SecurityFilterChain
selection
|
+----------------+----------------+
| |
/api/** everything else
| |
v v
Bearer token flow Form login flow
| |
v v
JwtAuthenticationProvider CustomAuthProvider
One subtle trap
Don't assume that because your API uses JWT, every authentication mechanism on that chain is JWT-only.
For example, if you add:
http.httpBasic();
to the JWT chain for "temporary testing", you have introduced another authentication mechanism into that chain.
That mechanism can use the application's authentication manager configuration and potentially reach providers you didn't intend to expose through the API.
So the question isn't only:
"Which authentication providers exist?"
It's also:
"Which authentication mechanisms are enabled on each SecurityFilterChain?"
And one more thing: don't write a custom provider unnecessarily
If your requirement is simply:
"My users are stored in a weird database table."
you probably don't need a custom AuthenticationProvider.
If UserDetailsService can represent your user lookup, use it with DaoAuthenticationProvider.
A custom provider makes sense when the actual authentication process is custom:
- legacy authentication API
- external RPC service
- hardware token
- proprietary credential format
- verification requiring business logic beyond loading
UserDetails - authentication that isn't naturally represented by
UserDetailsService
Otherwise, you're taking ownership of security code that Spring already knows how to maintain.
The takeaway
The main thing I learned from digging into this was:
Don't think of AuthenticationProvider as something you simply "register globally." Think about which AuthenticationManager and which SecurityFilterChain should actually use it.
For a simple application, automatic wiring can make everything look effortless.
For a larger application with multiple providers or multiple authentication mechanisms, explicit configuration becomes much safer and easier to reason about.
I wrote up the complete walkthrough, including:
- implementing
AuthenticationProviderfrom scratch -
ProviderManagerandAuthenticationManagerinternals - automatic provider discovery
- the multiple-provider failure mode
PasswordEncoder- authentication exceptions
- multiple
SecurityFilterChains - custom authentication + JWT
- common 401/403 and provider-wiring problems
Full article: https://ankurm.com/
I'd be particularly interested in hearing from people running multiple authentication mechanisms in the same Spring Security application: do you prefer explicit provider wiring everywhere, or do you still rely on Spring's automatic configuration when possible?
Top comments (0)