Role-Based Access Control (RBAC) in Spring Security with Kotlin
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
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.
To follow along with this tutorial, you'll need:
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.
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")
}
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
}
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"
}
}
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)
}
}
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()
}
}
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
)
}
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
)
}
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)
}
}
When implementing API key authentication, follow these best practices:
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:
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
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
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