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

December 30, 2024 4 min read Intermediate

Role-Based Access Control (RBAC) is a crucial security mechanism that restricts system access based on the roles of individual users. In this comprehensive guide, we'll implement RBAC in a Spring Boot application using Kotlin, covering everything from basic setup to advanced configurations.

Introduction

When building enterprise applications, you often need different levels of access for different types of users. For example:

  • Administrators who can manage user accounts
  • Managers who can view reports
  • Regular users who can only access their own data

RBAC provides a clean, scalable way to manage these permissions. We'll build a complete example showing how to implement this in Spring Boot with Kotlin.

Prerequisites

To follow this tutorial, you'll need:

  • Kotlin 1.9.0 or later
  • Spring Boot 3.2.0 or later
  • JDK 17 or later
  • Basic understanding of Spring Security concepts
  • Familiarity with Kotlin syntax

Project Setup

First, create a new Spring Boot project with the following dependencies in 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 demonstration purposes, we'll use H2 database
    runtimeOnly("com.h2database:h2")
    
    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation("org.springframework.security:spring-security-test")
}

Implementing the Core RBAC Components

1. Define the Role Enum

First, let's define our roles:

enum class Role {
    ROLE_USER,
    ROLE_MANAGER,
    ROLE_ADMIN
}

2. Create the User Entity

Next, we'll create our user entity with role support:

@Entity
@Table(name = "users")
class User(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,
    
    @Column(unique = true)
    val username: String,
    
    val password: String,
    
    @ElementCollection(fetch = FetchType.EAGER)
    @CollectionTable(name = "user_roles", joinColumns = [JoinColumn(name = "user_id")])
    @Enumerated(EnumType.STRING)
    val roles: Set<Role> = setOf(Role.ROLE_USER)
)

3. Implement UserDetailsService

Create a service to load user details:

@Service
class UserDetailsServiceImpl(
    private val userRepository: UserRepository
) : UserDetailsService {

    override fun loadUserByUsername(username: String): UserDetails {
        val user = userRepository.findByUsername(username)
            ?: throw UsernameNotFoundException("User not found")

        return org.springframework.security.core.userdetails.User
            .withUsername(user.username)
            .password(user.password)
            .roles(*user.roles.map { it.name.removePrefix("ROLE_") }.toTypedArray())
            .build()
    }
}

4. Configure Security

Set up the security configuration:

@Configuration
@EnableWebSecurity
class SecurityConfig(
    private val userDetailsService: UserDetailsServiceImpl
) {

    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http
            .csrf { it.disable() }
            .authorizeHttpRequests { auth ->
                auth
                    .requestMatchers("/api/public/**").permitAll()
                    .requestMatchers("/api/admin/**").hasRole("ADMIN")
                    .requestMatchers("/api/manager/**").hasRole("MANAGER")
                    .requestMatchers("/api/user/**").hasRole("USER")
                    .anyRequest().authenticated()
            }
            .sessionManagement {
                it.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            }
            .userDetailsService(userDetailsService)

        return http.build()
    }

    @Bean
    fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()
}

Creating Protected Resources

Let's create some endpoints with different role requirements:

@RestController
@RequestMapping("/api")
class UserController {

    @GetMapping("/public/info")
    fun getPublicInfo() = "This is public information"

    @GetMapping("/user/profile")
    @PreAuthorize("hasRole('USER')")
    fun getUserProfile(authentication: Authentication): String {
        return "User profile for: ${authentication.name}"
    }

    @GetMapping("/manager/reports")
    @PreAuthorize("hasRole('MANAGER')")
    fun getManagerReports(): String {
        return "Manager reports"
    }

    @GetMapping("/admin/users")
    @PreAuthorize("hasRole('ADMIN')")
    fun getAdminUserList(): String {
        return "Admin user list"
    }
}

Method-Level Security

Spring Security also supports method-level security. Here's how to implement it:

@Configuration
@EnableMethodSecurity
class MethodSecurityConfig

@Service
class UserService {
    @PreAuthorize("hasRole('ADMIN')")
    fun deleteUser(userId: Long) {
        // Delete user logic
    }

    @PreAuthorize("hasRole('MANAGER') or @userSecurity.isOwner(#userId)")
    fun updateUser(userId: Long, userDetails: UserDetails) {
        // Update user logic
    }
}

Custom Security Expressions

You can create custom security expressions for complex authorization rules:

@Component("userSecurity")
class UserSecurityEvaluator(
    private val userRepository: UserRepository
) {
    fun isOwner(userId: Long): Boolean {
        val authentication = SecurityContextHolder.getContext().authentication
        val username = authentication.name
        return userRepository.findByIdAndUsername(userId, username) != null
    }
}

Testing RBAC Implementation

Here's how to test your RBAC implementation:

@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {

    @Autowired
    private lateinit var mockMvc: MockMvc

    @Test
    @WithMockUser(roles = ["ADMIN"])
    fun `admin can access admin endpoints`() {
        mockMvc.perform(get("/api/admin/users"))
            .andExpect(status().isOk)
    }

    @Test
    @WithMockUser(roles = ["USER"])
    fun `user cannot access admin endpoints`() {
        mockMvc.perform(get("/api/admin/users"))
            .andExpect(status().isForbidden)
    }
}

Best Practices

  1. Role Hierarchy
    • Implement role hierarchies for complex permissions:
@Bean
fun roleHierarchy(): RoleHierarchy {
    val hierarchy = RoleHierarchyImpl()
    hierarchy.setHierarchy("""
        ROLE_ADMIN > ROLE_MANAGER
        ROLE_MANAGER > ROLE_USER
    """.trimIndent())
    return hierarchy
}
  1. Dynamic Roles
    • Store roles in a database for runtime flexibility
    • Implement a caching strategy for role checks
  2. Security Auditing
    • Enable security auditing to track access:
@EnableJpaAuditing
class AuditConfig {
    @Bean
    fun auditorAware(): AuditorAware<String> {
        return AuditorAware {
            Optional.ofNullable(SecurityContextHolder.getContext().authentication?.name)
        }
    }
}

Common Pitfalls to Avoid

  1. Role Prefixes: Always use the ROLE_ prefix in database but remove it when using hasRole()
  2. Permission Granularity: Don't create too many roles; use permissions for fine-grained control
  3. Security Context: Be careful when accessing SecurityContext in async operations
  4. Role vs Authority: Understand the difference between roles and authorities

Conclusion

RBAC is a powerful way to manage access control in your Spring Boot applications. By following this guide, you've learned how to:

  • Implement basic role-based security
  • Configure method-level security
  • Create custom security expressions
  • Test your RBAC implementation
  • Apply best practices for production use

Role-Based Access Control is commonly used alongside JWT authentication. If you haven't set up authentication yet, check out our guide on [implementing JWT authentication in Spring Boot](https://kotlincraft.dev/articles/jwt-authentication-in-spring-boot-with-kotlin-from-zero-to-production). For API-only applications, you might also want to review our guide on [CSRF protection in REST APIs](https://kotlincraft.dev/articles/why-you-should-disable-csrf-protection-for-rest-apis-in-spring-boot-and-how-to-do-it-right) to ensure your security configuration is optimized for your use case.

Additional Resources

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