Security 4 min read

Securing Spring Boot REST APIs with Kotlin: Best Practices Guide

Learn how to secure your Spring Boot REST APIs using Kotlin with industry-standard security practices and implementations.

Introduction

Security is a critical aspect of any modern web application. In this comprehensive guide, we'll walk through implementing robust security measures for your Spring Boot REST APIs using Kotlin. We'll cover everything from basic authentication to rate limiting, ensuring your APIs are protected against common security threats.

Prerequisites

  • Basic knowledge of Kotlin and Spring Boot
  • JDK 17 or later installed
  • Your favorite IDE (IntelliJ IDEA recommended)
  • Gradle or Maven for dependency management

Table of Contents

Setting Up the Project

First, let's create a new Spring Boot project with the necessary dependencies. You can use Spring Initializr or add the following to your existing build.gradle.kts:

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-security")
    implementation("io.github.bucket4j:bucket4j-core:8.1.0")
    implementation("org.springframework.boot:spring-boot-starter-validation")
}

Implementing API Key Authentication

Let's start with implementing API key authentication. We'll create a custom filter that validates API keys against our database.

@Component
class ApiKeyAuthFilter : OncePerRequestFilter() {
    @Autowired
    private lateinit var apiKeyService: ApiKeyService

    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        val apiKey = request.getHeader("X-API-Key")
        
        if (apiKey == null) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "No API key provided")
            return
        }
        
        if (!apiKeyService.validateApiKey(apiKey)) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid API key")
            return
        }
        
        filterChain.doFilter(request, response)
    }
}

Create a service to handle API key validation:

@Service
class ApiKeyService {
    // In a real application, this would be stored in a database
    private val validApiKeys = setOf(
        "your-secret-api-key-1",
        "your-secret-api-key-2"
    )

    fun validateApiKey(apiKey: String): Boolean {
        return validApiKeys.contains(apiKey)
    }
}

Rate Limiting Implementation

To prevent abuse of our API, we'll implement rate limiting using Bucket4j:

@Component
class RateLimitingFilter : OncePerRequestFilter() {
    private val buckets = ConcurrentHashMap<String, Bucket>()
    
    private fun resolveBucket(apiKey: String): Bucket {
        return buckets.computeIfAbsent(apiKey) { _ ->
            Bucket4j.builder()
                .addLimit(Bandwidth.classic(100, Refill.intervally(100, Duration.ofMinutes(1))))
                .build()
        }
    }

    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        val apiKey = request.getHeader("X-API-Key")
        
        if (apiKey != null) {
            val bucket = resolveBucket(apiKey)
            val probe = bucket.tryConsumeAndReturnRemaining(1)
            
            response.addHeader("X-Rate-Limit-Remaining", probe.remainingTokens.toString())
            
            if (!probe.isConsumed) {
                response.sendError(
                    HttpServletResponse.SC_TOO_MANY_REQUESTS,
                    "Rate limit exceeded. Try again in ${probe.nanosToWaitForRefill / 1_000_000_000} seconds"
                )
                return
            }
        }
        
        filterChain.doFilter(request, response)
    }
}

Security Headers Configuration

Configure security headers to protect against common web vulnerabilities:

@Configuration
@EnableWebSecurity
class SecurityConfig : WebSecurityConfigurerAdapter() {

    override fun configure(http: HttpSecurity) {
        http
            .headers()
                .xssProtection()
                .and()
                .contentSecurityPolicy("default-src 'self'")
                .and()
                .frameOptions().deny()
                .and()
                .contentTypeOptions()
                .and()
            .and()
            .csrf().disable()  // For REST APIs, CSRF is typically disabled
            .addFilterBefore(apiKeyAuthFilter, UsernamePasswordAuthenticationFilter::class.java)
            .addFilterAfter(rateLimitingFilter, ApiKeyAuthFilter::class.java)
    }
}

Modern REST APIs require careful security configuration. While Spring Security provides robust defaults, some need adjustment for API scenarios. For instance, [CSRF protection might need to be disabled for REST APIs](https://kotlincraft.dev/articles/why-you-should-disable-csrf-protection-for-rest-apis-in-spring-boot-and-how-to-do-it-right) in certain situations.

Input Validation and Sanitization

Implement input validation using Spring's validation framework:

data class UserRequest(
    @field:NotBlank(message = "Username is required")
    @field:Pattern(regexp = "^[a-zA-Z0-9]{4,}$", message = "Username must be alphanumeric and at least 4 characters")
    val username: String,
    
    @field:Email(message = "Invalid email format")
    val email: String,
    
    @field:NotBlank(message = "Password is required")
    @field:Size(min = 8, message = "Password must be at least 8 characters")
    val password: String
)

@RestController
@RequestMapping("/api/users")
class UserController {
    
    @PostMapping
    fun createUser(@Valid @RequestBody request: UserRequest): ResponseEntity<UserResponse> {
        // Implementation
        return ResponseEntity.ok(UserResponse(/* ... */))
    }
}

@ControllerAdvice
class GlobalExceptionHandler {
    
    @ExceptionHandler(MethodArgumentNotValidException::class)
    fun handleValidationExceptions(ex: MethodArgumentNotValidException): ResponseEntity<Map<String, String>> {
        val errors = ex.bindingResult.fieldErrors.associate { 
            it.field to (it.defaultMessage ?: "Invalid input") 
        }
        return ResponseEntity.badRequest().body(errors)
    }
}

Best Practices and Common Pitfalls

1. API Key Management

  • Never expose API keys in client-side code
  • Implement key rotation mechanisms
  • Use environment variables for sensitive data

2. Rate Limiting Considerations

  • Set appropriate limits based on your infrastructure
  • Implement different limits for different user tiers
  • Consider using Redis for distributed rate limiting in a microservices environment

3. Input Validation

  • Always validate input on the server side
  • Use appropriate data types and constraints
  • Implement request size limits

4. Security Headers

  • Regularly review and update security headers
  • Use security scanning tools to verify configuration
  • Monitor security advisories for new threats

Testing the Security Implementation

Here's how to test your security implementation:

@SpringBootTest
@AutoConfigureMockMvc
class SecurityConfigTest {
    
    @Autowired
    private lateinit var mockMvc: MockMvc
    
    @Test
    fun `should return 401 when no API key is provided`() {
        mockMvc.perform(get("/api/users"))
            .andExpect(status().isUnauthorized)
    }
    
    @Test
    fun `should return 200 when valid API key is provided`() {
        mockMvc.perform(
            get("/api/users")
                .header("X-API-Key", "your-secret-api-key-1")
        )
            .andExpect(status().isOk)
    }
    
    @Test
    fun `should enforce rate limiting`() {
        val apiKey = "your-secret-api-key-1"
        
        // Make 101 requests (exceeding our 100/minute limit)
        repeat(101) {
            mockMvc.perform(
                get("/api/users")
                    .header("X-API-Key", apiKey)
            )
        }
        
        // The 101st request should be rate limited
        mockMvc.perform(
            get("/api/users")
                .header("X-API-Key", apiKey)
        )
            .andExpect(status().isTooManyRequests)
    }
}

Conclusion

In this guide, we've covered the essential aspects of securing your Spring Boot REST APIs using Kotlin. We implemented API key authentication, rate limiting, security headers, and input validation. Remember that security is an ongoing process, and it's important to regularly review and update your security measures.

Further Reading