Halo sobat kampus dan sesama developer muda! Buat kalian yang lagi sibuk ngerjain skripsi, bikin proyek startup, atau sekadar hobi ngulik kode di kamar kos, cerita dari balik layar pengembangan aplikasi ini bakal kasih kalian pelajaran mahal tentang arsitektur keamanan sistem. Cerita ini datang langsung dari pengalaman nyata seorang solo developer yang membangun aplikasi keuangan pribadi untuk iOS bernama EveryPenny. Niat awal bikin sistem keamanan sekelas standar industri, tapi justru fitur itu yang bikin seisi aplikasi lockout massal, bahkan mengunci akun developernya sendiri!

Meniru Buku Teks Keamanan Sempurna

Dalam dunia pengembangan perangkat lunak modern, khususnya manajemen autentikasi (auth), ada satu pola arsitektur yang dianggap paling ideal dan dianjurkan oleh berbagai panduan keamanan global, termasuk OWASP. Skemanya kurang lebih seperti ini:

  • Sistem menggunakan access token berumur pendek (short-lived) dan refresh token berumur panjang yang bisa di-rotate.
  • Setiap kali klien melakukan refresh, mereka harus mengirimkan refresh token lamanya.
  • Server kemudian akan memverifikasi token tersebut, mencabutnya (revoke), dan menerbitkan sepasang token baru (access + refresh).
  • Token lama yang sudah dipakai otomatis mati—alias hanya bisa dipakai sekali (single use).

Nah, ada satu rekomendasi penting yang sering ditekankan, yaitu deteksi penggunaan ulang refresh token (refresh-token reuse detection). Logikanya begini: kalau ada klien yang mengirimkan refresh token yang statusnya sudah dicabut, sistem harus menganggapnya sebagai indikasi pencurian. Kenapa? Karena asumsinya ada hacker jahat yang berhasil mencuri token dan mencoba memakainya lagi. Respons standar dari sistem keamanan yang ketat adalah kebijakan bumi hangus (scorched earth): cabut semua sesi aktif untuk user tersebut, sehingga baik si hacker maupun korban sama-sama terdepak dan dipaksa login ulang dari awal.

Teorinya sih brilian dan sangat aman. Kelihatan keren di atas kertas, langsung ship it ke App Store!

Insiden Horor di Malam Hari

Suatu malam yang tenang, saat gue lagi buka aplikasi buatan gue sendiri, tiba-tiba muncul auth error yang bikin jantungan. Saldo keuangan yang tadinya tercatat mendadak berubah jadi Rp0, lengkap dengan layar sambutan bertuliskan "add your first account"—seolah-olah gue adalah pengguna baru yang belum pernah login seumur hidup. Padahal, kalau dicek di backend, semua data keuangan aman sentosa. Yang mati cuma sesi login-nya doang!

Setelah ngecek audit log, barulah misteri ini terungkap. Sepanjang hari, proses refresh berjalan normal setiap 15 menit. Tapi kemudian, ada satu momen di mana server mendapati sebuah token yang sudah di-revoke dipakai ulang. Sistem langsung panik, mendeklarasikan kondisi darurat pencurian, dan otomatis menendang keluar semua sesi. Pertanyaannya, siapa hacker jahat yang membajak akun ini?

Usut punya usut, semua sesi di dalam rantai tersebut ternyata membawa device id yang persis sama. Pelakunya adalah HP gue sendiri. Smartphone gue telah merampok dirinya sendiri!

Bagaimana Sebuah Perangkat Bisa Merampok Dirinya Sendiri?

Ternyata, penggunaan ulang token (replay) oleh perangkat yang sama itu sangat lumrah terjadi karena alasan teknis yang sama sekali tidak ada hubungannya dengan kejahatan siber. Beberapa skenario umum penyebabnya meliputi:

  • Respons HTTP yang Hilang (Dropped Response): Server berhasil memproses rotasi token dan menyimpan data baru, tapi jaringan tiba-tiba ngadat atau aplikasi beralih ke latar belakang (backgrounded). Klien tidak pernah menerima token baru, sehingga dia masih memegang token lama dan mencoba melakukan retry. Di mata server, ini terlihat seperti token basi yang dipakai lagi.
  • Gagal Menulis ke Penyimpanan Lokal (Failed Local Write): Pasangan token baru berhasil dikirim kembali ke HP, tapi proses penyimpanan ke keychain gagal atau mengalami race condition saat aplikasi mendadak suspend. Saat aplikasi dibuka kembali, klien terpaksa mengirimkan token lama.
  • Dua Proses Berjalan Bersamaan (Process Racing): Aplikasi utama dan widget extension di layar utama sama-sama mendeteksi status 401 (Unauthorized) secara bersamaan, lalu kompak melakukan refresh menggunakan token yang sama. Proses pertama berhasil menang dan melakukan rotasi token, sementara proses kedua datang sedetik kemudian membawa token yang sudah dianggap mati.

Tidak satupun dari skenario di atas adalah aksi pencurian data. Tapi karena server memukul rata bahwa "token mati yang dikirim ulang = pencurian", protokol bumi hangus pun aktif dan sukses bikin gue logout dari semua perangkat.

Wawasan Baru dan Solusi Perbaikan

Dari kejadian konyol ini, ada satu pencerahan besar yang terlewatkan saat merancang sistem: untuk kasus penggunaan ulang token dari perangkat yang sama (same-device replay), mekanisme pencabutan total hampir tidak memberikan tambahan keamanan apa pun.

Deteksi penggunaan ulang token sebenarnya dirancang untuk melawan penjahat siber yang berhasil mencuri refresh token dari database atau jaringan, lalu memakainya dari perangkat lain yang entah di mana. Perangkat lain itulah yang menjadi sinyal bahaya. Kalau ada hacker mencoba memakainya dari perangkat yang sama dengan device id yang sama, mereka sebenarnya sudah punya akses penuh ke penyimpanan lokal perangkat tersebut. Jadi, kebijakan bumi hangus tidak akan menyelamatkanmu karena si hacker toh bisa langsung mengambil token aktif yang sedang dipakai.

Kesimpulannya, sinyal pencurian yang sesungguhnya bukanlah sekadar "token yang sudah di-revoke dipakai lagi", melainkan "token yang sudah di-revoke dipakai dari perangkat yang berbeda dari yang seharusnya".

Cara Memperbaikinya di Kode

Untuk mengatasi masalah ini, kuncinya adalah mengikat sesi (session) ke device id yang spesifik, lalu membatasi respons ekstrem hanya pada skenario yang benar-benar mencurigakan:

if (session.revokedAt) {
  const sameDeviceBenignReplay =
    session.deviceId !== UNBOUND &&
    session.deviceId === presentedDeviceId &&
    session.revokedReason === "rotated";

  if (!sameDeviceBenignReplay) {
    await revokeAllSessions(session.userId, "theft_detected");
  }
  throw new RefreshError("token_replayed"); // 401 either way
}

Dengan logika di atas, dampaknya jadi jauh lebih masuk akal:

  • Jika perangkat yang sama mengirimkan token lamanya sendiri, sistem cukup memberikan respons 401 standar. Klien bisa langsung melakukan re-autentikasi atau mengambil token baru milik proses yang menang tadi tanpa harus mendepak user keluar aplikasi.
  • Jika token dikirim dari perangkat yang berbeda, atau pencabutannya bukan karena rotasi normal (misalnya karena sign-out manual), barulah jalankan protokol bumi hangus. Sinyal pencurian yang valid akan tetap ditangani secara tegas.

Selain itu, ada tiga langkah perbaikan pendukung yang membuat sistem ini benar-benar tahan banting:

  • Koordinator Refresh Lintas Proses: Aplikasi utama dan widget sekarang menggunakan mekanisme single-flight mutex (menggunakan file lock di direktori bersama), sehingga hanya ada satu proses refresh yang berjalan dalam satu waktu per perangkat. Proses yang kalah akan menunggu token baru dari proses yang menang alih-alih saling balapan.
  • Perkuat Penyimpanan Keychain: Tambahkan pengecekan status saat menulis ke keychain, lakukan retry sekali jika gagal, dan catat log dengan jelas agar kegagalan senyap bisa terdeteksi.
  • Jangan Ubah Gangguan Sementara Jadi Logout: Masalah jaringan yang terputus atau error server 5xx pada endpoint refresh harus bisa dicoba ulang (retryable), bukan malah langsung memvonis bahwa sesi pengguna telah mati. Hanya respons 401 atau 402/403 murni dari server auth yang boleh memicu perintah keluar aplikasi.

Worth It Gak Buat Mahasiswa?

Buat kalian mahasiswa teknik informatika, sistem, atau peminat software engineering, memahami studi kasus seperti ini sangat worth it banget buat nambah wawasan industri. Belajar dari kesalahan arsitektur nyata jauh lebih berharga ketimbang sekadar membaca teori di buku teks kuliah. Jangan sampai aplikasi portofolio tugas akhir kalian nanti malah bikin pusing pengguna karena terlalu berlebihan menerapkan standar keamanan tanpa memperhitungkan realitas jaringan internet kampus yang kadang naik-turun!

Kelebihan Pendekatan Ini:

  • Meningkatkan kenyamanan pengguna (user experience) tanpa mengorbankan standar keamanan siber.
  • Mencegah bug fatal akibat sistem keamanan yang terlalu agresif (over-engineered).
  • Mempersiapkan pola pikir arsitektur backend yang tahan terhadap skenario jaringan dunia nyata.

Kekurangan Pendekatan Ini:

  • Membutuhkan logika kode yang sedikit lebih kompleks di sisi penanganan sesi dan sinkronisasi lintas proses.
  • Harus teliti dalam mengelola device binding agar tidak terjadi celah keamanan baru.

Pelajaran terbesarnya sederhana: sebuah kontrol keamanan yang terlalu agresif tidak ada bedanya dengan bug bagi pengguna yang terkunci di luar aplikasi. Batasi radius ledakan sistem keamanan hanya pada ancaman nyata yang benar-benar terjadi!

Buat kalian yang lagi merintis aplikasi sendiri atau mempersiapkan diri menembus dunia kerja di bidang teknologi, yuk asah terus skill ngoding kalian! Segera siapkan CV terbaik dan portofolio proyek keren kalian di GitHub, karena industri tech selalu butuh developer muda yang jago mik kritis dan solutif menghadapi masalah rumit!