š Spring Security, Part V ā Implementing Multiple Authentication Providers
Need Both Basic Auth and API Key Support? Here's How to Wire Them Together in Spring Security
Welcome back to the Spring Security Series!
If you havenāt read the earlier tutorials, I recommend checking them out firstāāāthey lay the groundwork for what weāll build in this post.
š” Scenario
Imagine you have a Spring Boot application secured with Basic Auth or Form Login. Now, you need to support an additional mechanismāāāsay, API Key Authentication.
How do you support both seamlessly, allowing clients to choose either method to authenticate?
In this tutorial, weāll implement two authentication mechanisms:
Basic Auth
API Key Auth
This setup is especially useful when:
External users access your API via Basic Auth.
Internal services (within the same cluster) use API keys.
ā
What Youāll Learn
How
AuthenticationManagermanages authenticationHow to implement a custom
AuthenticationManagerthat supports multipleAuthenticationProvidersHow to set up an
AuthenticationFilterto support both typesHow to build a full Spring Boot app supporting multiple authentication mechanisms
š Authentication Flow in Spring Security
All authentication flows in Spring Security are filter-based. Filters are responsible for both authenticating and authorizing incoming requests.
Hereās how it works:
When an HTTP request hits an endpoint, Spring Security applies a chain of filters before the request reaches your controller.
Depending on the authentication type, the appropriate filter is used.
For example, theBasicAuthenticationFilterhandles Basic Auth requests.The selected filter delegates authentication to the
AuthenticationManager, which further delegates it to the appropriateAuthenticationProvider.
š ļø What Weāll Implement
To support multiple authentication mechanisms, we need:
ApiKeyFilterApiKeyAuthenticationCustomAuthenticationManagerApiKeyAuthenticationProvider
Note:
CustomAuthenticationManageris an internal object managed by Spring. If you donāt define one explicitly, Spring provides a default. However, defining your own will override the default.
Only oneAuthenticationManagercan exist perSecurityFilterChain.
š Project Setup
Hereās the build.gradle.kts file you'll need:
plugins {
kotlin("jvm") version "1.9.25"
kotlin("plugin.spring") version "1.9.25"
id("org.springframework.boot") version "3.5.4"
id("io.spring.dependency-management") version "1.1.7"
}
group = "com.atomic.coding"
version = "0.0.1-SNAPSHOT"
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testImplementation("org.springframework.security:spring-security-test")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
kotlin {
compilerOptions {
freeCompilerArgs.addAll("-Xjsr305=strict")
}
}
tasks.withType<Test> {
useJUnitPlatform()
}š¦ Simple Controller for Testing
@RestController
@RequestMapping("/api/orders")
class OrderController(
private val orderService: OrderService
) {
@GetMapping
fun orders(): List<Order> {
return orderService.orders()
}
}š Default Spring Security Behavior
By default, Spring Security protects all routes using HTTP Basic authentication. When you start the app and hit /api/orders, youāll see:
But check your logsāāāSpring generates a default password:
Using generated security password: 8cbecab7-4d14-4c38-b9ca-2d072b8ba8f6Use this default password to authenticate and youāll receive:
By default, Spring Security secures all endpoints using Basic Authentication with an auto-generated in-memory user.
In the next section, weāll extend this behavior to support both Basic Auth and API Key Authentication, allowing clients to authenticate using either mechanism.
š Implementing Multi-Authentication
Letās now implement a fully functional app supporting both API Key Authentication and Basic Auth.
ā
Step 1: Custom Authentication for API Key
To represent API key-based authentication, we create a class that implements Spring Securityās Authentication interface. Since we arenāt using credentials, roles or user details, most of the interface methods will return either null or empty collections for ApiKeyAuthentication.
package com.atomic.coding.config.authentication
import org.springframework.security.core.Authentication
import org.springframework.security.core.GrantedAuthority
class ApiKeyAuthentication(
val apiKey: String,
private val authenticated: Boolean
) : Authentication {
override fun getName(): String? = null
override fun getAuthorities(): List<GrantedAuthority> = emptyList()
override fun getCredentials(): Any? = null
override fun getDetails(): Any? = null
override fun getPrincipal(): Any? = null
override fun isAuthenticated(): Boolean = authenticated
override fun setAuthenticated(isAuthenticated: Boolean) {}
}ā This class acts as a wrapper for the API key and tracks whether the request is authenticated.
ā
2. ApiKeyAuthenticationProvider ā Verifying the API Key
Once a request reaches the authentication layer, Spring Security delegates the authentication task to the AuthenticationManager, which in turn delegates it to an AuthenticationProvider.
Each AuthenticationProvider is responsible for handling a specific authentication mechanism.
Spring defines this interface as:
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
boolean supports(Class<?> authentication);
}In our case, we need a custom implementation to validate requests that use an API key. Our custom ApiKeyAuthenticationProvider does two things:
authenticate(Authentication)ā Validates the API key against a pre-configured secret defined inapplication.yml.supports(Class<?>)ā Indicates that this provider only supportsApiKeyAuthenticationobjects.
ā
Implementation: ApiKeyAuthenticationProvider
package com.atomic.coding.config.provider
import com.atomic.coding.config.authentication.ApiKeyAuthentication
import org.springframework.beans.factory.annotation.Value
import org.springframework.security.authentication.AuthenticationProvider
import org.springframework.security.authentication.BadCredentialsException
import org.springframework.security.core.Authentication
import org.springframework.stereotype.Component
@Component
class ApiKeyAuthenticationProvider(
@Value("\${authentication.secret.api-key}")
private val apiKey: String,
) : AuthenticationProvider {
override fun authenticate(authentication: Authentication?): Authentication {
val apiKeyAuthentication = authentication as ApiKeyAuthentication
if (apiKeyAuthentication.apiKey == apiKey) {
return ApiKeyAuthentication(apiKey, true)
}
throw BadCredentialsException("Invalid API key")
}
override fun supports(authentication: Class<*>?): Boolean =
authentication == ApiKeyAuthentication::class.java
}š ļø Configuration: application.yml
spring:
application:
name: multi-providers
authentication:
secret:
api-key: some_secret_keyā
3. DaoAuthenticationProvider ā Verifying Users Against the Database
In previous parts of this series, we discussed how UserDetailsService and PasswordEncoder work together to authenticate users. When using username and password-based authentication (like Basic Auth or Form Login), Spring Security relies on a built-in class called DaoAuthenticationProvider.
Since our application needs to support two authentication mechanisms (API Key and Basic Auth), we must explicitly provide a DaoAuthenticationProvider to handle user authentication via username and password.
š What is DaoAuthenticationProvider?
DaoAuthenticationProvider is a Spring Security implementation of the AuthenticationProvider interface. It performs authentication by:
Calling the provided
UserDetailsServiceto load user details from memory or a database.Using the configured
PasswordEncoderto verify passwords.
Normally, Spring configures this automatically. But because weāre providing a custom
AuthenticationManager, we need to manually declare and register theDaoAuthenticationProvider.
ā
Implementation: Configuring DaoAuthenticationProvider
@Configuration
class DaoAuthenticationProviderConfig {
@Bean
fun inMemoryUserDetailsManager(): UserDetailsService {
val user = User.builder()
.username("alice")
.password(passwordEncoder().encode("qwerty"))
.roles("USER")
.build()
return InMemoryUserDetailsManager(user)
}
@Bean
fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()
@Bean
fun daoAuthenticationProvider(userDetailsService: UserDetailsService): DaoAuthenticationProvider =
DaoAuthenticationProvider(userDetailsService)
}š§ Explanation:
UserDetailsService: For simplicity, we useInMemoryUserDetailsManagerto simulate a user store.PasswordEncoder: We useBCryptPasswordEncoder, a secure password hashing algorithm.DaoAuthenticationProvider: Combines both to validate credentials during Basic Auth.
ā
4. CustomAuthenticationManager ā Coordinating Authentication
The CustomAuthenticationManager is the core component that coordinates which AuthenticationProvider should handle each authentication request.
Since our application supports two different authentication mechanisms(API Key and Basic Auth), we need a manager that:
Identifies which authentication type is being used.
Delegates the request to the correct
AuthenticationProvider.Throws an error if no provider supports the given authentication type.
š§ Implementation
package com.atomic.coding.config.manager
import com.atomic.coding.config.provider.ApiKeyAuthenticationProvider
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.BadCredentialsException
import org.springframework.security.authentication.dao.DaoAuthenticationProvider
import org.springframework.security.core.Authentication
import org.springframework.stereotype.Component
@Component
class CustomAuthenticationManager(
private val apiKeyAuthenticationProvider: ApiKeyAuthenticationProvider,
private val daoAuthenticationProvider: DaoAuthenticationProvider,
) : AuthenticationManager {
override fun authenticate(authentication: Authentication): Authentication = when {
apiKeyAuthenticationProvider.supports(authentication::class.java) ->
apiKeyAuthenticationProvider.authenticate(authentication)
daoAuthenticationProvider.supports(authentication::class.java) ->
daoAuthenticationProvider.authenticate(authentication)
else -> throw BadCredentialsException("Unsupported authentication type: ${authentication.javaClass.simpleName}")
}
}š§ Explanation
The
authenticate()method is invoked for every request that requires authentication.
2. It checks:
If the request is of type
ApiKeyAuthentication, itās delegated toApiKeyAuthenticationProvider.If the request is of type
UsernamePasswordAuthenticationToken(used by Basic Auth), itās handled byDaoAuthenticationProvider.
3. If no provider supports the authentication type, a BadCredentialsException is thrown, resulting in a 401 Unauthorized response.
š Why use a custom AuthenticationManager?
By default, Spring Security auto-configures its own AuthenticationManager. But when we introduce multiple custom providers, we take full control of the decision-making process with a custom implementation like this.
ā
5. ApiKeyAuthenticationFilter ā Intercepting HTTP Requests
As previously discussed, Spring Security is filter-based. Every incoming request flows through a chain of filters , each responsible for handling a specific security concern.
In our case, we introduce a custom filter, ApiKeyAuthenticationFilter, to handle API Key-based authentication.
š§ Implementation
package com.atomic.coding.config.filter
import com.atomic.coding.config.authentication.ApiKeyAuthentication
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.http.MediaType
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.core.AuthenticationException
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter
@Component
class ApiKeyAuthenticationFilter(
private val authenticationManager: AuthenticationManager,
) : OncePerRequestFilter() {
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
val apiKey = request.getHeader("X-Api-Key")
// If no API key is provided, continue the filter chain.
if (apiKey == null) {
logger.info("API key is missing, passing request to next filter")
filterChain.doFilter(request, response)
return
}
try {
// Attempt API key authentication
val apiKeyAuthentication = authenticationManager.authenticate(ApiKeyAuthentication(apiKey, false))
SecurityContextHolder.getContext().authentication = apiKeyAuthentication
filterChain.doFilter(request, response)
} catch (ex: AuthenticationException) {
// Authentication failed
SecurityContextHolder.clearContext()
logger.error("Authentication failed: ${ex.message}")
response.status = HttpServletResponse.SC_UNAUTHORIZED
response.contentType = MediaType.APPLICATION_JSON_VALUE
response.writer.write("""{"error": "Authentication error"}""")
}
}
}š What This Filter Does
Hereās a breakdown of its responsibilities:
Extracts the API key from the
X-Api-KeyHTTP header.If no API key is found:
It simply passes the request along the filter chain.
This allows other filters (like the
BasicAuthenticationFilter) to handle the authenticationāāāenabling multiple auth mechanisms to coexist.
3. If an API key is provided:
It creates an
ApiKeyAuthenticationobject.It delegates authentication to the
AuthenticationManager.On success, it sets the authenticated object in the
SecurityContext.On failure, it responds with
401 Unauthorizedand a simple JSON error message.
ā
6. SecurityConfig ā Registering the Filter
The final step is to wire everything together by registering our custom filter within the Spring Security filter chain.
We ensure that our ApiKeyAuthenticationFilter is evaluated beforeSpring's built-in BasicAuthenticationFilter. This guarantees that API key authentication is attempted first, but doesn't block Basic Auth if no API key is provided.
š Implementation
package com.atomic.coding.config
import com.atomic.coding.config.filter.ApiKeyAuthenticationFilter
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter
@Configuration
class SecurityConfig(
private val apiKeyAuthenticationFilter: ApiKeyAuthenticationFilter
) {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain = http
.addFilterBefore(apiKeyAuthenticationFilter, BasicAuthenticationFilter::class.java)
.httpBasic { } // Enable Basic Auth
.authorizeHttpRequests { it.anyRequest().authenticated() } // Require auth for all endpoints
.build()
}š Key Notes
We explicitly call
.httpBasic { }to enable Basic Authentication.The
.addFilterBefore(...)method inserts our customApiKeyAuthenticationFilterbefore the built-inBasicAuthenticationFilter.All incoming HTTP requests are now required to be authenticated, whether via:
X-API-Keyheader, orBasic Auth credentials.
ā
Result
With this setup:
š All endpoints are secured.
ā Valid API keys or Basic Auth credentials will be accepted.
ā Any unauthenticated request will result in a
401 Unauthorizedor403 Forbidden, depending on context.
š§Ŗ Testing the Setup
Try sending requests using Postman, curl, or HTTPie:
No auth provided: 403 Forbidden ā
Basic Auth with right user credentials:
200 OKā
Basic Auth with wrong user credentials:
401 Unthorizedā
Providing the right ApiKey:
200 OKā
Providing the wrong ApiKey:
401 Unthorizedā
Recap: What we built:
Custom
Authenticationclass (ApiKeyAuthentication)Custom
AuthenticationProviderclass (ApiKeyAuthenticationProvider)Custom AuthenticationManager that supports both
ApiKeyAuthenticationandBasicAuthCustom
Filter(ApiKeyAuthenticationFilter)Integrated everything in
SecurityConfig
š® Whatās Next?
In the next tutorial, weāll explore:
š Authorization Strategies:
Authority vs Role
Entry-point level
Method-level
Weāre stepping into deeper Spring Security details, so stay tuned!
If you found this guide helpful, consider liking, sharing, or commenting below.
Thanks for readingāāāsee you in the next part! š










