Autentikasi memverifikasi identitas pemanggil — siapa yang membuat permintaan. Ktor menyatukan tiga mekanisme utama (Basic, JWT, Session) di bawah satu plugin Authentication. Setiap mekanisme diimplementasikan sebagai provider yang dapat dipasang secara berurutan. Pada chapter ini kita akan memahami kontrak Principal, AuthProvider, dan pipeline authentication, lalu memodelkannya dalam program yang dapat dijalankan tanpa dependensi eksternal.
Plugin Authentication #
Plugin Authentication menyimpan daftar provider. Saat permintaan masuk, plugin mencoba provider satu per satu sesuai urutan pemasangan. Provider pertama yang berhasil memverifikasi akan mengisi call.principal — dan pipeline berhenti mencari. Bila semua provider gagal, plugin tidak mengisi principal dan handler bertanggung jawab mengembalikan 401.
install(Authentication) {
basic("basic") {
realm = "My API"
validate { credentials ->
if (users[credentials.name] == credentials.password) UserIdPrincipal(credentials.name) else null
}
}
jwt("jwt") {
verifier(jwtVerifier)
realm = "My API"
validate { credential -> if (credential.payload.subject != null) UserPrincipal(...) else null }
}
session<UserSession>("session") {
validate { session -> if (sessions.contains(session.id)) session else null }
}
}Setiap blok (basic, jwt, session) memasang satu provider dengan nama unik. Route kemudian memilih provider yang dipakai via authenticate("jwt") { ... }.
Principal dan AuthProvider #
Principal adalah representasi identitas yang sudah terverifikasi — misalnya UserPrincipal(id, nama, peran). Setiap provider menghasilkan tipe principal sendiri, tetapi semuanya mengimplementasi interface Principal agar handler dapat memperlakukannya secara generik.
AuthProvider adalah kontrak untuk mekanisme autentikasi. Setiap provider menerima ApplicationCall, mengekstrak kredensial dari header/cookie, memverifikasi, dan mengembalikan Principal? (null bila gagal). Struktur ini membuat penambahan mekanisme baru (mis. OAuth, API Key) menjadi straightforward.
Tiga Provider Utama #
| Provider | Sumber kredensial | Cocok untuk |
|---|---|---|
| Basic | Header Authorization: Basic base64(user:pass) | Skrip internal, dev tool |
| JWT | Header Authorization: Bearer <token> | API publik, microservice |
| Session | Cookie SESSION | Aplikasi web dengan login |
Basic Authentication #
Basic auth mengirim kredensial di header Authorization dengan format Basic base64(user:pass). Karena kredensial dikirim ulang setiap permintaan, Basic auth harus selalu dipakai dengan HTTPS. Provider membaca header, decode base64, pisahkan user dan password, lalu verifikasi terhadap penyimpanan kredensial.
JWT Authentication #
JWT (JSON Web Token) adalah token yang ditandatangani server. Klien mengirim token di header Authorization: Bearer <token> untuk setiap permintaan. Server memverifikasi tanda tangan (tanpa query database), membaca klaim, dan menghasilkan Principal. JWT cocok untuk distributed system karena verifikasi bersifat stateless.
Session Authentication #
Session memakai cookie SESSION yang berisi ID sesi. Server menyimpan mapping ID sesi ke data pengguna di cache (Redis, Memcached). Saat permintaan datang, provider membaca cookie, mencari sesi di cache, dan menghasilkan principal. Session cocok untuk aplikasi web dengan logout aktif.
Demo Authentication Pipeline #
Kita akan memodelkan plugin Authentication dengan tiga provider: Basic, JWT, dan Session. Program ini mendemonstrasikan bagaimana pipeline mencoba provider satu per satu sampai salah satu berhasil.
// ktor_authentication.kt
// Mendemonstrasikan plugin Authentication pada Ktor: Basic auth, JWT,
// dan Session. Pada kode produksi, plugin ini menyediakan pipeline
// auth yang dipasang ke Application.
import java.util.Base64
// Principal = identitas terverifikasi
interface Principal
data class UserPrincipal(val id: Int, val nama: String, val peran: String) : Principal
data class SessionPrincipal(val sessionId: String, val userId: Int) : Principal
// Authentication provider
interface AuthProvider {
val name: String
fun authenticate(call: ApplicationCall): Principal?
}
class ApplicationCall(
val headers: Map<String, String> = emptyMap(),
val cookies: Map<String, String> = emptyMap()
) {
var principal: Principal? = null
var responseStatus: Int = 200
var responseBody: String = ""
}
// Basic auth: header "Authorization: Basic base64(user:pass)"
class BasicAuthProvider(
private val validCredentials: Map<String, String>
) : AuthProvider {
override val name = "basic"
override fun authenticate(call: ApplicationCall): Principal? {
val authHeader = call.headers["Authorization"] ?: return null
if (!authHeader.startsWith("Basic ")) return null
val decoded = String(Base64.getDecoder().decode(authHeader.removePrefix("Basic ")))
val (user, pass) = decoded.split(":", limit = 2)
if (validCredentials[user] == pass) {
return UserPrincipal(id = user.hashCode(), nama = user, peran = "user")
}
return null
}
}
// JWT auth (mock): header "Authorization: Bearer <token>"
class JwtAuthProvider(
private val validTokens: Map<String, UserPrincipal>
) : AuthProvider {
override val name = "jwt"
override fun authenticate(call: ApplicationCall): Principal? {
val authHeader = call.headers["Authorization"] ?: return null
if (!authHeader.startsWith("Bearer ")) return null
val token = authHeader.removePrefix("Bearer ").trim()
return validTokens[token]
}
}
// Session auth: cookie "SESSION"
class SessionAuthProvider(
private val validSessions: Map<String, SessionPrincipal>
) : AuthProvider {
override val name = "session"
override fun authenticate(call: ApplicationCall): Principal? {
val sessionId = call.cookies["SESSION"] ?: return null
return validSessions[sessionId]
}
}
// Authentication pipeline: coba provider satu per satu
class Authentication {
val providers = mutableListOf<AuthProvider>()
fun install(provider: AuthProvider) {
providers.add(provider)
}
fun authenticate(call: ApplicationCall): Principal? {
for (provider in providers) {
val principal = provider.authenticate(call)
if (principal != null) {
call.principal = principal
return principal
}
}
return null
}
}
fun main() {
val basic = BasicAuthProvider(mapOf("budi" to "rahasia123", "siti" to "katasandi"))
val jwt = JwtAuthProvider(mapOf(
"token-budi-2026" to UserPrincipal(1, "Budi", "admin"),
"token-siti-2026" to UserPrincipal(2, "Siti", "editor")
))
val session = SessionAuthProvider(mapOf(
"sess-xyz" to SessionPrincipal("sess-xyz", 1)
))
val auth = Authentication().apply {
install(basic)
install(jwt)
install(session)
}
println("=== Basic auth ===")
val basicToken = Base64.getEncoder().encodeToString("budi:rahasia123".toByteArray())
val c1 = ApplicationCall(headers = mapOf("Authorization" to "Basic $basicToken"))
val p1 = auth.authenticate(c1)
println("Principal: $p1")
println("\n=== JWT auth ===")
val c2 = ApplicationCall(headers = mapOf("Authorization" to "Bearer token-siti-2026"))
val p2 = auth.authenticate(c2)
println("Principal: $p2")
println("\n=== Session auth ===")
val c3 = ApplicationCall(cookies = mapOf("SESSION" to "sess-xyz"))
val p3 = auth.authenticate(c3)
println("Principal: $p3")
println("\n=== Invalid credentials ===")
val c4 = ApplicationCall(headers = mapOf("Authorization" to "Bearer invalid-token"))
val p4 = auth.authenticate(c4)
println("Principal: $p4 (status ${if (p4 == null) "401" else "200"})")
println("\n=== Tanpa header ===")
val c5 = ApplicationCall()
val p5 = auth.authenticate(c5)
println("Principal: $p5 (status ${if (p5 == null) "401" else "200"})")
}Output yang Dihasilkan:
=== Basic auth ===
Principal: UserPrincipal(id=3035160, nama=budi, peran=user)
=== JWT auth ===
Principal: UserPrincipal(id=2, nama=Siti, peran=editor)
=== Session auth ===
Principal: SessionPrincipal(sessionId=sess-xyz, userId=1)
=== Invalid credentials ===
Principal: null (status 401)
=== Tanpa header ===
Principal: null (status 401)Mari kita bedah bagian penting kode di atas:
interface Principal: marker interface untuk identitas terverifikasi. Setiap provider menghasilkan tipe principal sendiri (UserPrincipal,SessionPrincipal) yang mengimplementasi interface ini.interface AuthProvider { fun authenticate(call): Principal? }: kontrak provider. Method mengembalikannullbila gagal — pipeline akan mencoba provider berikutnya.BasicAuthProvider: membaca headerAuthorization, decode base64, pisahkan user:pass, dan verifikasi. Berhasil →UserPrincipal. Gagal → null.JwtAuthProvider: membaca headerAuthorization: Bearer <token>, mencari token di map. Pada produksi, JWT diverifikasi via signature — bukan lookup table — sehingga stateless.for (provider in providers): pipeline mencoba provider berurutan. Provider pertama yang berhasil mengisicall.principaldan menghentikan iterasi.
Referensi Lanjutan #
- Ktor authentication
— panduan resmi plugin
Authenticationmencakup BASIC, FORM, JWT, dan OAuth di Ktor
Selanjutnya #
Autentikasi menjawab “siapa”. Langkah berikutnya adalah authorization — “apa yang boleh dilakukan”. Chapter Ktor Authorization akan membahas role-based access control, route-level guard, dan kebijakan akses berbasis principal.