Custom AuthenticationProvider in Spring Security: DAOs and Custom Providers
Learn to implement custom AuthenticationProvider in Spring Security for non-standard auth flows.
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
- ✓Basic knowledge of Spring Security
- ✓Familiarity with dependency injection in Spring
- ✓Understanding of authentication and authorization concepts
- Use AuthenticationProvider for custom authentication logic like LDAP, legacy systems, or multi-factor auth.
- Implement the
authenticate()andsupports()methods. - Register the provider in the security configuration.
- Avoid common mistakes like forgetting to set authentication object details or misusing
supports(). - In production, ensure thread-safety and handle exceptions properly.
Think of AuthenticationProvider as a custom lock for your app's front door. Spring Security has standard locks (like username/password), but if you need a fingerprint scanner or a voice recognition system, you build your own lock. The authenticate method checks if the key fits, and supports tells the system which doors this lock works on.
If you've ever worked with Spring Security, you know it handles standard username/password authentication out of the box. But what happens when your boss says, 'We need to authenticate users against our legacy mainframe system' or 'We're adding multi-factor authentication using one-time codes'? That's where the default DaoAuthenticationProvider falls short.
I've seen too many teams try to hack around this by overriding UserDetailsService or stuffing custom logic into filters. That's a recipe for security holes and unmaintainable code. The right tool for the job is a custom AuthenticationProvider.
In this article, I'll show you how to implement a custom AuthenticationProvider from scratch. We'll use a realistic example: authenticating users against a third-party REST API that returns a token. You'll learn the contract, the pitfalls, and what the official docs gloss over.
By the end, you'll have a production-ready provider that you can drop into any Spring Boot application. Let's get our hands dirty.
Understanding the AuthenticationProvider Contract
Before we write any code, you need to understand what Spring Security expects from an AuthenticationProvider. The interface is deceptively simple: two methods. But getting them wrong will cause authentication to silently fail or bypass your custom logic entirely.
The method receives an authenticate()Authentication object (typically a token representing the user's credentials) and must return a fully authenticated Authentication object if successful. If authentication fails, throw an AuthenticationException. If the provider doesn't support the given token, return null — this is critical.
The method tells Spring Security whether this provider can handle a given token class. If you return supports()false, your provider will never be called for that token type. Most implementations check authentication.getClass().isAssignableFrom(UsernamePasswordAuthenticationToken.class). But don't be too narrow — you might want to support custom tokens too.
Here's the gotcha: if multiple providers claim to support the same token class, Spring Security will iterate through them in order. The first one that returns a non-null Authentication wins. If all return null, authentication fails with ProviderNotFoundException. So if your provider returns null for a token it claimed to support, you'll get an error.
authenticate() must return a fully populated Authentication object or throw an exception; supports() must correctly indicate token compatibility.Registering Your Custom Provider
Creating the provider class is only half the battle. You need to register it with Spring Security's AuthenticationManager. There are two common approaches: using a custom AuthenticationManagerBuilder in a WebSecurityConfigurerAdapter (or its replacement in Spring Security 5.7+), or exposing the provider as a bean and injecting it.
In modern Spring Boot (2.7+), the recommended way is to create a @Configuration class that extends WebSecurityConfigurerAdapter or, even better, use the lambda DSL. Let me show you both.
First, the traditional way with WebSecurityConfigurerAdapter (still works but deprecated in 3.1):
```java @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomAuthenticationProvider authProvider;
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(authProvider); } } ```
If you're using the new lambda DSL (Spring Security 5.7+), you can do this:
``java @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authenticationProvider(new ``CustomAuthenticationProvider()) .authorizeHttpRequests(authz -> authz .anyRequest().authenticated() ) .formLogin(); return http.build(); } }
Notice I'm creating a new instance of the provider in the lambda DSL. That's fine if the provider is stateless. If it has dependencies, inject them via constructor and create the bean in a @Bean method.
One common mistake I see: developers forget to add @Component to their provider or forget to inject it. The provider simply never gets called, and they waste hours debugging why authentication fails.
@Component with singleton scope. Stateless providers are preferred.AuthenticationManagerBuilder or by using the lambda DSL. Ensure the provider is a Spring bean to get dependency injection.Real-World Example: Token-Based Authentication with External API
Let's build something you'll actually use. Imagine you're integrating with a third-party identity provider that returns a JWT token. The user sends username/password, your provider calls the external API, gets a token, and then sets that token in the security context.
First, let's create a service to call the external API. I'll use RestTemplate for simplicity, but in production you'd use WebClient with proper timeouts.
```java @Service public class ExternalAuthService { private final RestTemplate restTemplate;
public ExternalAuthService() { this.restTemplate = new RestTemplate(); restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory()); }
public String authenticate(String username, String password) { // Call external API String url = "https://auth.example.com/login"; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); Map<String, String> body = Map.of("username", username, "password", password); HttpEntity<Map<String, String>> request = new HttpEntity<>(body, headers);
ResponseEntity<Map> response = restTemplate.postForEntity(url, request, Map.class); if (response.getStatusCode() == HttpStatus.OK) { return response.getBody().get("token").toString(); } else { throw new BadCredentialsException("Authentication failed"); } } } ```
Now the provider:
```java @Component public class TokenAuthenticationProvider implements AuthenticationProvider {
private final ExternalAuthService authService;
public TokenAuthenticationProvider(ExternalAuthService authService) { this.authService = authService; }
@Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String username = authentication.getName(); String password = authentication.getCredentials().toString();
String token = authService.authenticate(username, password);
// Create a custom principal that holds the token UserPrincipal principal = new UserPrincipal(username, token); return new UsernamePasswordAuthenticationToken(principal, null, List.of(new SimpleGrantedAuthority("ROLE_USER"))); }
@Override public boolean supports(Class<?> authentication) { return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); } } ```
Notice I'm setting the credentials to null in the returned token. This is a security best practice: once authenticated, you don't need to store the password. The token is stored in the principal, which you can retrieve later.
One thing the docs don't tell you: if you set credentials to null, Spring Security will not erase them again (since they're already null). This avoids a second null assignment.
Chaining Multiple AuthenticationProviders
In many applications, you need to support multiple authentication mechanisms. For example, you might have a legacy system using username/password and a new system using OAuth2 tokens. Spring Security allows you to chain multiple providers.
When you register multiple providers, Spring Security iterates through them in the order they were added. The first provider that returns a non-null Authentication wins. If a provider returns null, the next one is tried. If all return null, authentication fails.
Here's how to configure multiple providers:
``java @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authenticationProvider(new ``LdapAuthenticationProvider()) .authenticationProvider(new DatabaseAuthenticationProvider()) .authorizeHttpRequests(authz -> authz .anyRequest().authenticated() ) .formLogin(); return http.build(); } }
But be careful: if two providers support the same token type, the order matters. Also, if a provider throws an exception, the chain stops and authentication fails immediately. So if you want fallback behavior, don't throw exceptions for unsupported tokens — return null instead.
I once worked on a project where the team had a provider that threw BadCredentialsException for invalid credentials, but they wanted to fall back to a second provider. They didn't realize that exceptions stop the chain. We had to refactor to return null for tokens that weren't meant for that provider.
What the Official Docs Won't Tell You
I've been using Spring Security since the Acegi days, and I've seen the same mistakes repeated over and over. Here are the things the official documentation glosses over.
1. Thread Safety is Your Responsibility
The official docs mention that AuthenticationProvider instances are shared across threads, but they don't emphasize it enough. If your provider has any mutable state (like a SimpleDateFormat or a non-thread-safe cache), you'll get intermittent failures. I once debugged a production issue where a provider used HashMap without synchronization, causing occasional NullPointerException under load. Always use thread-safe constructs or make your provider stateless.
2. The Method Can Be Trickysupports()
You might think should return supports()true for all token types you can handle. But if you return true for a broad class like Authentication.class, your provider will be called for every authentication request, even those meant for other providers. This can cause performance issues or unexpected behavior. Be as specific as possible.
3. Setting Credentials to Null
Spring Security has a feature that erases credentials after authentication to prevent accidental exposure. But if you set credentials to null in your returned token, it won't try to erase them again. However, if you leave them non-null, Spring Security will erase them, but only if the token is an instance of UsernamePasswordAuthenticationToken. If you use a custom token, you need to handle erasure yourself.
4. The AuthenticationManager is a Singleton
There is only one AuthenticationManager per security filter chain. If you have multiple filter chains (e.g., for different URL patterns), each has its own manager and provider list. This is important when you have different authentication requirements for different parts of your app.
5. ProviderNotFoundException is Silent
If no provider supports a given token, Spring Security throws ProviderNotFoundException. But this exception is often caught and translated to a generic 'Authentication failed' message. This can be confusing because the user might have valid credentials, but the token type is wrong. Always check your methods.supports()
supports(), credential erasure, or the singleton nature of AuthenticationManager. Keep these in mind to avoid production issues.Testing Your Custom AuthenticationProvider
You should never deploy a custom AuthenticationProvider without testing it thoroughly. Here's how I test mine.
First, unit test the provider in isolation. Mock the external dependencies and verify that: - Valid credentials return a non-null Authentication with correct authorities. - Invalid credentials throw the appropriate exception. - Unsupported token types cause the provider to return null.
```java @ExtendWith(MockitoExtension.class) class CustomAuthenticationProviderTest {
@Mock private ExternalAuthService authService;
@InjectMocks private CustomAuthenticationProvider provider;
@Test void testAuthenticateSuccess() { when(authService.authenticate("user", "pass")).thenReturn("token123"); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("user", "pass"); Authentication result = provider.authenticate(token); assertNotNull(result); assertTrue(result.isAuthenticated()); assertEquals("user", result.getName()); assertEquals(1, result.getAuthorities().size()); }
@Test void testAuthenticateFailure() { when(authService.authenticate("user", "wrong")).thenThrow(new BadCredentialsException("Invalid")); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("user", "wrong"); assertThrows(BadCredentialsException.class, () -> provider.authenticate(token)); }
@Test void testSupports() { assertTrue(provider.supports(UsernamePasswordAuthenticationToken.class)); assertFalse(provider.supports(OtherToken.class)); } } ```
Second, integration test with the full Spring context. Use @SpringBootTest and TestRestTemplate to send requests and verify authentication works end-to-end.
```java @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class AuthenticationIntegrationTest {
@Autowired private TestRestTemplate restTemplate;
@Test void testLoginSuccess() { ResponseEntity<String> response = restTemplate.postForEntity("/login", Map.of("username", "user", "password", "pass"), String.class); assertEquals(200, response.getStatusCodeValue()); }
@Test void testLoginFailure() { ResponseEntity<String> response = restTemplate.postForEntity("/login", Map.of("username", "user", "password", "wrong"), String.class); assertEquals(401, response.getStatusCodeValue()); } } ```
Don't forget to test concurrency. Use an executor service to simulate multiple simultaneous authentication requests and ensure no race conditions occur.
authenticate().The Midnight Outage: AuthenticationProvider Thread Safety
SimpleDateFormat instance that was not thread-safe. Under high concurrency, date parsing corrupted the token validation logic.SimpleDateFormat with DateTimeFormatter (immutable and thread-safe) and added synchronization around the external API call to prevent race conditions on the connection pool.- Always ensure AuthenticationProvider implementations are thread-safe; they are called by multiple threads.
- Avoid mutable shared state; prefer stateless providers or use immutable objects.
- External API calls in authentication should have proper timeouts and retry mechanisms.
- Test under load with concurrent requests to uncover race conditions.
- Log authentication failures with enough context to debug, but never log credentials.
supports() returns true for the token class. Also verify that the authenticate() method throws the correct exception type (e.g., BadCredentialsException).AuthenticationManagerBuilder.authenticationProvider() or define a bean in a @Configuration class.SimpleDateFormat, Calendar, or non-thread-safe collections. Also verify external service timeouts.authenticate() method, ensure you set the GrantedAuthority list correctly on the UsernamePasswordAuthenticationToken. Also verify that the UserDetails object is populated.curl -v https://your-api/login -d 'username=test&password=test'Check logs for 'Authenticated user' or 'Authentication failed'| File | Command / Code | Purpose |
|---|---|---|
| AuthenticationProviderExample.java | @Component | Understanding the AuthenticationProvider Contract |
| SecurityConfig.java | @Configuration | Registering Your Custom Provider |
| TokenAuthenticationProvider.java | @Component | Real-World Example |
| MultiProviderConfig.java | @Configuration | Chaining Multiple AuthenticationProviders |
| CustomAuthenticationProviderTest.java | @ExtendWith(MockitoExtension.class) | Testing Your Custom AuthenticationProvider |
Key takeaways
Interview Questions on This Topic
Explain the AuthenticationProvider interface and its two methods.
authenticate(Authentication) and supports(Class<?>). authenticate() performs authentication and returns a fully populated Authentication object on success, or throws AuthenticationException on failure. supports() indicates whether the provider can handle a given token class. If supports() returns false, the provider will not be called for that token.Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
That's Spring Security. Mark it forged?
6 min read · try the examples if you haven't