Kotlin Authorization
Introduction
Authorization is a crucial aspect of backend development that determines what resources a user can access and what actions they can perform within your application. While authentication verifies who the user is, authorization determines what they're allowed to do.
In this tutorial, we'll explore how to implement authorization in Kotlin backend applications. You'll learn different authorization strategies, how to integrate with popular frameworks, and best practices for securing your API endpoints.
Understanding Authorization Basics
Before diving into code, let's clarify the key concepts of authorization:
- Authentication: Verifies the identity of a user (who they are)
- Authorization: Determines what resources a user can access (what they can do)
- Roles: Common groupings of permissions (like "admin", "user", "editor")
- Permissions: Specific actions a user can perform (like "read_posts", "edit_users")
Basic Role-based Authorization in Kotlin
Let's start with a simple role-based authorization implementation using Kotlin:
enum class Role {
USER,
EDITOR,
ADMIN
}
data class User(
val id: String,
val username: String,
val role: Role
)
class AuthorizationService {
fun canAccessResource(user: User, requiredRole: Role): Boolean {
return when (user.role) {
Role.ADMIN -> true // Admins can access everything
Role.EDITOR -> requiredRole != Role.ADMIN // Editors can't access admin resources
Role.USER -> requiredRole == Role.USER // Users can only access user resources
}
}
}
Using this service:
fun main() {
val authService = AuthorizationService()
val adminUser = User("1", "admin", Role.ADMIN)
val editorUser = User("2", "editor", Role.EDITOR)
val normalUser = User("3", "user", Role.USER)
// Check access to admin resource
println("Admin accessing admin area: ${authService.canAccessResource(adminUser, Role.ADMIN)}") // true
println("Editor accessing admin area: ${authService.canAccessResource(editorUser, Role.ADMIN)}") // false
println("User accessing admin area: ${authService.canAccessResource(normalUser, Role.ADMIN)}") // false
// Check access to editor resource
println("Admin accessing editor area: ${authService.canAccessResource(adminUser, Role.EDITOR)}") // true
println("Editor accessing editor area: ${authService.canAccessResource(editorUser, Role.EDITOR)}") // true
println("User accessing editor area: ${authService.canAccessResource(normalUser, Role.EDITOR)}") // false
}
Output:
Admin accessing admin area: true
Editor accessing admin area: false
User accessing admin area: false
Admin accessing editor area: true
Editor accessing editor area: true
User accessing editor area: false
Permission-based Authorization
Role-based authorization can be limiting. A more flexible approach is permission-based authorization:
class Permission(val name: String) {
companion object {
val READ_POSTS = Permission("read_posts")
val CREATE_POSTS = Permission("create_posts")
val EDIT_POSTS = Permission("edit_posts")
val DELETE_POSTS = Permission("delete_posts")
val MANAGE_USERS = Permission("manage_users")
}
}
data class User(
val id: String,
val username: String,
val permissions: Set<Permission>
)
class AuthorizationService {
fun hasPermission(user: User, requiredPermission: Permission): Boolean {
return user.permissions.contains(requiredPermission)
}
fun hasAllPermissions(user: User, requiredPermissions: Set<Permission>): Boolean {
return user.permissions.containsAll(requiredPermissions)
}
fun hasAnyPermission(user: User, permissions: Set<Permission>): Boolean {
return user.permissions.any { permissions.contains(it) }
}
}
Usage example:
fun main() {
val authService = AuthorizationService()
val adminUser = User(
"1",
"admin",
setOf(
Permission.READ_POSTS,
Permission.CREATE_POSTS,
Permission.EDIT_POSTS,
Permission.DELETE_POSTS,
Permission.MANAGE_USERS
)
)
val editorUser = User(
"2",
"editor",
setOf(
Permission.READ_POSTS,
Permission.CREATE_POSTS,
Permission.EDIT_POSTS
)
)
val normalUser = User(
"3",
"user",
setOf(Permission.READ_POSTS)
)
// Check single permissions
println("Can admin delete posts? ${authService.hasPermission(adminUser, Permission.DELETE_POSTS)}") // true
println("Can editor delete posts? ${authService.hasPermission(editorUser, Permission.DELETE_POSTS)}") // false
// Check multiple permissions
val postModifyPermissions = setOf(Permission.CREATE_POSTS, Permission.EDIT_POSTS)
println("Can admin modify posts? ${authService.hasAllPermissions(adminUser, postModifyPermissions)}") // true
println("Can editor modify posts? ${authService.hasAllPermissions(editorUser, postModifyPermissions)}") // true
println("Can user modify posts? ${authService.hasAllPermissions(normalUser, postModifyPermissions)}") // false
}
Output:
Can admin delete posts? true
Can editor delete posts? false
Can admin modify posts? true
Can editor modify posts? true
Can user modify posts? false
JWT-based Authorization in Ktor
For web applications, JWT (JSON Web Token) is a popular method for handling authorization. Here's how to implement JWT authorization in a Ktor application:
First, add the necessary dependencies to your build.gradle.kts
:
dependencies {
implementation("io.ktor:ktor-server-core:2.2.0")
implementation("io.ktor:ktor-server-netty:2.2.0")
implementation("io.ktor:ktor-server-auth:2.2.0")
implementation("io.ktor:ktor-server-auth-jwt:2.2.0")
}
Then, set up JWT authentication in your Ktor application:
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import io.ktor.server.routing.*
import io.ktor.http.*
import io.ktor.server.response.*
import io.ktor.server.request.*
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import java.util.*
fun Application.configureAuth() {
// JWT configuration
val secret = "my-secret-key" // In production, use a secure secret from environment variables
val issuer = "my-application"
val audience = "my-audience"
val myRealm = "my-realm"
install(Authentication) {
jwt("auth-jwt") {
realm = myRealm
verifier(
JWT
.require(Algorithm.HMAC256(secret))
.withAudience(audience)
.withIssuer(issuer)
.build()
)
validate { credential ->
// Validate JWT claims
if (credential.payload.audience.contains(audience) &&
credential.payload.expiresAt.time > System.currentTimeMillis()) {
// Extract user role and permissions
val role = credential.payload.getClaim("role").asString()
val permissions = credential.payload.getClaim("permissions").asList(String::class.java)
JWTPrincipal(credential.payload)
} else null
}
}
}
// Routing with authorization
routing {
// Public route
get("/api/public") {
call.respondText("Public endpoint - anyone can access")
}
// Protected route with role-based access
authenticate("auth-jwt") {
get("/api/protected") {
val principal = call.principal<JWTPrincipal>()
val role = principal?.payload?.getClaim("role")?.asString() ?: "none"
// Authorization check
if (role in listOf("admin", "editor")) {
call.respondText("Protected endpoint - you have access with role: $role")
} else {
call.respond(HttpStatusCode.Forbidden, "You don't have permission to access this resource")
}
}
// Admin-only route
get("/api/admin") {
val principal = call.principal<JWTPrincipal>()
val role = principal?.payload?.getClaim("role")?.asString() ?: "none"
if (role == "admin") {
call.respondText("Admin endpoint - only admins can access")
} else {
call.respond(HttpStatusCode.Forbidden, "Admin access required")
}
}
}
}
}
Function to generate a JWT token:
fun generateJwtToken(
userId: String,
role: String,
permissions: List<String>,
expirationTimeMillis: Long = 3600000 // 1 hour
): String {
val secret = "my-secret-key"
val issuer = "my-application"
val audience = "my-audience"
return JWT.create()
.withSubject(userId)
.withIssuer(issuer)
.withAudience(audience)
.withClaim("role", role)
.withClaim("permissions", permissions)
.withExpiresAt(Date(System.currentTimeMillis() + expirationTimeMillis))
.sign(Algorithm.HMAC256(secret))
}
Spring Security with Kotlin
For Spring Boot applications, Spring Security provides comprehensive authorization capabilities. Here's how to implement authorization in a Spring Boot application with Kotlin:
First, add the dependencies to your build.gradle.kts
:
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-security")
}
Create a security configuration:
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.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.provisioning.InMemoryUserDetailsManager
import org.springframework.security.web.SecurityFilterChain
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()
@Bean
fun userDetailsService(encoder: PasswordEncoder): UserDetailsService {
val userDetailsManager = InMemoryUserDetailsManager()
// Create users with roles
val admin = User.builder()
.username("admin")
.password(encoder.encode("adminPass"))
.roles("ADMIN", "USER")
.build()
val user = User.builder()
.username("user")
.password(encoder.encode("userPass"))
.roles("USER")
.build()
val editor = User.builder()
.username("editor")
.password(encoder.encode("editorPass"))
.roles("EDITOR", "USER")
.build()
userDetailsManager.createUser(admin)
userDetailsManager.createUser(user)
userDetailsManager.createUser(editor)
return userDetailsManager
}
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http
.authorizeHttpRequests { auth ->
auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/user/**").hasRole("USER")
.requestMatchers("/api/editor/**").hasRole("EDITOR")
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
}
.formLogin { it.permitAll() }
.logout { it.permitAll() }
return http.build()
}
}
Create a controller with protected endpoints:
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/api")
class ResourceController {
@GetMapping("/public")
fun publicResource(): String {
return "This is a public resource, anyone can access it"
}
@GetMapping("/user")
fun userResource(): String {
return "This resource requires USER role"
}
@GetMapping("/editor")
fun editorResource(): String {
return "This resource requires EDITOR role"
}
@GetMapping("/admin")
fun adminResource(): String {
return "This resource requires ADMIN role"
}
// Method-level authorization using @PreAuthorize
@GetMapping("/advanced")
@PreAuthorize("hasRole('ADMIN') or (hasRole('EDITOR') and #id < 100)")
fun advancedAuth(id: Int): String {
return "This uses complex authorization rules"
}
}
To enable method-level security with @PreAuthorize
, add this configuration:
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
@Configuration
@EnableMethodSecurity(prePostEnabled = true)
class MethodSecurityConfig
Custom Policy-based Authorization
For more complex authorization logic, you might want to implement a policy-based approach:
// Resource to be protected
data class Article(
val id: Long,
val title: String,
val content: String,
val authorId: Long,
val published: Boolean
)
// Authorization policy interface
interface AuthorizationPolicy<T> {
fun evaluate(user: User, resource: T): Boolean
}
// Concrete policy for article access
class ArticleViewPolicy : AuthorizationPolicy<Article> {
override fun evaluate(user: User, resource: Article): Boolean {
// Anyone can view published articles
if (resource.published) {
return true
}
// Unpublished articles can only be viewed by authors or admins
return user.id == resource.authorId || user.roles.contains("ADMIN")
}
}
class ArticleEditPolicy : AuthorizationPolicy<Article> {
override fun evaluate(user: User, resource: Article): Boolean {
// Only the author or admins can edit
return user.id == resource.authorId || user.roles.contains("ADMIN")
}
}
// Authorization service
class PolicyAuthorizationService {
private val policies = mutableMapOf<String, AuthorizationPolicy<*>>()
fun <T> registerPolicy(name: String, policy: AuthorizationPolicy<T>) {
policies[name] = policy
}
@Suppress("UNCHECKED_CAST")
fun <T> authorize(user: User, resource: T, policyName: String): Boolean {
val policy = policies[policyName] as? AuthorizationPolicy<T>
?: throw IllegalArgumentException("Policy $policyName not found")
return policy.evaluate(user, resource)
}
}
Using the policy-based authorization:
data class User(
val id: Long,
val username: String,
val roles: Set<String>
)
fun main() {
val authService = PolicyAuthorizationService()
// Register policies
authService.registerPolicy("article.view", ArticleViewPolicy())
authService.registerPolicy("article.edit", ArticleEditPolicy())
val admin = User(1, "admin", setOf("ADMIN", "USER"))
val regularUser = User(2, "user", setOf("USER"))
val author = User(3, "author", setOf("USER"))
val publishedArticle = Article(1, "Public Article", "Content", 3, true)
val unpublishedArticle = Article(2, "Draft Article", "Draft Content", 3, false)
// Authorization checks
println("Can admin view published article: ${authService.authorize(admin, publishedArticle, "article.view")}") // true
println("Can regular user view published article: ${authService.authorize(regularUser, publishedArticle, "article.view")}") // true
println("Can admin view unpublished article: ${authService.authorize(admin, unpublishedArticle, "article.view")}") // true
println("Can author view their unpublished article: ${authService.authorize(author, unpublishedArticle, "article.view")}") // true
println("Can regular user view unpublished article: ${authService.authorize(regularUser, unpublishedArticle, "article.view")}") // false
println("Can admin edit any article: ${authService.authorize(admin, publishedArticle, "article.edit")}") // true
println("Can regular user edit article: ${authService.authorize(regularUser, publishedArticle, "article.edit")}") // false
println("Can author edit their own article: ${authService.authorize(author, publishedArticle, "article.edit")}") // true
}
Output:
Can admin view published article: true
Can regular user view published article: true
Can admin view unpublished article: true
Can author view their unpublished article: true
Can regular user view unpublished article: false
Can admin edit any article: true
Can regular user edit article: false
Can author edit their own article: true
Best Practices for Authorization in Kotlin
-
Separate Authentication from Authorization: Keep them as distinct processes in your code
-
Use Principle of Least Privilege: Give users only the permissions they need
-
Favor Permission-based over Role-based: More granular control with permissions
-
Layer Your Security: Use multiple layers of security checks
-
Don't Trust the Client: Always verify authorization server-side
-
Audit and Log Authorization Decisions: For security review and debugging
-
Apply Authorization at Different Levels:
- At the route level (for API endpoints)
- At the service level (for business logic)
- At the data access level (for data protection)
-
Test Authorization Logic: Write unit and integration tests specifically for authorization rules
Summary
In this tutorial, we've explored various approaches to implementing authorization in Kotlin backend applications:
- Basic role-based authorization
- Permission-based authorization
- JWT-based authorization in Ktor
- Spring Security integration with Kotlin
- Custom policy-based authorization
Authorization is a critical aspect of application security that determines what authenticated users can access and do in your system. By implementing proper authorization, you can protect your application's resources and ensure that users only have access to what they need.
Additional Resources
- Ktor Authentication Documentation
- Spring Security Documentation
- OWASP Authorization Cheat Sheet
- JWT Official Documentation
Exercises
- Implement a hybrid system that combines both role-based and permission-based authorization.
- Extend the policy-based authorization example to handle more complex scenarios, like time-based access restrictions.
- Create a Ktor application with JWT authorization that includes both role and permission checks.
- Implement a Spring Security configuration that uses custom authorization rules beyond simple role checks.
- Build an authorization system that supports hierarchical roles (e.g., admin > editor > user).
If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)