Ktor Auth
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
- Ktor Authentication Official Documentation
- Ktor JWT Authentication
- Ktor OAuth Documentation
- Ktor Session Authentication
- Ktor GitHub Repository
- Ktor Official Site
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}")
}
}
}
}