Master Custom Authentication Providers in Spring Security 6

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.

  1. AuthenticationFilter: Intercepts the request.
  2. AuthenticationManager: The interface defining how authentication is processed.
  3. ProviderManager: The standard implementation that iterates through a list of providers.
  4. 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:

  1. authenticate: Contains the logic to verify credentials.
  2. supports: Tells Spring Security which token type this provider can handle (usually UsernamePasswordAuthenticationToken).

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.

EndpointCredentials UsedExpected HTTP StatusExplanation
/public/statusNone200 OKPublicly accessible.
/user/dashboarduser / password200 OKAuthenticated as User.
/admin/paneluser / password403 ForbiddenValid login, but insufficient role.
/admin/paneladmin / secret200 OKAuthenticated as Admin.
/profileinvalid / invalid401 UnauthorizedAuthentication 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 your CustomAuthProvider.
  • 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 authenticate method.

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?

FeatureBuilt-in (DaoAuthenticationProvider)Custom AuthenticationProvider
Data SourceJDBC, JPA, LDAPAny (API, Legacy DB, File, RPC)
ComplexityLow (Configuration only)Medium (Requires coding)
FlexibilityLimited to standard schemasInfinite
MaintenanceHandled by Spring TeamHandled 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).

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.