Spring Security is the de facto standard for securing Java applications, offering robust support for OAuth2, LDAP, and standard form logins. But what happens when “standard” isn’t enough?
You might face a scenario where user credentials live in a legacy mainframe, a proprietary API, or a multi-tenant database with complex sharding rules. In these cases, the default DaoAuthenticationProvider fails. Trying to hack standard filters to fit these requirements leads to brittle code, security vulnerabilities, and maintenance nightmares.
The solution lies in Spring Security’s modular architecture. By implementing a Custom Authentication Provider, you can inject your specific business logic directly into the security lifecycle without compromising the framework’s integrity.
In this guide, we will build a production-ready Custom Authentication Provider using Spring Boot 3 and Spring Security 6. We will cover the architecture, implementation details, configuration, and critical edge cases you must handle.
Understanding the Architecture
Before writing code, it is crucial to understand where your custom logic fits. Spring Security uses a delegation model.
- AuthenticationFilter: Intercepts the request.
- AuthenticationManager: The interface defining how authentication is processed.
- ProviderManager: The standard implementation that iterates through a list of providers.
- AuthenticationProvider: The component that actually validates the user.
When you create a custom provider, you are essentially adding a new “voter” to the ProviderManager.
Note: If your custom provider returns null, the manager will try the next provider in the chain. If it throws an exception, authentication fails immediately.
Step 1: Project Setup and Dependencies
To get started, ensure you have a Spring Boot 3 application set up. We will use Maven for dependency management.
Add the following to your pom.xml. Note that we are using spring-boot-starter-security.
<properties>
<java.version>17</java.version>
<spring-boot.version>3.2.0</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Step 2: Implementing the Custom Authentication Provider
The heart of this implementation is the AuthenticationProvider interface. You must override two methods:
authenticate: Contains the logic to verify credentials.supports: Tells Spring Security which token type this provider can handle (usuallyUsernamePasswordAuthenticationToken).
The Code
Create a file named CustomAuthProvider.java.
package com.ankurm.security.auth;
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.core.authority.SimpleGrantedAuthority;
import java.util.ArrayList;
import java.util.List;
public class CustomAuthProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 1. Extract credentials from the authentication object
String username = authentication.getName();
String password = authentication.getCredentials().toString();
// 2. VALIDATION LOGIC
// In a real app, inject a Service or Repository here to check DB/External API
if ("admin".equals(username) && "secret".equals(password)) {
return createSuccessfulAuth(username, password, "ROLE_ADMIN", "ROLE_USER");
} else if ("user".equals(username) && "password".equals(password)) {
return createSuccessfulAuth(username, password, "ROLE_USER");
}
// 3. Fail explicitly if credentials don't match
throw new BadCredentialsException("Invalid credentials provided");
}
// Helper method to construct the authenticated token
private Authentication createSuccessfulAuth(String username, String password, String... roles) {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (String role : roles) {
authorities.add(new SimpleGrantedAuthority(role));
}
// IMPORTANT: The third argument (authorities) marks the token as authenticated
return new UsernamePasswordAuthenticationToken(username, password, authorities);
}
@Override
public boolean supports(Class<?> authenticationType) {
// Ensure this provider only processes standard username/password tokens
return authenticationType.equals(UsernamePasswordAuthenticationToken.class);
}
}
Step 3: Wiring the Configuration (Spring Security 6)
In Spring Boot 3, the WebSecurityConfigurerAdapter is deprecated. We must now move to a component-based configuration using SecurityFilterChain.
We need to register our CustomAuthProvider with the AuthenticationManager and define our authorization rules.
package com.ankurm.security.config;
import com.ankurm.security.auth.CustomAuthProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import java.util.Collections;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
/**
* Register the CustomAuthProvider as a Bean.
*/
@Bean
public CustomAuthProvider customAuthProvider() {
return new CustomAuthProvider();
}
/**
* Define the AuthenticationManager.
* We manually create a ProviderManager and pass our custom provider.
*/
@Bean
public AuthenticationManager authenticationManager() {
return new ProviderManager(Collections.singletonList(customAuthProvider()));
}
/**
* The Security Filter Chain defining URL protection.
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// Explicitly wire the AuthenticationManager
.authenticationManager(authenticationManager())
// Define URL Authorization rules
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
)
// Enable Form Login
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
// Enable Logout
.logout(logout -> logout
.permitAll()
);
return http.build();
}
}
Why This Configuration Matters
By explicitly defining the AuthenticationManager bean with new ProviderManager(...), you are telling Spring to ignore its default auto-configuration for authentication and use only your custom logic. This prevents standard in-memory users or default JDBC behaviors from interfering.
Step 4: Verification and Testing
To ensure our provider works, we will create a REST controller with endpoints protected by different roles.
package com.ankurm.security.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@GetMapping("/public/status")
public String publicEndpoint() {
return "Public access - System Healthy";
}
@GetMapping("/user/dashboard")
public String userDashboard() {
return "User content - Visible to USER and ADMIN";
}
@GetMapping("/admin/panel")
public String adminPanel() {
return "Admin Content - RESTRICTED";
}
}
Testing Scenarios Matrix
Use curl or Postman to verify these results.
| Endpoint | Credentials Used | Expected HTTP Status | Explanation |
/public/status | None | 200 OK | Publicly accessible. |
/user/dashboard | user / password | 200 OK | Authenticated as User. |
/admin/panel | user / password | 403 Forbidden | Valid login, but insufficient role. |
/admin/panel | admin / secret | 200 OK | Authenticated as Admin. |
/profile | invalid / invalid | 401 Unauthorized | Authentication failed. |
Critical Edge Cases & Best Practices
While the code above works for demonstration, a production environment requires stricter controls.
1. Password Encoding
Never compare plain text passwords in production logic (like password.equals("secret")).
- The Fix: Inject a
PasswordEncoder(like BCrypt) into yourCustomAuthProvider. - Implementation: Use
passwordEncoder.matches(rawPassword, encodedPasswordFromDB).
2. Erasing Credentials
Spring Security attempts to erase credentials (passwords) from the Authentication object after successful login to prevent heap dumps from leaking secrets.
- The Pitfall: If your custom provider relies on the password after the authentication process, you might find it null. Ensure you finish all credential validation inside the
authenticatemethod.
3. Exception Handling
Be specific with exceptions. Throwing BadCredentialsException is standard, but you might also need LockedException (for too many attempts) or DisabledException (for inactive users). Spring’s default authentication failure handler maps these to specific HTTP error messages.
External Resource: For a deep dive into exception types, refer to the official Spring Security Exception Hierarchy documentation.
Comparison: Custom vs. Built-in Providers
When should you actually exert the effort to build what we just built?
| Feature | Built-in (DaoAuthenticationProvider) | Custom AuthenticationProvider |
| Data Source | JDBC, JPA, LDAP | Any (API, Legacy DB, File, RPC) |
| Complexity | Low (Configuration only) | Medium (Requires coding) |
| Flexibility | Limited to standard schemas | Infinite |
| Maintenance | Handled by Spring Team | Handled by You |
Conclusion
Implementing a custom authentication provider in Spring Security empowers you to bridge modern security frameworks with legacy or complex user stores. By adhering to the AuthenticationProvider contract and correctly wiring the SecurityFilterChain, you maintain a clean separation of concerns while solving difficult business requirements.
Next Steps: Now that you have a working provider, try integrating a PasswordEncoder to hash your secrets, or connect your provider to an external REST API for validation.
Frequently Asked Questions
What is the difference between AuthenticationProvider and UserDetailsService?
UserDetailsService is a lighter interface used only to retrieve user data (username, password, authorities) from a database. The standard DaoAuthenticationProvider uses it. You implement AuthenticationProvider when you need to customize the actual verification logic (how passwords are checked) or if your user data isn’t in a database compatible with UserDetails.
Can I have multiple Authentication Providers in Spring Security?
Yes. The ProviderManager iterates through a list of providers. You can configure multiple providers (e.g., one for LDAP and one for a custom DB). Spring will try them in order until one successfully authenticates the user or all fail.
How do I handle third-party API authentication failures in a custom provider?
If your custom provider calls an external API and that API is down, you should throw an AuthenticationServiceException. This signals a system error rather than a “Bad Credentials” error, allowing you to handle the failure gracefully (e.g., by showing a “Service Unavailable” page).