Endpoint kita sejauh ini terbuka untuk siapa saja. Pada Bagian 4 kita melindunginya dengan authentication JWT: klien harus mengirim token yang valid untuk mengakses /tasks. Kita juga menulis test suite yang memvalidasi perilaku API termasuk kasus sukses, galat validasi, dan token tidak valid.
Konsep JWT #
JSON Web Token (JWT) adalah format token yang ditandatangani secara kriptografis. Token terdiri dari tiga bagian yang dipisahkan titik: header, payload, dan signature. Server memverifikasi signature untuk memastikan token tidak dimodifikasi. JWT bersifat stateless: server tidak perlu menyimpan sesi; verifikasi hanya memerlukan secret key.
Tiga manfaat JWT untuk REST API:
- Stateless: tidak butuh server-side session store.
- Portable: token dapat divalidasi oleh multiple service.
- Ekspirable: claim
expmengatur masa berlaku.
Implementasi JWT Ringkas #
Kode berikut menunjukkan JWT dengan HMAC-SHA256 tanpa library eksternal. Untuk produksi, gunakan library seperti jsonwebtoken atau Ktor auth-jwt:
// Bagian 4: Authentication (JWT) dan testing.
// Mendemonstrasikan pembuatan dan verifikasi token JWT sederhana (HMAC),
// serta struktur test untuk endpoint yang dilindungi.
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import java.util.Base64
object JwtRingkas {
private const val ALGO = "HmacSHA256"
private const val SECRET = "demo-secret-key-tidak-untuk-produksi"
fun buat(userId: String, payload: Map<String, String>): String {
val header = """{"alg":"HS256","typ":"JWT"}"""
val body = buildString {
append("{")
append("\"sub\":\"$userId\"")
payload.forEach { (k, v) -> append(",\"$k\":\"$v\"") }
append("}")
}
val headerB64 = base64Url(header.toByteArray())
val bodyB64 = base64Url(body.toByteArray())
val signingInput = "$headerB64.$bodyB64"
val signature = hmac(signingInput.toByteArray())
return "$signingInput.${base64Url(signature)}"
}
fun verifikasi(token: String): Result<Map<String, String>> {
val parts = token.split(".")
if (parts.size != 3) return Result.failure(IllegalArgumentException("Format token tidak valid"))
val (headerB64, bodyB64, sigB64) = parts
val signingInput = "$headerB64.$bodyB64"
val expectedSig = base64Url(hmac(signingInput.toByteArray()))
if (sigB64 != expectedSig) return Result.failure(IllegalArgumentException("Signature tidak cocok"))
val bodyJson = String(Base64.getUrlDecoder().decode(bodyB64))
return Result.success(parseKlaim(bodyJson))
}
private fun hmac(data: ByteArray): ByteArray {
val mac = Mac.getInstance(ALGO)
mac.init(SecretKeySpec(SECRET.toByteArray(), ALGO))
return mac.doFinal(data)
}
private fun base64Url(bytes: ByteArray): String =
Base64.getUrlEncoder().withoutPadding().encodeToString(bytes)
private fun parseKlaim(json: String): Map<String, String> {
val isi = json.trim().removePrefix("{").removeSuffix("}")
val result = mutableMapOf<String, String>()
val re = Regex(""""([^"]+)":"([^"]*)"""")
re.findAll(isi).forEach { m ->
val (k, v) = m.destructured
result[k] = v
}
return result.toMap()
}
}
object Auth {
private val pengguna = mapOf(
"budi" to "rahasia123",
"siti" to "aman123"
)
fun login(nama: String, sandi: String): Result<String> {
val expected = pengguna[nama]
if (expected == null || expected != sandi) {
return Result.failure(IllegalArgumentException("Kredensial tidak valid"))
}
val token = JwtRingkas.buat(nama, mapOf("role" to "user"))
return Result.success(token)
}
fun otorisasi(token: String): Result<String> {
val klaim = JwtRingkas.verifikasi(token).getOrElse {
return Result.failure(IllegalArgumentException("Token tidak valid"))
}
return Result.success(klaim["sub"] ?: "unknown")
}
}
object Test {
private var lulus = 0
private var gagal = 0
fun nama(nama: String, blok: () -> Unit) {
try { blok(); println("PASS $nama"); lulus++ }
catch (e: Throwable) { println("FAIL $nama: ${e.message}"); gagal++ }
}
fun assertEquals(a: Any?, b: Any?) { if (a != b) throw AssertionError("diharapkan <$a> aktual <$b>") }
fun assertTrue(v: Boolean) { if (!v) throw AssertionError("diharapkan true") }
fun assertFalse(v: Boolean) { if (v) throw AssertionError("diharapkan false") }
fun ringkasan() { println(); println("Total: ${lulus + gagal} | Lulus: $lulus | Gagal: $gagal") }
}
fun main() {
println("=== Login ===")
val tokenBerhasil = Auth.login("budi", "rahasia123")
println("Login budi: ${tokenBerhasil.fold(onSuccess = { "token: ${it.take(40)}..." }, onFailure = { it.message })}")
val tokenGagal = Auth.login("budi", "salah")
println("Login salah: ${tokenGagal.fold(onSuccess = { "berhasil?!" }, onFailure = { it.message })}")
println()
println("=== Verifikasi token ===")
val token = tokenBerhasil.getOrThrow()
val klaim = JwtRingkas.verifikasi(token).getOrThrow()
println("Klaim: $klaim")
println()
println("=== Token dirusak ===")
val rusak = token.dropLast(5) + "XXXXX"
val hasilRusak = JwtRingkas.verifikasi(rusak)
println("Verifikasi rusak: ${hasilRusak.fold(onSuccess = { "berhasil?!" }, onFailure = { it.message })}")
println()
println("=== Test suite ===")
with(Test) {
nama("login berhasil kembalikan token") {
val t = Auth.login("siti", "aman123")
assertTrue(t.isSuccess)
}
nama("login gagal kembalikan galat") {
val t = Auth.login("siti", "salah")
assertTrue(t.isFailure)
}
nama("otorisasi token valid kembalikan sub") {
val t = Auth.login("budi", "rahasia123").getOrThrow()
val sub = Auth.otorisasi(t).getOrThrow()
assertEquals("budi", sub)
}
nama("otorisasi token rusak gagal") {
val r = Auth.otorisasi("abc.def.ghi")
assertTrue(r.isFailure)
}
nama("token dengan payload kustom") {
val t = JwtRingkas.buat("user123", mapOf("role" to "admin", "tim" to "backend"))
val k = JwtRingkas.verifikasi(t).getOrThrow()
assertEquals("admin", k["role"])
assertEquals("backend", k["tim"])
}
ringkasan()
}
}Output yang Dihasilkan:
=== Login ===
Login budi: token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ...
Login salah: Kredensial tidak valid
=== Verifikasi token ===
Klaim: {sub=budi, role=user}
=== Token dirusak ===
Verifikasi rusak: Signature tidak cocok
=== Test suite ===
PASS login berhasil kembalikan token
PASS login gagal kembalikan galat
PASS otorisasi token valid kembalikan sub
PASS otorisasi token rusak gagal
PASS token dengan payload kustom
Total: 5 | Lulus: 5 | Gagal: 0Mari kita bedah komponen JWT:
Mac.getInstance("HmacSHA256"): mendapatkan instance HMAC dengan algoritma SHA-256.Macadalah class Java standard library untuk komputasi HMAC.SecretKeySpec(SECRET.toByteArray(), ALGO): membuat kunci rahasia dari string. Secret harus tetap konfidensial; jika bocor, siapa pun dapat memalsukan token.Base64.getUrlEncoder().withoutPadding(): Base64URL adalah varian Base64 yang aman untuk URL (+menjadi-,/menjadi_, dan trailing=dihilangkan). Format standar JWT.token.split("."): token JWT selalu memiliki tiga bagian. Jika bukan tiga, format tidak valid.sigB64 != expectedSig: perbandingan signature. Jika tidak cocok, token telah dimodifikasi dan harus ditolak.Regex(""""([^"]+)":"([^"]*)""""): parsing JSON sederhana untuk mengekstrak klaim. Untuk produksi, gunakankotlinx.serializationyang menangani JSON dengan benar.
Alur Authentication #
Alur authentication terdiri dari dua langkah:
- Login: klien mengirim username dan password ke
/auth/login. Server memverifikasi kredensial dan mengembalikan JWT. - Akses endpoint terlindungi: klien menyertakan JWT di header
Authorization: Bearer <token>. Server memverifikasi signature dan ekstrak identitas pengguna.
Pola ini memisahkan login dari akses, sehingga token dapat dipakai berkali-kali tanpa mengirim password.
Test Suite #
Test suite mencakup tiga kategori:
- Login sukses/gagal: kredensial valid mengembalikan token; kredensial salah mengembalikan galat.
- Otorisasi token valid/rusak: token yang ditandatangani server valid; token yang dimodifikasi ditolak.
- Payload kustom: klaim tambahan seperti
roledantimdapat ditambahkan dan dibaca kembali.
Test yang baik tidak bergantung pada urutan eksekusi. Setiap @Test harus dapat berjalan terpisah. Pada kode di atas, state pengguna tidak berubah, sehingga test tidak saling memengaruhi.
Referensi Lanjutan #
- Ktor authentication
— panduan
Authenticationuntuk melindungi endpoint dengan JWT atau sesi pengguna - Ktor testing
— panduan
testApplicationuntuk menguji route tanpa menjalankan server nyata
Ringkasan Bagian 4 dan Penutup Proyek #
Authentication JWT dan test suite kini lengkap. JwtRingkas membuat dan memverifikasi token dengan HMAC-SHA256; Auth mengelola login dan otorisasi; Test harness memvalidasi lima skenario kunci.
Proyek REST API Manajemen Tugas telah selesai. Kita mendesain endpoint di Bagian 1, mengimplementasikan routing di Bagian 2, menambahkan database SQLite di Bagian 3, dan melindungi endpoint dengan JWT serta menulis test di Bagian 4. API kini siap melayani klien dengan keamanan dan ketertiban kontrak. Selanjutnya, kita akan beralih ke proyek Aplikasi Inventaris yang fokus pada transaksi barang masuk dan keluar dengan database yang lebih kompleks.