Kontrak API auth yang tahan integrasi tidak dimulai dari middleware, library JWT, atau fitur SDK. Ia dimulai dari pertanyaan yang lebih dasar: state apa yang sedang dikelola, invariants apa yang tidak boleh rusak, siapa yang bertanggung jawab atas keputusan auth, dan bagaimana sistem gagal ketika token kedaluwarsa, request terduplikasi, atau waktu antar-mesin tidak sinkron.
Dengan kerangka software from first principles, tujuan kita bukan membuat auth yang “canggih”, tetapi membuat kontrak yang tetap konsisten ketika diakses banyak klien: web, mobile, service-to-service, job worker, atau integrasi pihak ketiga. Jika kontraknya jelas, klien bisa menulis retry logic, refresh flow, dan error handling tanpa menebak-nebak perilaku server.
Mulai dari first principles, bukan dari framework
Pola pikir first principles berguna karena masalah integrasi auth hampir selalu muncul dari asumsi yang tidak ditulis. Framework biasanya memberi mekanisme, tetapi tidak otomatis memberi kontrak yang stabil. Sebelum memilih bearer token, cookie session, JWT, opaque token, atau OAuth flow tertentu, definisikan dulu hal-hal berikut.
1. Definisikan state auth secara eksplisit
Minimal, sistem auth modern biasanya memiliki state berikut:
- Unauthenticated: request belum punya identitas yang tervalidasi.
- Authenticated: identitas valid, tetapi belum tentu punya izin.
- Authorized: identitas valid dan memiliki hak untuk aksi tertentu.
- Expired: token pernah valid tetapi masa berlakunya habis.
- Revoked: token tidak lagi boleh dipakai walaupun belum expired.
- Refreshable: access token habis, tetapi refresh token masih valid.
- Non-refreshable: sesi harus login ulang.
Jika state ini tidak dibedakan, klien akan menerima error yang terlihat sama untuk masalah yang sebenarnya berbeda. Akibatnya, aplikasi mobile bisa terus mencoba refresh untuk token yang sudah dicabut, atau frontend menampilkan layar login padahal sebenarnya hanya kurang permission.
2. Tentukan invariants yang tidak boleh dilanggar
Invariant adalah aturan yang harus selalu benar. Untuk kontrak API auth, beberapa invariant penting adalah:
- Access token yang expired tidak boleh menghasilkan akses sukses.
- Refresh token yang sudah dipakai untuk rotasi tidak boleh diterima lagi tanpa aturan yang jelas.
- Endpoint sensitif seperti refresh, logout, dan perubahan kredensial harus punya perilaku idempotent atau setidaknya aman terhadap duplikasi request.
- Status code dan body error harus konsisten untuk kelas kegagalan yang sama.
- Server tidak boleh mengandalkan jam klien sebagai sumber kebenaran.
Begitu invariant ini tertulis, keputusan implementasi menjadi lebih mudah. Misalnya, jika invariant Anda menyatakan refresh token sekali pakai, maka Anda perlu strategi untuk menangani dua request refresh yang datang hampir bersamaan dari perangkat yang sama.
3. Tetapkan batas tanggung jawab
Bug auth sering terjadi karena tanggung jawab kabur. Kontrak yang sehat biasanya membagi tanggung jawab seperti ini:
- Server auth: memverifikasi identitas, masa berlaku token, revocation, dan izin.
- Klien: menyimpan token sesuai model ancaman, mengirim header yang benar, dan menjalankan retry/refresh hanya sesuai kontrak.
- Gateway atau middleware: menegakkan kebijakan yang seragam, tetapi tidak mengubah semantik error seenaknya.
- Downstream service: tidak menebak status auth dari header parsial; ia menerima identitas yang sudah tervalidasi atau menolak request.
Jika klien dipaksa menebak apakah 401 berarti refresh atau login ulang, maka kontraknya belum cukup jelas. Jika server mengembalikan 200 dengan body error untuk kasus auth gagal, maka observability dan interop akan rusak.
Rancang kontrak auth dari state dan failure mode
Bedakan 401 dan 403 dengan disiplin
Perbedaan 401 Unauthorized dan 403 Forbidden harus dijaga ketat karena ini memengaruhi perilaku klien.
- 401: request belum memiliki autentikasi yang valid untuk resource tersebut. Contoh: token hilang, token malformed, token expired, signature invalid.
- 403: autentikasi valid, tetapi identitas tersebut tidak punya izin untuk aksi itu.
Kenapa ini penting? Karena banyak klien otomatis melakukan refresh ketika menerima 401. Jika server salah mengembalikan 401 untuk kasus kurang permission, klien akan terus refresh tanpa hasil. Sebaliknya, jika token expired dikembalikan sebagai 403, klien mungkin menampilkan “akses ditolak” padahal pengguna hanya butuh refresh sesi.
Contoh respons yang lebih jelas:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="api", error="invalid_token", error_description="access token expired"
Content-Type: application/json
{
"error": {
"code": "AUTH_TOKEN_EXPIRED",
"message": "Access token expired",
"retryable": true,
"action": "REFRESH_TOKEN"
}
}Untuk kasus izin tidak cukup:
HTTP/1.1 403 Forbidden
Content-Type: application/json
{
"error": {
"code": "AUTH_INSUFFICIENT_SCOPE",
"message": "Missing permission: invoices:write",
"retryable": false,
"action": "REQUEST_DIFFERENT_CREDENTIALS"
}
}Catatan penting: field seperti retryable dan action bukan standar wajib, tetapi sangat membantu jika Anda ingin klien lintas platform punya keputusan yang konsisten tanpa parsing string error.
Desain token refresh sebagai protokol, bukan endpoint biasa
Refresh flow sering dianggap sederhana: kirim refresh token, dapat access token baru. Dalam praktiknya, bagian ini rawan race condition, replay, dan logout paksa yang sulit direproduksi.
Pertanyaan desain yang perlu dijawab sejak awal:
- Apakah refresh token rotating atau reusable?
- Jika rotating, apa yang terjadi bila dua request refresh dikirim hampir bersamaan?
- Apakah access token lama tetap berlaku selama grace period pendek?
- Bagaimana server menandai token revoked?
- Apakah refresh token terikat ke device/session tertentu?
Untuk banyak sistem, pendekatan yang stabil adalah:
- Access token berumur pendek.
- Refresh token berumur lebih panjang.
- Setiap refresh mengembalikan pasangan token baru.
- Refresh token lama ditandai tidak aktif setelah rotasi berhasil.
- Server mendeteksi replay atau duplikasi request refresh.
Contoh request/response:
POST /auth/refresh HTTP/1.1
Content-Type: application/json
Idempotency-Key: 1f4c7f2a-8d9a-4c11-a3be-2fd9c2c91a40
{
"refresh_token": "rt_abc123...",
"client_id": "mobile-app"
}HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache
{
"access_token": "at_def456...",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token": "rt_new789..."
}Jika refresh token sudah tidak valid:
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"error": {
"code": "AUTH_REFRESH_INVALID",
"message": "Refresh token is invalid or revoked",
"retryable": false,
"action": "REAUTHENTICATE"
}
}Mengapa Idempotency-Key relevan di endpoint refresh? Karena klien mobile atau proxy bisa mengulang request akibat timeout jaringan. Tanpa deduplikasi, satu request refresh yang sebenarnya berhasil bisa tampak gagal di sisi klien, lalu klien mengirim ulang dan malah dianggap replay berbahaya. Dengan idempotency key, server bisa mengembalikan hasil yang sama untuk percobaan ulang dalam jendela waktu tertentu.
Clock skew: masalah kecil yang sering merusak integrasi
Jam server, gateway, dan perangkat klien jarang benar-benar identik. Jika kontrak auth mengasumsikan sinkronisasi sempurna, Anda akan melihat bug yang sulit dijelaskan seperti token “kadang” dianggap expired tepat setelah diterbitkan.
Prinsip yang aman:
- Server menjadi sumber kebenaran untuk validitas token.
- Klien tidak menunggu sampai detik terakhir untuk refresh.
- Berikan margin waktu pada sisi klien, misalnya refresh beberapa saat sebelum
expires_inhabis. - Jika memakai claim berbasis waktu, pertimbangkan toleransi validasi yang wajar terhadap skew kecil.
Jangan mendesain klien dengan logika: “refresh tepat saat now == expiry”. Logika itu rapuh terhadap latency, antrian, background execution di mobile, dan clock drift.
Idempotency, retry aman, dan deduplikasi request
Kenapa endpoint auth perlu idempotency
Banyak tim hanya menerapkan idempotency pada pembayaran. Padahal beberapa endpoint auth juga sensitif terhadap duplikasi:
POST /auth/refreshPOST /auth/logoutPOST /auth/revokePOST /auth/mfa/verifyPOST /auth/password/reset/confirm
Retry yang dipicu timeout atau jaringan putus bisa menyebabkan efek ganda: token diputar dua kali, session tertutup dua kali, atau OTP terpakai tetapi klien tidak tahu hasil akhirnya.
Kapan retry aman, kapan tidak
Aturan praktis:
- Aman diretry jika endpoint idempotent secara alami atau didukung
Idempotency-Key. - Jangan retry buta pada semua 401. Bedakan expired token vs invalid credentials vs revoked token.
- Gunakan backoff untuk kegagalan sementara seperti 429 atau 5xx, tetapi jangan campur dengan auth failure permanen.
- Batasi concurrency pada refresh flow agar satu sesi tidak mengirim banyak refresh bersamaan.
Salah satu pola klien yang sehat adalah single-flight refresh: ketika beberapa request mendapat 401 karena token expired, hanya satu proses refresh yang dijalankan; request lain menunggu hasilnya. Ini mencegah storm refresh dan race condition rotasi token.
// Pseudocode klien
if (response.status == 401 && error.code == "AUTH_TOKEN_EXPIRED") {
await refreshCoordinator.refreshOnce()
return retryOriginalRequestWithNewToken()
}
throw response.errorDeduplikasi request di sisi server
Dari sisi server, deduplikasi biasanya memerlukan penyimpanan hasil berdasarkan kombinasi:
Idempotency-Key- identitas klien atau session
- endpoint atau operasi
- hash payload yang relevan
Jika key yang sama datang lagi dalam jendela waktu aktif, server mengembalikan hasil yang sama atau menolak jika payload berbeda. Ini penting untuk mencegah penyalahgunaan key yang sama untuk operasi yang berbeda.
Contoh respons konflik idempotency:
HTTP/1.1 409 Conflict
Content-Type: application/json
{
"error": {
"code": "IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_PAYLOAD",
"message": "The same Idempotency-Key was used with a different request payload",
"retryable": false
}
}Bentuk kontrak yang jelas: header, body, dan error model
Gunakan header yang eksplisit
Minimal, dokumentasikan header yang diharapkan dan artinya:
Authorization: Bearer <access_token>untuk akses API.Idempotency-Key: <uuid-or-random-key>untuk operasi sensitif yang boleh diretry.X-Request-Idatau padanan lain untuk trace/debug lintas layanan.WWW-Authenticatepada respons 401 jika relevan.
Hindari membuat klien menebak apakah token dikirim via cookie, header, atau keduanya kecuali memang kontraknya mendukung dua mode dengan jelas. Kontrak ambigu akan memicu bug CSRF, cache leakage, atau perilaku berbeda antar platform.
Samakan bentuk error
Banyak integrasi rusak bukan karena auth-nya salah, tetapi karena bentuk error berbeda antar endpoint. Pilih satu struktur error dan pakai konsisten.
{
"error": {
"code": "AUTH_TOKEN_EXPIRED",
"message": "Access token expired",
"retryable": true,
"action": "REFRESH_TOKEN",
"request_id": "req_12345"
}
}Prinsip desainnya:
codestabil untuk dipakai program.messageuntuk manusia dan debugging.retryablemembantu klien tidak menebak.request_idmemudahkan korelasi log.
Jangan andalkan parsing teks natural-language seperti “token invalid”, “expired jwt”, atau “forbidden user”. Itu akan patah saat pesan berubah.
Skenario bug integrasi nyata yang sering terjadi
Bug 1: 401 loop karena semua endpoint di-refresh otomatis
Gejala: frontend terus memanggil /auth/refresh berkali-kali sampai user logout sendiri.
Akar masalah: interceptor klien menganggap semua 401 berarti token expired. Padahal sebagian 401 berasal dari refresh token invalid atau credential salah.
Perbaikan kontrak:
- Bedakan error code untuk
AUTH_TOKEN_EXPIREDvsAUTH_REFRESH_INVALID. - Tentukan bahwa hanya error tertentu yang boleh memicu refresh.
- Pastikan endpoint refresh tidak ikut diproses interceptor refresh yang sama.
Bug 2: logout acak pada mobile karena race refresh
Gejala: user aktif, tetapi sesekali dipaksa login ulang saat berpindah layar atau jaringan.
Akar masalah: dua request API gagal hampir bersamaan, keduanya memicu refresh. Refresh pertama sukses dan merotasi token. Refresh kedua masih memakai refresh token lama, lalu dianggap replay atau revoked. Klien menafsirkan ini sebagai sesi tidak valid.
Perbaikan kontrak:
- Gunakan single-flight refresh di klien.
- Dokumentasikan perilaku refresh token rotating.
- Jika perlu, sediakan grace handling terbatas untuk duplikasi request refresh yang identik.
Bug 3: token dianggap expired padahal baru diterbitkan
Gejala: setelah login berhasil, request berikutnya langsung 401 di perangkat tertentu.
Akar masalah: clock skew pada klien atau validasi waktu terlalu ketat di server/gateway.
Perbaikan kontrak:
- Gunakan
expires_inuntuk klien, bukan menyuruh klien menafsirkan semua claim waktu mentah. - Terapkan toleransi skew yang wajar di sisi verifikasi.
- Refresh sebelum benar-benar habis.
Bug 4: duplicate refresh akibat timeout jaringan
Gejala: server menerbitkan token baru, tetapi klien tidak menerima respons karena timeout. Klien mengirim refresh lagi dan menerima error yang tidak konsisten.
Akar masalah: endpoint refresh tidak idempotent dan tidak punya deduplikasi.
Perbaikan kontrak:
- Dukung
Idempotency-Keypada refresh. - Simpan hasil operasi untuk jendela waktu tertentu.
- Kembalikan hasil yang sama untuk retry identik.
Anti-pattern umum dalam kontrak API auth
- Mencampur 401 dan 403 secara acak. Ini memaksa klien menebak aksi lanjutan.
- Semua error auth dikembalikan sebagai 200 dengan body error. Monitoring, proxy, dan SDK akan salah menangani respons.
- Refresh token tak punya aturan rotasi yang jelas. Akibatnya replay dan race sulit dibedakan dari bug jaringan.
- Klien refresh token pada setiap request. Ini memboroskan resource dan memperbesar risiko race.
- Pesan error untuk manusia dijadikan API contract. String berubah, integrasi pecah.
- Endpoint sensitif tanpa idempotency atau deduplikasi. Retry jaringan menjadi sumber bug.
- Mengandalkan jam klien sebagai kebenaran. Ini rapuh terutama di mobile dan lingkungan terdistribusi.
- Dokumentasi hanya menjelaskan happy path. Padahal masalah auth paling sering muncul di failure path.
Checklist desain kontrak API auth yang tahan integrasi
- Apakah state auth utama sudah didefinisikan: unauthenticated, authenticated, authorized, expired, revoked, refreshable?
- Apakah perbedaan 401 dan 403 dijaga konsisten di semua endpoint?
- Apakah ada error code yang stabil dan dapat diproses program?
- Apakah klien tahu kapan harus refresh, kapan harus login ulang, dan kapan tidak boleh retry?
- Apakah refresh flow punya aturan rotasi token yang terdokumentasi?
- Apakah endpoint refresh aman terhadap race condition dan duplicate request?
- Apakah endpoint sensitif mendukung
Idempotency-Keyatau semantik setara? - Apakah ada strategi menghadapi clock skew?
- Apakah respons auth menyertakan header dan body yang eksplisit?
- Apakah request/response error menyertakan
request_idatau identitas trace untuk debugging? - Apakah gateway, auth service, dan downstream service menggunakan semantik error yang sama?
- Apakah test contract mencakup failure mode, bukan hanya login sukses?
Panduan implementasi dan pengujian praktis
Uji contract, bukan hanya unit test
Untuk auth, unit test saja tidak cukup. Anda perlu contract test atau integration test yang memverifikasi:
- Token expired menghasilkan 401 dengan code yang tepat.
- Permission kurang menghasilkan 403, bukan 401.
- Refresh dengan token valid mengembalikan pasangan token baru.
- Refresh duplikat dengan idempotency key yang sama mengembalikan hasil konsisten.
- Refresh token lama setelah rotasi ditolak sesuai aturan.
- Retry request setelah refresh benar-benar memakai access token baru.
Tambahkan observability pada jalur auth
Masalah auth sering sulit dilacak karena menyentuh klien, gateway, dan beberapa service. Minimal, log yang berguna harus punya:
request_id- jenis token atau session identifier yang aman untuk korelasi, bukan token mentah
- hasil validasi: missing, expired, revoked, insufficient scope
- apakah request merupakan retry atau reuse idempotency key
Jangan pernah mencatat access token atau refresh token mentah ke log. Jika perlu korelasi, gunakan identifier turunan yang aman, misalnya hash atau token ID yang memang dirancang untuk observability.
Dokumentasikan failure path setara dengan happy path
Jika dokumentasi hanya menunjukkan login sukses dan contoh bearer token, integrator akan membangun asumsi sendiri. Dokumentasi kontrak auth yang baik harus memuat:
- contoh request/response sukses
- contoh 401 expired token
- contoh 401 refresh invalid
- contoh 403 insufficient scope
- aturan retry dan idempotency
- batas rate limit atau throttling jika ada
Penutup
First Principles untuk Kontrak API Auth yang Tahan Integrasi berarti Anda merancang auth dari realitas sistem, bukan dari fitur framework. Mulailah dengan state, invariants, batas tanggung jawab, dan mode gagal. Setelah itu baru pilih mekanisme token, middleware, dan format payload.
Hasilnya bukan sekadar API yang “aman”, tetapi kontrak yang bisa diprediksi oleh banyak klien. Di situlah integrasi menjadi tahan lama: 401 dan 403 tidak ambigu, refresh tidak mudah race, retry tidak merusak state, dan bug dapat didiagnosis dari kontrak yang jelas, bukan dari tebakan.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!