Implementing API Key Authentication in Spring Boot with Kotlin: A Complete Guide (2024)

December 30, 2024 4 min read Intermediate

API key authentication is a straightforward yet effective way to secure your APIs. In this tutorial, we'll build a complete API key authentication system using Spring Boot and Kotlin that you can adapt for your own projects. We'll create a production-ready implementation that includes key management, rate limiting, and monitoring.

Prerequisites

To follow along with this tutorial, you'll need:

  • JDK 17 or later installed
  • Basic knowledge of Kotlin and Spring Boot
  • Your favorite IDE (IntelliJ IDEA recommended)
  • Gradle or Maven build tool
  • Basic understanding of REST APIs

Understanding API Key Authentication

Before diving into the implementation, let's understand what API key authentication is and when to use it.

API key authentication is a technique where clients include a pre-shared key in their requests to identify and authenticate themselves. This key is typically sent in the request header, such as:

curl -H "X-API-Key: your-api-key-here" https://api.example.com/data

Let's begin implementing our solution.

Client API Filter API Key Service Resource X-API-Key Validate Access Invalid Key Response

Step 1 — Project Setup

First, create a new Spring Boot project with Kotlin. Add these dependencies to your build.gradle.kts:

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-security")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.springframework.boot:spring-boot-starter-validation")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    
    // For rate limiting
    implementation("io.github.bucket4j:bucket4j-core:8.1.0")
    
    // For testing
    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation("org.springframework.security:spring-security-test")
}

Step 2 — Creating the API Key Model

Let's create our API key model with expiration and rate limiting properties:

@Entity
@Table(name = "api_keys")
data class ApiKey(
    @Id
    @Column(length = 64)
    val key: String = UUID.randomUUID().toString(),
    
    @Column(nullable = false)
    val name: String,
    
    @Column(nullable = false)
    val expiresAt: LocalDateTime,
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    val role: ApiKeyRole = ApiKeyRole.ROLE_CLIENT,
    
    @Column(nullable = false)
    val enabled: Boolean = true,
    
    // Rate limit in requests per hour
    @Column(nullable = false)
    val rateLimit: Int = 1000
)

enum class ApiKeyRole {
    ROLE_CLIENT,
    ROLE_ADMIN
}

Step 3 — Implementing the API Key Filter

Create a filter to validate API keys in incoming requests:

@Component
class ApiKeyAuthFilter(
    private val apiKeyService: ApiKeyService
) : OncePerRequestFilter() {

    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        // Skip authentication for certain paths
        if (shouldNotFilter(request)) {
            filterChain.doFilter(request, response)
            return
        }

        val apiKey = request.getHeader("X-API-Key")
        
        if (apiKey == null) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "No API key provided")
            return
        }

        try {
            val authentication = apiKeyService.validateKey(apiKey)
            SecurityContextHolder.getContext().authentication = authentication
            filterChain.doFilter(request, response)
        } catch (e: InvalidApiKeyException) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.message)
        } catch (e: RateLimitExceededException) {
            response.sendError(HttpServletResponse.SC_TOO_MANY_REQUESTS, e.message)
        }
    }

    private fun shouldNotFilter(request: HttpServletRequest): Boolean {
        val path = request.servletPath
        return path.startsWith("/public/") || path == "/error"
    }
}

Step 4 — Creating the API Key Service

The service handles key validation, rate limiting, and key management:

@Service
class ApiKeyService(
    private val apiKeyRepository: ApiKeyRepository
) {
    private val rateLimiters = ConcurrentHashMap<String, Bucket>()

    fun validateKey(key: String): Authentication {
        val apiKey = apiKeyRepository.findByKey(key)
            ?: throw InvalidApiKeyException("Invalid API key")

        if (!apiKey.enabled) {
            throw InvalidApiKeyException("API key is disabled")
        }

        if (apiKey.expiresAt.isBefore(LocalDateTime.now())) {
            throw InvalidApiKeyException("API key has expired")
        }

        // Check rate limit
        val bucket = rateLimiters.computeIfAbsent(key) { k ->
            Bucket4j.builder()
                .addLimit(Bandwidth.classic(apiKey.rateLimit, Refill.intervally(apiKey.rateLimit, Duration.ofHours(1))))
                .build()
        }

        if (!bucket.tryConsume(1)) {
            throw RateLimitExceededException("Rate limit exceeded")
        }

        // Create authentication token
        val authorities = listOf(SimpleGrantedAuthority(apiKey.role.name))
        return ApiKeyAuthentication(key, authorities)
    }

    fun createApiKey(name: String, role: ApiKeyRole, rateLimit: Int): ApiKey {
        val apiKey = ApiKey(
            name = name,
            role = role,
            rateLimit = rateLimit,
            expiresAt = LocalDateTime.now().plusYears(1)
        )
        return apiKeyRepository.save(apiKey)
    }
}

Step 5 — Configuring Security

Configure Spring Security to use our API key filter:

@Configuration
@EnableWebSecurity
class SecurityConfig(
    private val apiKeyAuthFilter: ApiKeyAuthFilter
) {

    @Bean
    fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http
            .csrf { it.disable() }
            .sessionManagement { 
                it.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            }
            .addFilterBefore(apiKeyAuthFilter, UsernamePasswordAuthenticationFilter::class.java)
            .authorizeHttpRequests { auth ->
                auth
                    .requestMatchers("/public/**").permitAll()
                    .requestMatchers("/admin/**").hasRole("ADMIN")
                    .anyRequest().authenticated()
            }

        return http.build()
    }
}

Step 6 — Creating the API Key Management Endpoints

Create endpoints to manage API keys:

@RestController
@RequestMapping("/admin/api-keys")
class ApiKeyController(
    private val apiKeyService: ApiKeyService
) {
    @PostMapping
    fun createApiKey(@RequestBody request: CreateApiKeyRequest): ApiKeyResponse {
        val apiKey = apiKeyService.createApiKey(
            name = request.name,
            role = request.role,
            rateLimit = request.rateLimit
        )
        return ApiKeyResponse(
            key = apiKey.key,
            name = apiKey.name,
            expiresAt = apiKey.expiresAt,
            role = apiKey.role,
            rateLimit = apiKey.rateLimit
        )
    }

    data class CreateApiKeyRequest(
        val name: String,
        val role: ApiKeyRole,
        val rateLimit: Int
    )

    data class ApiKeyResponse(
        val key: String,
        val name: String,
        val expiresAt: LocalDateTime,
        val role: ApiKeyRole,
        val rateLimit: Int
    )
}

Step 7 — Implementing Monitoring

Add monitoring endpoints to track API key usage:

@RestController
@RequestMapping("/admin/monitoring")
class MonitoringController(
    private val apiKeyRepository: ApiKeyRepository,
    private val meterRegistry: MeterRegistry
) {
    @GetMapping("/api-keys/usage")
    fun getApiKeyUsage(): List<ApiKeyUsage> {
        return apiKeyRepository.findAll().map { apiKey ->
            val requests = meterRegistry.get("api.requests")
                .tag("apiKey", apiKey.key)
                .counter()
                .count()
            
            ApiKeyUsage(
                name = apiKey.name,
                requests = requests.toLong(),
                rateLimit = apiKey.rateLimit
            )
        }
    }

    data class ApiKeyUsage(
        val name: String,
        val requests: Long,
        val rateLimit: Int
    )
}

Testing the Implementation

Here's how to test your API key authentication:

@SpringBootTest
@AutoConfigureMockMvc
class ApiKeyAuthenticationTest {
    @Autowired
    private lateinit var mockMvc: MockMvc

    @Autowired
    private lateinit var apiKeyService: ApiKeyService

    @Test
    fun `should allow access with valid API key`() {
        val apiKey = apiKeyService.createApiKey(
            name = "Test Key",
            role = ApiKeyRole.ROLE_CLIENT,
            rateLimit = 100
        )

        mockMvc.perform(
            get("/api/data")
                .header("X-API-Key", apiKey.key)
        )
            .andExpect(status().isOk)
    }

    @Test
    fun `should deny access with invalid API key`() {
        mockMvc.perform(
            get("/api/data")
                .header("X-API-Key", "invalid-key")
        )
            .andExpect(status().isUnauthorized)
    }
}

Security Best Practices

When implementing API key authentication, follow these best practices:

  1. Always use HTTPS to prevent key interception
  2. Store API keys securely (hashed if possible)
  3. Implement key rotation policies
  4. Use appropriate key length (at least 32 characters)
  5. Monitor for suspicious usage patterns
  6. Implement rate limiting per key
  7. Set appropriate expiration dates

Conclusion

You've now implemented a production-ready API key authentication system with rate limiting and monitoring capabilities. This implementation provides a solid foundation for securing your Spring Boot APIs while maintaining good performance and scalability.

Remember to:

  • Regularly rotate API keys
  • Monitor usage patterns
  • Maintain proper documentation for API key holders
  • Implement proper error handling and logging
  • Keep your security dependencies up to date

Latest Articles

Role-Based Access Control (RBAC) in Spring Security with Kotlin

Role-Based Access Control (RBAC) in Spring Security with Kotlin

4 min read Security · rest

Master Role-Based Access Control (RBAC) in Spring Boot applications using Kotlin with practical examples, from basic setup to advanced configurations with method-level security

Implementing API Key Authentication in Spring Boot with Kotlin: A Complete Guide (2024)

Implementing API Key Authentication in Spring Boot with Kotlin: A Complete Guide (2024)

4 min read Security · rest

Learn how to implement secure API key authentication in Spring Boot with Kotlin, including rate limiting, monitoring, and production-ready best practices - complete with practical examples and ready-to-use code

Why You Should Disable CSRF Protection for REST APIs in Spring Boot (And How to Do It Right)

Why You Should Disable CSRF Protection for REST APIs in Spring Boot (And How to Do It Right)

2 min read Security · rest

Learn why CSRF protection isn't necessary for most REST APIs and how to properly disable it in Spring Boot while maintaining security. Includes code examples and best practices