Fitur remember me aman bukan sekadar memperpanjang umur session. Session biasa hidup relatif singkat dan biasanya disimpan server-side, sedangkan persistent login harus tetap bekerja setelah browser ditutup atau session server habis. Karena itu, desainnya tidak boleh memakai pendekatan sederhana seperti menyimpan user ID di cookie atau menyimpan token mentah di database.
Pendekatan yang aman untuk remember me adalah memakai pasangan selector-validator, menyimpan hash token di database, melakukan rotasi token pada setiap pemakaian, memberi TTL yang terbatas, menambahkan device binding ringan, dan mendeteksi token reuse sebagai sinyal bahwa cookie mungkin dicuri. Artikel ini membahas alur backend, skema tabel, cookie flags, revoke, audit log, rate limit, dan contoh pseudocode yang netral framework.
Session biasa vs persistent login
Session biasa
Pada session biasa, server membuat session ID dan browser menyimpan cookie session. Jika cookie hilang, browser ditutup, session kedaluwarsa, atau store session dibersihkan, pengguna harus login lagi. Ini cocok untuk autentikasi utama setelah login berhasil.
Persistent login dengan remember me
Remember me adalah mekanisme tambahan agar pengguna bisa masuk kembali tanpa mengetik password setelah session utama habis. Umumnya ia bekerja seperti ini:
- Saat login dan pengguna mencentang remember me, server membuat token persistent.
- Browser menyimpan token itu di cookie terpisah.
- Ketika session biasa sudah tidak ada, backend memverifikasi cookie remember me.
- Jika valid, backend membuat session baru dan sekaligus merotasi token remember me.
Kesalahan umum adalah menganggap remember me hanya berarti menambah TTL session menjadi sangat panjang. Itu memperlemah kontrol session dan tidak memberi mekanisme rotasi atau deteksi pencurian token.
Desain token yang disarankan: selector-validator
Jangan simpan satu token mentah lalu lakukan pencocokan langsung. Pola yang lebih aman adalah memisahkan token menjadi dua bagian:
- Selector: identifier acak pendek/menengah untuk mencari record token dengan cepat.
- Validator: token rahasia acak yang hanya diketahui client dan server saat token dibuat.
Cookie menyimpan keduanya, misalnya:
remember_me = selector:validatorDi database, simpan:
- selector dalam bentuk teks biasa atau bentuk yang bisa diindeks
- hash dari validator, bukan validator mentah
Mengapa dipisah? Karena server perlu menemukan baris yang relevan tanpa harus memindai seluruh tabel dan membandingkan hash satu per satu. Selector dipakai sebagai lookup key, sementara validator diverifikasi dengan mencocokkan hash.
Mengapa token harus di-hash
Jika database bocor dan Anda menyimpan token mentah, penyerang bisa langsung memakai token itu untuk login sebagai pengguna. Jika yang bocor hanya hash validator, token tidak bisa langsung dipakai ulang tanpa mengetahui nilai mentahnya.
Gunakan fungsi hash kriptografis yang stabil untuk token acak, misalnya SHA-256. Karena validator dibuat dari entropy acak yang tinggi, kebutuhan utamanya adalah pencocokan aman dan penyimpanan non-plaintext. Tetap gunakan constant-time comparison saat membandingkan hash hasil komputasi dengan nilai yang tersimpan agar tidak membuka celah perbandingan yang sensitif terhadap waktu.
Skema tabel yang praktis
Skema berikut cukup umum untuk menyimpan token remember me:
Table: remember_tokens
- id bigint / uuid
- user_id foreign key ke users
- selector varchar, unique, indexed
- validator_hash varchar / binary
- expires_at timestamp
- created_at timestamp
- last_used_at timestamp, nullable
- revoked_at timestamp, nullable
- replaced_by_id bigint / uuid, nullable
- device_label varchar, nullable
- ua_hash varchar, nullable
- ip_prefix varchar, nullable
- last_seen_ip varchar, nullable
- created_from_ip varchar, nullable
- reuse_detected_at timestamp, nullablePenjelasan field penting:
- selector: dipakai untuk mencari token dengan cepat.
- validator_hash: hash dari validator mentah.
- expires_at: TTL absolut token.
- revoked_at: menandai token tidak lagi valid.
- replaced_by_id: membantu melacak rotasi token.
- ua_hash: hash ringan dari User-Agent untuk deteksi perubahan mencolok, bukan untuk identifikasi pasti.
- ip_prefix: binding ringan berdasarkan prefiks IP, misalnya subnet kasar. Jangan terlalu ketat karena IP pengguna bisa berubah.
- reuse_detected_at: indikator bahwa token lama dicoba dipakai lagi.
Catatan: Hindari device fingerprinting yang agresif. Untuk remember me, device binding ringan cukup berupa sinyal risiko, bukan syarat mutlak yang membuat pengguna sah sering terkunci.
Alur backend remember me yang aman
1. Saat login berhasil dan pengguna memilih remember me
- Autentikasi username/password seperti biasa.
- Buat session utama server-side.
- Generate selector dan validator acak dengan CSPRNG.
- Hash validator lalu simpan ke tabel bersama TTL, metadata device ringan, dan user_id.
- Kirim cookie remember me berisi
selector:validator.
2. Saat request datang tanpa session aktif
- Cek apakah cookie remember me ada.
- Parse selector dan validator. Jika format salah, tolak dan hapus cookie.
- Cari record berdasarkan selector.
- Pastikan token belum kedaluwarsa dan belum direvoke.
- Hitung hash validator dari cookie dan bandingkan dengan constant-time comparison.
- Jika cocok, buat session baru untuk user.
- Rotasi token: buat selector-validator baru, simpan record baru atau update terkontrol, revoke token lama, kirim cookie baru.
- Catat audit log sukses.
3. Jika token tidak cocok
Jika selector ditemukan tetapi hash validator tidak cocok, ada dua kemungkinan utama: cookie rusak atau token lama sedang dicoba ulang. Dalam sistem yang memakai rotasi, kondisi ini penting karena reuse token sering menandakan cookie dicuri.
Respons yang aman:
- Jangan buat session.
- Hapus cookie remember me dari browser.
- Tandai insiden di audit log.
- Pertimbangkan revoke seluruh token remember me milik user terkait, terutama jika selector valid tetapi validator salah pada token yang sebelumnya sudah diganti.
- Opsional: paksa re-login dengan password dan tampilkan notifikasi keamanan.
Rotasi token setiap pemakaian dan deteksi reuse
Mengapa harus rotasi setiap pemakaian
Tanpa rotasi, token remember me menjadi semacam password jangka panjang. Jika cookie dicuri sekali, penyerang bisa terus memakainya sampai TTL habis atau pengguna logout. Dengan rotasi, token lama langsung kehilangan nilai setelah sukses digunakan.
Pola rotasi yang aman
Setelah token valid dipakai:
- Buat token baru.
- Simpan token baru sebagai pengganti.
- Tandai token lama sebagai direvoke atau diganti oleh token baru.
- Kirim cookie baru ke client.
Jika kemudian token lama dipakai lagi, backend bisa membaca itu sebagai reuse. Ini jauh lebih kuat daripada sekadar mengatakan “token invalid”, karena backend tahu token lama seharusnya sudah tidak pernah muncul lagi pada client yang sah.
Skenario reuse token
Contoh:
- Pengguna sah memakai token A.
- Server memverifikasi A lalu merotasinya menjadi token B.
- Beberapa menit kemudian token A muncul lagi.
Token A semestinya sudah tidak ada pada client yang sah. Ini mengindikasikan salah satu hal berikut:
- cookie lama dicuri dan sedang dipakai penyerang,
- balapan request yang tidak ditangani dengan baik,
- sinkronisasi cookie buruk pada beberapa tab/perangkat,
- client mengulang request lama secara tidak normal.
Dalam praktiknya, perlakukan reuse sebagai peristiwa berisiko tinggi. Respons minimal:
- revoke seluruh token remember me user tersebut,
- akhiri session yang dibuat dari mekanisme remember me jika perlu,
- catat audit log dengan severity tinggi,
- minta login ulang dengan kredensial utama.
Mengurangi false positive karena race condition
Rotasi dapat bertabrakan dengan request paralel, misalnya dua request datang hampir bersamaan saat session sudah habis. Untuk mengurangi false positive:
- gunakan transaksi database saat validasi dan rotasi,
- kunci baris token yang sedang dipakai bila memungkinkan,
- izinkan jendela toleransi yang sangat sempit untuk request paralel dari token yang sama hanya jika benar-benar perlu,
- hindari endpoint yang memicu autentikasi otomatis remember me secara berlebihan.
Trade-off-nya: toleransi terlalu longgar akan melemahkan deteksi reuse. Biasanya lebih baik menjaga logika sederhana dan konsisten, lalu memonitor audit log untuk pola yang mencurigakan.
TTL, device binding ringan, dan revoke
TTL yang masuk akal
TTL remember me harus lebih panjang dari session biasa, tetapi tetap terbatas. Jangan memberi TTL sangat panjang tanpa alasan bisnis yang jelas. Semakin panjang TTL, semakin panjang pula jendela penyalahgunaan jika cookie dicuri.
Poin praktis:
- gunakan TTL absolut, bukan memperpanjang tanpa batas setiap request,
- opsional tambahkan idle timeout jika token lama tidak pernah dipakai,
- hapus token expired secara berkala dengan job terjadwal.
Device binding ringan
Binding ringan berarti Anda menyimpan sinyal tambahan untuk mengevaluasi risiko, misalnya:
- hash User-Agent,
- label device yang diberikan server atau pengguna,
- prefiks IP kasar, bukan IP penuh sebagai syarat mutlak.
Jangan menjadikan kecocokan IP atau User-Agent sebagai satu-satunya syarat validasi. Browser bisa berubah versi, jaringan bisa berpindah, dan IP mobile sangat dinamis. Gunakan data ini untuk:
- audit log,
- skor risiko,
- memicu verifikasi tambahan jika ada perubahan yang mencolok.
Revoke saat logout
Saat pengguna logout, bedakan dua aksi:
- Logout perangkat ini: revoke token remember me yang terkait dengan cookie saat ini dan hapus cookie.
- Logout semua perangkat: revoke seluruh token remember me milik user dan, bila perlu, putuskan session aktif lainnya.
Jangan hanya menghapus cookie di browser. Jika token di server tidak direvoke, cookie yang sempat dicuri tetap bisa dipakai pihak lain.
Cookie flags yang wajib
Cookie remember me adalah kredensial. Perlakukan seperti session cookie sensitif.
- HttpOnly: wajib agar tidak bisa dibaca JavaScript.
- Secure: wajib di koneksi HTTPS agar cookie tidak terkirim lewat HTTP biasa.
- SameSite: gunakan nilai yang sesuai kebutuhan aplikasi, umumnya Lax cukup untuk banyak aplikasi web. Jika ada alur lintas situs yang sah, evaluasi konsekuensinya dengan hati-hati.
- Path: batasi seperlunya.
- Domain: jangan terlalu luas jika tidak perlu.
- Max-Age atau Expires: sesuaikan dengan TTL server, jangan biarkan tidak sinkron.
Selain itu:
- jangan simpan data sensitif lain di cookie remember me,
- jangan menaruh user ID tanpa proteksi sebagai dasar autentikasi,
- pastikan semua endpoint autentikasi berjalan lewat HTTPS.
Pseudocode backend netral framework
Membuat token remember me saat login
function issueRememberMe(userId, request, response) {
selector = base64url(randomBytes(9))
validator = base64url(randomBytes(32))
validatorHash = sha256(validator)
token = {
user_id: userId,
selector: selector,
validator_hash: validatorHash,
expires_at: now() + REMEMBER_ME_TTL,
created_at: now(),
ua_hash: sha256(normalizeUserAgent(request.userAgent)),
ip_prefix: extractIpPrefix(request.ip),
created_from_ip: request.ip
}
db.insert('remember_tokens', token)
cookieValue = selector + ':' + validator
response.setCookie('remember_me', cookieValue, {
httpOnly: true,
secure: true,
sameSite: 'Lax',
path: '/',
maxAge: REMEMBER_ME_TTL
})
}Memverifikasi token dan merotasinya
function authenticateFromRememberMe(request, response) {
raw = request.cookies['remember_me']
if (!raw) return null
parts = raw.split(':')
if (parts.length != 2) {
clearRememberMeCookie(response)
audit('remember_me_invalid_format', { ip: request.ip })
return null
}
selector = parts[0]
validator = parts[1]
return db.transaction(() => {
token = db.findOneForUpdate('remember_tokens', {
selector: selector,
revoked_at: null
})
if (!token) {
clearRememberMeCookie(response)
audit('remember_me_selector_not_found', { selector: selector, ip: request.ip })
return null
}
if (token.expires_at < now()) {
db.update('remember_tokens', token.id, { revoked_at: now() })
clearRememberMeCookie(response)
audit('remember_me_expired', { user_id: token.user_id, token_id: token.id })
return null
}
incomingHash = sha256(validator)
if (!constantTimeEquals(incomingHash, token.validator_hash)) {
db.update('remember_tokens', token.id, { reuse_detected_at: now() })
db.updateWhere('remember_tokens', { user_id: token.user_id, revoked_at: null }, {
revoked_at: now()
})
clearRememberMeCookie(response)
audit('remember_me_reuse_detected', {
user_id: token.user_id,
token_id: token.id,
ip: request.ip,
ua_hash: sha256(normalizeUserAgent(request.userAgent))
})
return null
}
newSelector = base64url(randomBytes(9))
newValidator = base64url(randomBytes(32))
newValidatorHash = sha256(newValidator)
newTokenId = db.insert('remember_tokens', {
user_id: token.user_id,
selector: newSelector,
validator_hash: newValidatorHash,
expires_at: now() + REMEMBER_ME_TTL,
created_at: now(),
last_used_at: now(),
ua_hash: sha256(normalizeUserAgent(request.userAgent)),
ip_prefix: extractIpPrefix(request.ip),
created_from_ip: request.ip
})
db.update('remember_tokens', token.id, {
revoked_at: now(),
replaced_by_id: newTokenId,
last_used_at: now(),
last_seen_ip: request.ip
})
response.setCookie('remember_me', newSelector + ':' + newValidator, {
httpOnly: true,
secure: true,
sameSite: 'Lax',
path: '/',
maxAge: REMEMBER_ME_TTL
})
session = createSessionForUser(token.user_id)
audit('remember_me_login_success', {
user_id: token.user_id,
old_token_id: token.id,
new_token_id: newTokenId,
ip: request.ip
})
return session
})
}Logout dan revoke
function logoutCurrentDevice(request, response) {
raw = request.cookies['remember_me']
if (raw) {
parts = raw.split(':')
if (parts.length == 2) {
selector = parts[0]
db.updateWhere('remember_tokens', { selector: selector, revoked_at: null }, {
revoked_at: now()
})
}
}
destroyCurrentSession(request)
clearRememberMeCookie(response)
audit('logout_current_device', { user_id: request.user?.id, ip: request.ip })
}Pseudocode di atas menunjukkan prinsip utama, bukan kontrak API tertentu. Dalam implementasi nyata, tambahkan validasi input, penanganan error database, dan pengamanan transaksi sesuai stack yang dipakai.
Error handling, audit log, dan rate limit
Error handling
Untuk endpoint atau middleware remember me, targetnya adalah aman, senyap terhadap penyerang, dan tetap mudah dioperasikan.
- Jika cookie tidak valid atau format rusak, hapus cookie dan lanjutkan sebagai pengguna anonim.
- Jangan bocorkan apakah selector ada atau tidak ke client.
- Jika terjadi error internal, lebih aman gagal tertutup: jangan login otomatis.
- Hindari loop autentikasi berulang pada request yang sama.
Audit log yang perlu dicatat
Catat event berikut minimal:
- token dibuat saat login,
- remember me sukses membuat session baru,
- token expired,
- token direvoke saat logout,
- format cookie tidak valid,
- selector ditemukan tetapi validator salah,
- reuse token terdeteksi,
- revoke semua perangkat.
Data audit yang berguna:
- user_id jika diketahui,
- token_id atau selector yang dipseudonimkan seperlunya,
- timestamp,
- IP,
- User-Agent atau hash-nya,
- alasan revoke atau kegagalan.
Jangan menulis token mentah ke log. Ini kesalahan serius karena log sering diakses lebih luas daripada database autentikasi.
Rate limit endpoint terkait
Meskipun remember me berbasis token acak dan bukan password, tetap pasang rate limit pada endpoint yang memprosesnya, terutama jika token diverifikasi melalui endpoint khusus atau pada jalur bootstrap session.
Tujuannya:
- membatasi spam percobaan cookie palsu,
- menekan biaya komputasi hash dan query DB,
- mengurangi noise pada audit log,
- membantu mitigasi abuse terdistribusi.
Pola yang umum:
- rate limit per IP,
- opsional gabungan per IP dan selector,
- backoff bertahap untuk kegagalan berulang,
- alert jika ada lonjakan event invalid/reuse.
Kesalahan umum yang sering terjadi
- Menyimpan token mentah di database. Jika DB bocor, token langsung bisa dipakai.
- Tidak memisahkan selector dan validator. Lookup menjadi kurang efisien atau mendorong desain yang buruk.
- Tidak merotasi token. Token menjadi kredensial statis jangka panjang.
- TTL terlalu panjang. Jendela serangan membesar tanpa kontrol tambahan.
- Tidak revoke token saat logout. Hanya menghapus cookie client tidak cukup.
- Tidak memakai HttpOnly dan Secure. Cookie lebih mudah dicuri atau bocor.
- Menulis token ke log. Log berubah menjadi sumber kebocoran kredensial.
- Mengikat token terlalu ketat ke IP/User-Agent. Pengguna sah mudah terkunci, terutama di jaringan mobile.
- Tidak menangani race condition. Sistem salah menandai reuse atau membuat pengalaman login tidak stabil.
- Mencampur session biasa dengan remember me tanpa batas yang jelas. Debugging dan kontrol revoke menjadi sulit.
Checklist implementasi remember me aman
- Gunakan token acak dari CSPRNG, bukan nilai yang dapat ditebak.
- Pakai desain selector-validator.
- Simpan hash validator, bukan validator mentah.
- Verifikasi dengan constant-time comparison.
- Rotasi token pada setiap pemakaian yang sukses.
- Tandai token lama sebagai revoked/replaced.
- Deteksi reuse token dan respons sebagai insiden keamanan.
- Terapkan TTL absolut yang wajar dan bersihkan token expired.
- Gunakan cookie HttpOnly, Secure, dan SameSite yang tepat.
- Revoke token saat logout perangkat ini.
- Sediakan opsi revoke semua perangkat.
- Catat audit log tanpa menyimpan token mentah.
- Pasang rate limit pada jalur autentikasi terkait.
- Gunakan transaksi dan, jika perlu, locking untuk mencegah race condition.
- Perlakukan perubahan IP/User-Agent sebagai sinyal risiko, bukan kebenaran mutlak.
Penutup
Remember me aman membutuhkan desain yang berbeda dari session biasa. Intinya bukan membuat login bertahan lebih lama, melainkan membuat persistent login yang tetap aman jika cookie bocor, session habis, atau token dicoba ulang oleh pihak lain. Kombinasi selector-validator, hash token, rotasi per penggunaan, revoke yang benar, dan deteksi reuse memberi fondasi yang kuat tanpa bergantung pada framework tertentu.
Jika Anda sedang membangun middleware atau service autentikasi sendiri, mulailah dari alur data dan model tabelnya terlebih dahulu. Setelah itu, pastikan observabilitasnya cukup: audit log, metrik kegagalan, dan alarm reuse token sering kali sama pentingnya dengan kode validasinya.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!