Device & session management adalah halaman "Perangkat" di pengaturan akun: daftar semua tempat akunmu sedang login, lengkap dengan perangkat, lokasi perkiraan, dan kapan terakhir dipakai, plus tombol untuk mengeluarkan salah satunya. Bayangin kartu anggota gym yang dicek ke sistem di setiap pintu putar. Kalau kartumu hilang, resepsionis memblokirnya di sistem, dan tap berikutnya langsung ditolak. Bandingkan dengan tiket kertas yang tercetak "berlaku sampai 17.00": tiket itu tetap sah sampai jam itu, siapa pun yang memegangnya.
🧪 Tebakan cepat: pukul 07.20 Siti mengganti password di laptop dan menekan "Keluarkan perangkat lain". HP-nya baru saja dapat access token JWT pukul 07.12, berumur 15 menit, dan server gak menyimpan daftar token yang dicabut. Sampai jam berapa HP itu masih bisa membuka rapor? Tebak dulu, jawabannya ada di bagian "Kenapa pencabutannya langsung berlaku".
Satu baris per perangkat
Siti login di tiga tempat (seperti di lesson Auth Best Practices). Server Portal Sekolah menyimpan satu baris per login:
| id | Perangkat | IP | Wilayah | Terakhir aktif |
|---|---|---|---|---|
sess_1_0 |
Laptop: Chrome 141, Windows 11 | 36.68.10.21 | Bandung | 07.20, perangkat ini |
sess_1_1 |
HP: Chrome 141, Android 15 | 182.253.5.9 | Bandung | 07.12 |
sess_1_2 |
Komputer lab: Firefox 143, Ubuntu | 36.68.200.4 | Bandung | kemarin 13.40 |
Kolom tiap baris:
- id sesi atau token. Yang disimpan di database adalah hash-nya (SHA-256), bukan token aslinya, jadi kalau database bocor, token-nya gak ikut bocor.
- user_id, supaya halaman Perangkat cuma menampilkan milik user itu.
- perangkat, dibaca dari header
User-Agent, dan IP. Wilayahnya dari geolokasi IP, perkiraan kota, bukan alamat. - last_active_at, diperbarui saat ada request (cukup sekali per menit, supaya database gak ditulis tiap request).
- revoked_at:
nullberarti masih aktif. Diisi waktu pencabutan saat dikeluarkan.
Di Laravel dengan session driver database, tabel sessions sudah punya kolom id, user_id, ip_address, user_agent dan last_activity. Untuk token API, tabel personal_access_tokens milik Sanctum menyimpan hash token dan last_used_at.
Kode: buat, daftar, cabut
function createSessionManager() {
const sessions = []; // di server sungguhan: tabel sessions
return {
create(userId, deviceName, ip) {
const session = { id: "sess_" + userId + "_" + sessions.length, userId, deviceName, ip, lastActive: Date.now(), revoked: false };
sessions.push(session);
return session;
},
getSessions(userId) {
return sessions.filter((s) => s.userId === userId && s.revoked === false);
},
revoke(sessionId) {
const session = sessions.find((s) => s.id === sessionId);
if (!session) return false;
session.revoked = true;
return true;
},
revokeAllExcept(userId, keepSessionId) {
sessions.filter((s) => s.userId === userId && s.id !== keepSessionId).forEach((s) => { s.revoked = true; });
},
};
}
const sm = createSessionManager();
sm.create(1, "Chrome 141, Windows 11", "36.68.10.21"); // sess_1_0, laptop
sm.create(1, "Chrome 141, Android 15", "182.253.5.9"); // sess_1_1, HP
sm.create(1, "Firefox 143, Ubuntu", "36.68.200.4"); // sess_1_2, komputer lab
sm.create(2, "Safari 26, iOS 26", "114.124.7.88"); // sess_2_3, Pak Andi
console.log(sm.getSessions(1).length); // 3
console.log(sm.revoke("sess_1_2")); // true, komputer lab dikeluarkan
console.log(sm.getSessions(1).length); // 2
sm.revokeAllExcept(1, "sess_1_0"); // setelah ganti password
console.log(sm.getSessions(1).map((s) => s.deviceName)); // [ 'Chrome 141, Windows 11' ]
console.log(sm.getSessions(2).length); // 1, sesi Pak Andi gak tersentuh
iddirangkai dari"sess_", userId, dan urutan sesi di store. Di server sungguhan, id-nya acak 128 bit seperti di lesson Session-based Authentication; urutan dipakai di sini supaya hasilnya bisa ditebak.getSessionsmenyaring dua syarat: milik user itu dan belum dicabut.revokegak menghapus baris, cuma menandairevoked. Barisnya tetap ada untuk riwayat, danrevokemengembalikanfalsekalau id-nya gak ditemukan.revokeAllExceptjuga memeriksas.userId === userId. Tanpa syarat itu, sesi Pak Andi ikut tercabut.
Kenapa pencabutannya langsung berlaku
Dengan sesi di server, setiap request membawa session ID, dan server mencarinya di store, seperti di lesson Session. Begitu sess_1_2 ditandai revoked, request berikutnya dari komputer lab dijawab 401 {"message": "Unauthenticated."}, detik itu juga.
JWT yang stateless lain cerita (lesson JSON Web Token (JWT)). Server cuma mengecek tanda tangan dan exp, tanpa mencari apa pun. Token HP dibuat pukul 07.12 dengan umur 15 menit, jadi exp-nya 07.27. Mencabut sesi pukul 07.20 menghentikan refresh token-nya, tapi access token yang sudah ada tetap sah. HP Siti masih bisa membuka rapor sampai 07.27. Itu jawaban tebakannya.
Cara menutup celah itu: buat access token sangat pendek dan cabut refresh token-nya (lesson Token Refresh Flow), atau simpan daftar jti yang dicabut di Redis dan periksa di setiap request. Cara kedua artinya server mencari sesuatu lagi di setiap request, persis seperti sesi.
Ganti password: keluarkan perangkat lain
Setelah password diganti, semua sesi lain harus berakhir, kecuali sesi yang sedang dipakai. Kalau password lama sempat bocor, perangkat yang login dengan password itu ikut keluar. Di Laravel:
// Route-nya memakai middleware 'auth.session'
Auth::logoutOtherDevices($request->input('password'));
// Atau langsung, dengan session driver database:
DB::table('sessions')
->where('user_id', $request->user()->id)
->where('id', '!=', $request->session()->getId())
->delete();
Lalu tulis satu baris token_revoke di audit log (lesson Audit Logs & Auth Trail), dan kirim email ke Siti: "Password-mu diganti, dua perangkat lain sudah dikeluarkan."
💡 Login yang mencurigakan
Halaman Perangkat juga tempat yang pas untuk menandai login aneh. Dua pola yang paling berguna:
- Impossible travel: login dari Jakarta pukul 10.00, lalu dari New York pukul 10.05. Jaraknya sekitar 16.000 km. Dalam 5 menit itu butuh kecepatan 16.000 ÷ (5 ÷ 60) = 192.000 km/jam, padahal pesawat sekitar 900 km/jam. Salah satu login itu bukan orang yang sama.
- Skor risiko: tiap sinyal diberi bobot, lalu dijumlah. Misalnya impossible travel +50, browser dan OS yang belum pernah dipakai +20, IP VPN yang dikenal +15, jam yang gak biasa +10, IP di daftar ancaman +30. Ambangnya menentukan tindakan: 70 ke atas minta MFA lagi, 40 ke atas izinkan tapi kirim email, di bawahnya izinkan. Bobot dan ambang bisa disetel dari data log, tanpa menulis ulang aturannya. Mulai dari aturan sederhana seperti ini dulu, bukan model machine learning.
⚠️ Jebakan yang sering ketemu
- Menghapus dari daftar, tapi gak dari store. Kartunya hilang dari halaman, tapi session ID-nya masih diterima server. Cabut di tempat server mengecek.
revokeAllExcepttanpa cek user. Semua sesi selain milik Siti yang dipertahankan ikut tercabut, termasuk milik user lain.- Ikut mencabut sesi yang sedang dipakai. Siti langsung terlempar keluar setelah ganti password. Kecualikan id sesinya sendiri.
- Menyimpan token apa adanya. Simpan hash-nya.
- Menjanjikan "keluar seketika" dengan JWT stateless. Token yang sudah keluar tetap sah sampai
exp.
TL;DR: Halaman Perangkat menampilkan satu baris per login: id (simpan hash-nya), user_id, perangkat dari User-Agent, IP dan wilayah perkiraan, last_active_at, dan revoked_at. getSessions menyaring milik user yang belum dicabut, revoke menandai satu sesi, revokeAllExcept mencabut semua milik user itu kecuali sesi yang sedang dipakai, dan dipanggil setelah ganti password. Sesi di server dicabut seketika, karena server mencarinya di setiap request. JWT stateless tetap sah sampai exp, kecuali server menyimpan daftar token yang dicabut. Untuk login aneh: impossible travel dan skor risiko dengan ambang (MFA, email, izinkan).