đ Spring Security, Part IV â Custom Authentication with API Key
đ Part IV â Building Custom API Key Authentication with Spring Security
đ Spring Security, Part IVâââCustom Authentication with API Key
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âre about to build.
In this post, weâre going to implement a custom authentication mechanism in Spring Security using API keys. While this is a relatively rare use case (since most applications rely on authentication types like Basic Auth, JWT, OAuth2, OIDC, etc.), implementing a custom solution gives you valuable insight into how Spring Security works under the hood. And who knowsâââsomeday, you might need it.
đ What Youâll Learn
What is Authentication in Spring Security
Key interfaces:
Authentication,AuthenticationManager,AuthenticationProviderUnderstanding
SecurityContextHolderHow to build a custom filter (
ApiKeyAuthenticationFilter)End-to-end implementation of API Key Authentication
đ§ What is Authentication?
Authentication is the process of identifying who the client is.
In Spring Security, itâs represented by the Authentication interface:
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}If youâre implementing custom authentication, youâll need to create your own implementation of this interface.
đ§± Key Concepts in Spring Security
Spring Security is filter-based, and these filters delegate authentication to various components:
đ Authentication Flow
A request hits the server.
It goes through a chain of filters.
If a filter is responsible for authentication (like
UsernamePasswordAuthenticationFilter), it delegates the process to theAuthenticationManager.AuthenticationManageruses an appropriateAuthenticationProvider.If authentication is successful, a
SecurityContextis populated.
đ Project Setup
Hereâs the build.gradle.kts 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")
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 Behaviour
By default, Spring Security protects all routes with HTTP Basic authentication. If you run the app and request /api/orders, youâll receive:
But check your logsâââSpring generates a default password:
Using generated security password: 8cbecab7-4d14-4c38-b9ca-2d072b8ba8f6Use this to authenticate, and youâll get:
This the default behaviour of Spring Security with BasicAuth.
đ Implementing API Key Authentication
Letâs now implement a fully working API Key Authentication mechanism.
â
Step 1: Custom Authentication Implementation
To represent API key-based authentication, we create a class that implements Spring Securityâs Authenticationinterface.
Since we arenât using credentials, roles, or user details, most of the interface methods will return either null or empty collections.
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 = apiKey
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
Spring Security delegates authentication logic to an AuthenticationProvider. Our custom provider will:
Check if the authentication object is of type
ApiKeyAuthenticationValidate the API key against a pre-configured value from
application.yml
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.api.secret-key}")
private val apiKey: String,
) : AuthenticationProvider {
override fun authenticate(authentication: Authentication): Authentication {
val apiKeyAuthentication = authentication as ApiKeyAuthentication
if (apiKeyAuthentication.apiKey != apiKey) {
throw BadCredentialsException("API key is incorrect!")
}
return ApiKeyAuthentication(apiKey, true)
}
override fun supports(authentication: Class<*>): Boolean =
authentication == ApiKeyAuthentication::class.java
}# application.yml
authentication:
api:
secret-key: "some_secret_key" # Only for testing/demo purposesâ If the API key is valid, the request is marked as authenticated; otherwise, a
BadCredentialsExceptionis thrown.
â
3. ApiKeyAuthenticationManager â Coordinating Authentication
The AuthenticationManager is responsible for delegating authentication to the appropriate AuthenticationProvider.
Since our app uses only API key authentication, this manager simply checks whether the provider supports the given authentication type.
package com.atomic.coding.config.manager
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.AuthenticationProvider
import org.springframework.security.authentication.AuthenticationServiceException
import org.springframework.security.core.Authentication
import org.springframework.stereotype.Component
@Component
class ApiKeyAuthenticationManager(
private val authenticationProvider: AuthenticationProvider
) : AuthenticationManager {
override fun authenticate(authentication: Authentication): Authentication {
if (authenticationProvider.supports(authentication.javaClass)) {
return authenticationProvider.authenticate(authentication)
}
throw AuthenticationServiceException("No suitable AuthenticationProvider found")
}
}â This setup allows flexibility to support multiple providers in the future.
â
4. ApiKeyAuthenticationFilter â Intercepting HTTP Requests
Spring Security is filter-based. Every request flows through a chain of filters. We add our custom ApiKeyAuthenticationFilter to:
Extract the API key from the
X-API-Keyrequest header.If no API key is provided, simply continue the filter chain to allow other filters or public endpoints to handle the request
Create an
ApiKeyAuthenticationobject with the extracted key.Delegate authentication to the
AuthenticationManager.If authentication succeeds, populate the
SecurityContextwith the authenticated object.If authentication fails, respond with a 401 Unauthorized error and a JSON message.
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.slf4j.LoggerFactory
import org.springframework.http.MediaType
import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.core.AuthenticationException
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter
import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter
@Component
class ApiKeyAuthenticationFilter(
private val authenticationManager: AuthenticationManager,
) : OncePerRequestFilter() {
private val logger = LoggerFactory.getLogger(ApiKeyAuthenticationFilter::class.java)
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
val apiKey = request.getHeader("X-API-Key")
if (apiKey == null) {
logger.info("No API key provided; proceeding without authentication")
filterChain.doFilter(request, response)
return
}
try {
val authRequest = ApiKeyAuthentication(apiKey, false)
val authResult: Authentication = authenticationManager.authenticate(authRequest)
SecurityContextHolder.getContext().authentication = authResult
filterChain.doFilter(request, response)
} catch (ex: AuthenticationException) {
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"}""")
}
}
}â If no key is provided, the filter simply passes the request on if some endpoints are public. If the key is invalid, it returns a
401 Unauthorizedwith a JSON response.
â
5. SecurityConfig â Registering the Filter
Finally, we wire everything up by registering our custom filter in the Spring Security filter chain.
We place ApiKeyAuthenticationFilter before Springâs default BasicAuthenticationFilter to ensure it processes requests first.
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
.addFilterAt(apiKeyAuthenticationFilter, BasicAuthenticationFilter::class.java)
.authorizeHttpRequests { requests ->
requests.anyRequest().authenticated()
}
.build()
}â All endpoints are now protected by API key authentication, and unauthenticated requests will be rejected.
đ§Ș Testing the Setup
Try sending requests using Postman, curl, or HTTPie:
No auth provided: 403 Forbidden â
Basic Auth: â Unsupported Authentication Type
Providing the wrong ApiKey:
401 Unthorizedâ
Providing the right ApiKey:
200 OKâ
â
Recap: What We Built
Custom
Authenticationclass (ApiKeyAuthentication)Custom
AuthenticationProviderCustom
AuthenticationManageCustom
Filter(ApiKeyAuthenticationFilter)Integrated everything in
SecurityConfig
This structure mirrors how Spring Security handles authentication flows and gives you flexibility to plug in any custom logic you might need.
đź Whatâs Next?
In the next tutorial, weâll explore:
đ Multiple Authentication Providers (e.g., database + external service)
đ Authorization Strategies:
Entry-point level
Method-level
Weâre stepping into enterprise-level security architecture, so stay tuned!
If you found this guide helpful, consider liking, sharing, or commenting below.
Thanks for readingâââsee you in the next part! đ










