Ktor Auth

Authentication LibraryKotlinKtorJWTOAuthSession ManagementREST API

Authentication Library

Ktor Auth

Overview

Ktor Auth is an integrated authentication solution for Kotlin server applications within the Ktor framework, providing multiple authentication methods including JWT, OAuth, and session authentication through a unified API.

Details

Ktor Auth is the official authentication plugin for the Ktor web framework developed by JetBrains. It enables easy implementation of enterprise-grade authentication features in multiplatform Kotlin server applications. Key authentication methods supported include JWT (JSON Web Token) authentication, OAuth 2.0 / OpenID Connect, Basic authentication, Digest authentication, Form-based authentication, Session authentication, and Bearer Token authentication. For JWT authentication specifically, it supports encryption algorithms like RS256 and HS256, with automatic token verification and custom claim processing capabilities. The OAuth implementation facilitates easy integration with major providers like Google, Facebook, and GitHub, and also supports PKCE (Proof Key for Code Exchange). Session management features support server-side sessions, client-side sessions (encrypted cookies), and distributed sessions (Redis, etc.), with comprehensive support for session expiration management and security settings. The plugin architecture allows selection of only required authentication methods for lightweight implementation.

Pros and Cons

Pros

  • Integrated Plugin: Unified API for managing multiple authentication methods
  • Kotlin Native: Type-safe implementation leveraging Kotlin language features
  • Multiplatform: Support for JVM, Android, and native platforms
  • Plugin Architecture: Lightweight implementation by selecting only needed authentication methods
  • Async Support: High-performance processing with Kotlin Coroutines
  • Flexible Configuration: Easy implementation of custom authentication logic
  • Security Compliant: Implementation adhering to latest security standards

Cons

  • Ktor Dependency: Exclusive to Ktor framework, unusable with other frameworks
  • Learning Curve: Requires understanding of Kotlin Coroutines and Ktor-specific concepts
  • Documentation: Limited documentation compared to other major authentication libraries
  • Ecosystem: Smaller community size compared to Spring Security
  • Debugging: Detailed debugging of authentication flows can be challenging

Key Links

Code Examples

Hello World (Basic JWT Authentication)

import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm

fun Application.configureAuthentication() {
    install(Authentication) {
        jwt("auth-jwt") {
            realm = "ktor sample app"
            verifier(
                JWT
                    .require(Algorithm.HMAC256("secret"))
                    .withAudience("jwt-audience")
                    .withIssuer("jwt-issuer")
                    .build()
            )
            validate { credential ->
                if (credential.payload.getClaim("username").asString() != "") {
                    JWTPrincipal(credential.payload)
                } else {
                    null
                }
            }
        }
    }
}

fun Application.configureRouting() {
    routing {
        authenticate("auth-jwt") {
            get("/hello") {
                val principal = call.principal<JWTPrincipal>()
                val username = principal!!.payload.getClaim("username").asString()
                call.respondText("Hello $username!")
            }
        }
    }
}

OAuth 2.0 Integration Authentication

import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.sessions.*
import io.ktor.http.*

data class UserSession(val name: String, val count: Int)

fun Application.configureOAuth() {
    install(Authentication) {
        oauth("auth-oauth-google") {
            urlProvider = { "http://localhost:8080/callback" }
            providerLookup = {
                OAuthServerSettings.OAuth2ServerSettings(
                    name = "google",
                    authorizeUrl = "https://accounts.google.com/o/oauth2/auth",
                    accessTokenUrl = "https://oauth2.googleapis.com/token",
                    requestMethod = HttpMethod.Post,
                    clientId = System.getenv("GOOGLE_CLIENT_ID"),
                    clientSecret = System.getenv("GOOGLE_CLIENT_SECRET"),
                    defaultScopes = listOf("openid", "profile", "email")
                )
            }
            client = HttpClient(Apache)
        }
    }
    
    install(Sessions) {
        cookie<UserSession>("user_session") {
            cookie.path = "/"
            cookie.maxAgeInSeconds = 60 * 60 * 24 * 30 // 30 days
        }
    }
}

fun Application.configureOAuthRouting() {
    routing {
        authenticate("auth-oauth-google") {
            get("/login") {
                // Redirects to OAuth provider
            }
            
            get("/callback") {
                val principal: OAuthAccessTokenResponse.OAuth2? = call.principal()
                call.sessions.set(UserSession(principal?.state ?: "", 1))
                call.respondRedirect("/hello")
            }
        }
        
        get("/hello") {
            val userSession = call.sessions.get<UserSession>()
            if (userSession != null) {
                val countText = if (userSession.count == 1) "first" else "${userSession.count}th"
                call.respondText("Hello ${userSession.name}, this is your $countText visit.")
            } else {
                call.respondRedirect("/login")
            }
        }
    }
}

Session Authentication System

import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.sessions.*
import io.ktor.http.*
import kotlinx.serialization.Serializable

@Serializable
data class UserSession(
    val userId: String,
    val username: String,
    val roles: List<String>,
    val loginTime: Long,
    val lastActivity: Long
)

@Serializable
data class LoginRequest(val username: String, val password: String)

// User database (use DB in actual implementation)
data class User(
    val id: String,
    val username: String,
    val passwordHash: String,
    val roles: List<String>
)

val users = mapOf(
    "admin" to User("1", "admin", "hashed_admin_password", listOf("admin", "user")),
    "user" to User("2", "user", "hashed_user_password", listOf("user"))
)

fun Application.configureSessionAuth() {
    install(Sessions) {
        cookie<UserSession>("user_session") {
            cookie.path = "/"
            cookie.maxAgeInSeconds = 60 * 60 * 8 // 8 hours
            cookie.httpOnly = true
            cookie.secure = true
            cookie.extensions["SameSite"] = "Strict"
        }
    }
    
    install(Authentication) {
        session<UserSession>("auth-session") {
            validate { session ->
                // Session validation check
                val now = System.currentTimeMillis()
                val sessionAge = now - session.loginTime
                val inactivityTime = now - session.lastActivity
                
                when {
                    sessionAge > 8 * 60 * 60 * 1000 -> null // Invalid after 8 hours
                    inactivityTime > 30 * 60 * 1000 -> null // Invalid after 30 min inactivity
                    else -> {
                        // Update activity
                        val updatedSession = session.copy(lastActivity = now)
                        updatedSession
                    }
                }
            }
            challenge {
                call.respondRedirect("/login")
            }
        }
    }
}

fun Application.configureSessionRouting() {
    routing {
        post("/login") {
            val loginRequest = call.receive<LoginRequest>()
            val user = users[loginRequest.username]
            
            if (user != null && validatePassword(loginRequest.password, user.passwordHash)) {
                val currentTime = System.currentTimeMillis()
                val session = UserSession(
                    userId = user.id,
                    username = user.username,
                    roles = user.roles,
                    loginTime = currentTime,
                    lastActivity = currentTime
                )
                call.sessions.set(session)
                call.respond(HttpStatusCode.OK, "Login successful")
            } else {
                call.respond(HttpStatusCode.Unauthorized, "Invalid credentials")
            }
        }
        
        post("/logout") {
            call.sessions.clear<UserSession>()
            call.respond(HttpStatusCode.OK, "Logout successful")
        }
        
        authenticate("auth-session") {
            get("/profile") {
                val session = call.principal<UserSession>()
                call.respond(session!!)
            }
            
            get("/admin") {
                val session = call.principal<UserSession>()
                if ("admin" in session!!.roles) {
                    call.respondText("Admin area")
                } else {
                    call.respond(HttpStatusCode.Forbidden, "Admin access required")
                }
            }
        }
    }
}

fun validatePassword(password: String, hash: String): Boolean {
    // Use bcrypt etc. in actual implementation
    return "hashed_${password}_password" == hash
}

JWT Authentication with Refresh Tokens

import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.http.*
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import kotlinx.serialization.Serializable
import java.util.*

@Serializable
data class TokenResponse(
    val accessToken: String,
    val refreshToken: String,
    val expiresIn: Long
)

@Serializable
data class RefreshRequest(val refreshToken: String)

object JWTConfig {
    const val ISSUER = "http://localhost:8080/"
    const val AUDIENCE = "jwt-audience"
    const val REALM = "ktor sample app"
    private const val SECRET = "your-super-secret-key"
    private const val ACCESS_TOKEN_VALIDITY = 15 * 60 * 1000L // 15 minutes
    private const val REFRESH_TOKEN_VALIDITY = 7 * 24 * 60 * 60 * 1000L // 7 days
    
    val algorithm = Algorithm.HMAC256(SECRET)
    
    fun generateAccessToken(userId: String, username: String, roles: List<String>): String {
        return JWT.create()
            .withAudience(AUDIENCE)
            .withIssuer(ISSUER)
            .withClaim("userId", userId)
            .withClaim("username", username)
            .withClaim("roles", roles)
            .withExpiresAt(Date(System.currentTimeMillis() + ACCESS_TOKEN_VALIDITY))
            .sign(algorithm)
    }
    
    fun generateRefreshToken(userId: String): String {
        return JWT.create()
            .withAudience(AUDIENCE)
            .withIssuer(ISSUER)
            .withClaim("userId", userId)
            .withClaim("type", "refresh")
            .withExpiresAt(Date(System.currentTimeMillis() + REFRESH_TOKEN_VALIDITY))
            .sign(algorithm)
    }
}

fun Application.configureJWTAuth() {
    install(Authentication) {
        jwt("auth-jwt") {
            realm = JWTConfig.REALM
            verifier(
                JWT.require(JWTConfig.algorithm)
                    .withAudience(JWTConfig.AUDIENCE)
                    .withIssuer(JWTConfig.ISSUER)
                    .build()
            )
            validate { credential ->
                val userId = credential.payload.getClaim("userId").asString()
                val username = credential.payload.getClaim("username").asString()
                if (userId != null && username != null) {
                    JWTPrincipal(credential.payload)
                } else {
                    null
                }
            }
            challenge { defaultScheme, realm ->
                call.respond(HttpStatusCode.Unauthorized, "Token is not valid or has expired")
            }
        }
        
        jwt("refresh-jwt") {
            realm = JWTConfig.REALM
            verifier(
                JWT.require(JWTConfig.algorithm)
                    .withAudience(JWTConfig.AUDIENCE)
                    .withIssuer(JWTConfig.ISSUER)
                    .withClaim("type", "refresh")
                    .build()
            )
            validate { credential ->
                val userId = credential.payload.getClaim("userId").asString()
                val type = credential.payload.getClaim("type").asString()
                if (userId != null && type == "refresh") {
                    JWTPrincipal(credential.payload)
                } else {
                    null
                }
            }
        }
    }
}

fun Application.configureJWTRouting() {
    routing {
        post("/auth/login") {
            val loginRequest = call.receive<LoginRequest>()
            val user = users[loginRequest.username]
            
            if (user != null && validatePassword(loginRequest.password, user.passwordHash)) {
                val accessToken = JWTConfig.generateAccessToken(user.id, user.username, user.roles)
                val refreshToken = JWTConfig.generateRefreshToken(user.id)
                
                call.respond(
                    TokenResponse(
                        accessToken = accessToken,
                        refreshToken = refreshToken,
                        expiresIn = 15 * 60 // 15 minutes
                    )
                )
            } else {
                call.respond(HttpStatusCode.Unauthorized, "Invalid credentials")
            }
        }
        
        authenticate("refresh-jwt") {
            post("/auth/refresh") {
                val principal = call.principal<JWTPrincipal>()
                val userId = principal!!.payload.getClaim("userId").asString()
                
                // Get user info (from DB in actual implementation)
                val user = users.values.find { it.id == userId }
                if (user != null) {
                    val newAccessToken = JWTConfig.generateAccessToken(user.id, user.username, user.roles)
                    val newRefreshToken = JWTConfig.generateRefreshToken(user.id)
                    
                    call.respond(
                        TokenResponse(
                            accessToken = newAccessToken,
                            refreshToken = newRefreshToken,
                            expiresIn = 15 * 60
                        )
                    )
                } else {
                    call.respond(HttpStatusCode.Unauthorized, "User not found")
                }
            }
        }
        
        authenticate("auth-jwt") {
            get("/api/protected") {
                val principal = call.principal<JWTPrincipal>()
                val username = principal!!.payload.getClaim("username").asString()
                val roles = principal.payload.getClaim("roles").asList(String::class.java)
                
                call.respond(mapOf(
                    "message" to "Protected resource accessed",
                    "username" to username,
                    "roles" to roles
                ))
            }
        }
    }
}

Multiple Authentication Provider Integration

import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.http.*

fun Application.configureMultiAuth() {
    install(Authentication) {
        // Basic authentication
        basic("auth-basic") {
            realm = "Basic Auth"
            validate { credentials ->
                if (credentials.name == "admin" && credentials.password == "admin") {
                    UserIdPrincipal(credentials.name)
                } else {
                    null
                }
            }
        }
        
        // Bearer Token authentication
        bearer("auth-bearer") {
            realm = "Bearer Auth"
            authenticate { tokenCredential ->
                // API key validation
                if (tokenCredential.token == "your-api-key") {
                    UserIdPrincipal("api-user")
                } else {
                    null
                }
            }
        }
        
        // Form-based authentication
        form("auth-form") {
            userParamName = "user"
            passwordParamName = "password"
            challenge {
                // Redirect to custom login page
                call.respondRedirect("/login")
            }
            validate { credentials ->
                if (credentials.name == "form-user" && credentials.password == "form-pass") {
                    UserIdPrincipal(credentials.name)
                } else {
                    null
                }
            }
        }
    }
}

fun Application.configureMultiAuthRouting() {
    routing {
        // Allow multiple authentication methods
        authenticate("auth-jwt", "auth-basic", optional = false) {
            get("/api/flexible") {
                val principal = call.principal<UserIdPrincipal>() ?: call.principal<JWTPrincipal>()
                call.respondText("Access granted via multiple auth methods")
            }
        }
        
        // Basic authentication only endpoint
        authenticate("auth-basic") {
            get("/admin/basic") {
                call.respondText("Admin area with Basic Auth")
            }
        }
        
        // Bearer Token authentication only endpoint
        authenticate("auth-bearer") {
            get("/api/external") {
                call.respondText("External API access")
            }
        }
        
        // Optional authentication (supports both authenticated and unauthenticated)
        authenticate("auth-jwt", optional = true) {
            get("/public-or-private") {
                val principal = call.principal<JWTPrincipal>()
                if (principal != null) {
                    val username = principal.payload.getClaim("username").asString()
                    call.respondText("Welcome back, $username!")
                } else {
                    call.respondText("Public access")
                }
            }
        }
        
        // No authentication required endpoint
        get("/public") {
            call.respondText("Public endpoint")
        }
    }
}

Custom Authentication Provider

import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.util.*

// Custom authentication configuration
class CustomAuthenticationConfig : AuthenticationProvider.Config("custom") {
    var userProvider: ((String) -> User?)? = null
    var headerName: String = "X-API-Key"
    var rateLimitProvider: ((String) -> Boolean)? = null
}

// Custom authentication provider
class CustomAuthenticationProvider(config: CustomAuthenticationConfig) : AuthenticationProvider(config) {
    val userProvider = config.userProvider
    val headerName = config.headerName
    val rateLimitProvider = config.rateLimitProvider

    override suspend fun onAuthenticate(context: AuthenticationContext) {
        val token = context.call.request.headers[headerName]
        
        if (token == null) {
            context.challenge("CustomAuth", AuthenticationFailedCause.NoCredentials) {
                it.respond(HttpStatusCode.Unauthorized, "Missing $headerName header")
                it.finish()
            }
            return
        }
        
        // Rate limit check
        if (rateLimitProvider?.invoke(token) == false) {
            context.challenge("CustomAuth", AuthenticationFailedCause.InvalidCredentials) {
                it.respond(HttpStatusCode.TooManyRequests, "Rate limit exceeded")
                it.finish()
            }
            return
        }
        
        val user = userProvider?.invoke(token)
        if (user != null) {
            context.principal(UserIdPrincipal(user.username))
        } else {
            context.challenge("CustomAuth", AuthenticationFailedCause.InvalidCredentials) {
                it.respond(HttpStatusCode.Unauthorized, "Invalid API key")
                it.finish()
            }
        }
    }
}

// Extension function to easily configure custom authentication
fun AuthenticationConfig.custom(
    name: String? = null,
    configure: CustomAuthenticationConfig.() -> Unit
) {
    val provider = CustomAuthenticationProvider(CustomAuthenticationConfig().apply(configure))
    register(provider)
}

fun Application.configureCustomAuth() {
    install(Authentication) {
        custom("custom-api-key") {
            headerName = "X-API-Key"
            userProvider = { apiKey ->
                // Search user from API key
                when (apiKey) {
                    "secret-key-1" -> User("1", "service-a", "", listOf("service"))
                    "secret-key-2" -> User("2", "service-b", "", listOf("service"))
                    else -> null
                }
            }
            rateLimitProvider = { apiKey ->
                // Simple rate limiting (use Redis etc. in actual implementation)
                true
            }
        }
    }
}

fun Application.configureCustomAuthRouting() {
    routing {
        authenticate("custom-api-key") {
            get("/api/services") {
                val principal = call.principal<UserIdPrincipal>()
                call.respondText("Service API accessed by: ${principal?.name}")
            }
        }
    }
}