Session-based authentication dari chapter sebelumnya punya kelemahan pengalaman: cookie PHPSESSID mati saat browser ditutup, dan data session di server dibersihkan setelah idle 24 menit. Pengguna yang tiap pagi harus login ulang akan segera kabur. Remember-me menambalnya dengan cookie berumur panjang — tetapi menghubungkan cookie tersebut ke alur autentikasi menuntut kehati-hatian tersendiri: token harus diverifikasi tanpa memercayakan isinya, session baru harus dibuat dengan benar, dan seluruh token harus bisa dicabut saat password berubah. Desain token selector:validator beserta alasan kriptografisnya sudah kita bangun tuntas pada chapter Remember-Me Patterns
; chapter ini fokus pada sisi integrasinya ke dalam authentication.
Token Remember-Me dalam Alur Autentikasi #
Mari kita posisikan dulu di mana remember-me bekerja. Saat login dengan kotak “ingat saya” dicentang, aplikasi menerima kredensial, membuat session seperti biasa, lalu menambahkan satu langkah: menerbitkan token dan mengirimnya sebagai cookie berumur 30 hari. Pada kunjungan-kunjungan berikutnya, urutannya berbeda — browser datang tanpa session aktif, dan middleware autentikasi mencoba jalur kedua:
- Baca cookie
remember_medari request. - Cari baris token berdasarkan
selectordi database. - Tolak bila token tidak ada, sudah kedaluwarsa, atau
validatortidak cocok dengan hash tersimpan. - Bila lolos: buat session baru (lengkap dengan regenerasi ID), lalu teruskan request seperti pengguna yang baru saja login.
Perbedaan kuncinya dengan password: token diverifikasi setiap request tanpa melibatkan password_verify() — perbandingannya memakai hash_equals() terhadap hash SHA-256, karena token bukan buatan manusia melainkan keacakan murni. Karena token hidup di tabel database, aplikasi bisa mencabutnya kapan pun — kekuatan yang tidak dimiliki session cookie biasa.
Auto-Login dari Cookie #
Implementasi berikut menyatukan seluruh jalur di atas. Tabel remember_tokens menyimpan hash validator, token demo memakai nilai tetap agar contoh deterministik, dan proses pembuatan session memakai pola yang sama dengan login biasa:
<?php
$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec('CREATE TABLE users (
id INTEGER PRIMARY KEY,
username TEXT NOT NULL
)');
$pdo->exec('CREATE TABLE remember_tokens (
selector TEXT PRIMARY KEY,
user_id INTEGER NOT NULL,
validator_hash TEXT NOT NULL,
expires_at TEXT NOT NULL
)');
$pdo->prepare('INSERT INTO users (id, username) VALUES (?, ?)')->execute([42, 'budi']);
// Token yang "diterbitkan saat login 30 hari lalu" - nilai tetap untuk pengujian
$selector = 'selector-tetap-demo-01';
$validator = 'validator-tetap-demo-untuk-pengujian';
$pdo->prepare('INSERT INTO remember_tokens (selector, user_id, validator_hash, expires_at)
VALUES (?, ?, ?, ?)')
->execute([$selector, 42, hash('sha256', $validator), '2026-02-14 08:00:00']);
function attemptAutoLogin(PDO $pdo, string $cookieValue, string $now): ?int
{
[$tokenSelector, $tokenValidator] = array_pad(explode(':', $cookieValue, 2), 2, '');
$stmt = $pdo->prepare('SELECT user_id, validator_hash, expires_at FROM remember_tokens WHERE selector = ?');
$stmt->execute([$tokenSelector]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row === false || $now > $row['expires_at']) {
return null; // selector tidak dikenal, atau token kedaluwarsa
}
if (!hash_equals($row['validator_hash'], hash('sha256', $tokenValidator))) {
return null; // validator tidak cocok
}
return (int) $row['user_id'];
}
$dir = sys_get_temp_dir() . '/php-kursus-sesi';
if (!is_dir($dir)) {
mkdir($dir);
}
session_save_path($dir);
session_id('auth-remember-demo');
@unlink($dir . '/sess_auth-remember-demo');
$log = [];
// "Request": pengguna datang tanpa session aktif (cookie PHPSESSID sudah mati)
session_start();
$log[] = 'Sebelum auto-login: user_id di session = ' . ($_SESSION['user_id'] ?? 'tidak ada');
$cookieDariBrowser = $selector . ':' . $validator;
$userId = attemptAutoLogin($pdo, $cookieDariBrowser, '2026-01-20 08:00:00');
if ($userId !== null) {
session_regenerate_id(true); // auto-login adalah login: ID session baru
$_SESSION['user_id'] = $userId;
$log[] = 'Auto-login sukses: user_id ' . $userId . ' kini ada di session';
} else {
$log[] = 'Auto-login ditolak: pengguna tetap tamu';
}
session_write_close();
// Cookie yang dipalsukan (validator diganti) harus ditolak
$cookiePalsu = $selector . ':validator-yang-dipalsukan';
$log[] = 'Validator palsu diterima: ' . var_export(attemptAutoLogin($pdo, $cookiePalsu, '2026-01-20 08:00:00') !== null, true);
// Kunjungan setelah tanggal kedaluwarsa harus ditolak
$log[] = 'Kunjungan setelah kedaluwarsa diterima: ' . var_export(attemptAutoLogin($pdo, $cookieDariBrowser, '2026-03-01 08:00:00') !== null, true);
echo implode("\n", $log), "\n";Output yang Dihasilkan:
Sebelum auto-login: user_id di session = tidak ada
Auto-login sukses: user_id 42 kini ada di session
Validator palsu diterima: false
Kunjungan setelah kedaluwarsa diterima: falseMari kita bedah bagian-bagian pentingnya:
- Pemecahan cookie (baris pertama dalam fungsi):
explode(':', $cookieValue, 2)memisahkan selector dan validator;array_pad(..., 2, '')menjamin kedua variabel selalu terisi, sehingga cookie tanpa titik dua pun tidak memicu error. - Penolakan bertahap: selector tak dikenal dan token kedaluwarsa ditangani satu kondisi; validator palsu ditolak
hash_equals(). Output membuktikan ketiganya: hanya cookie utuh dan masih segar yang diterima. - Pembuatan session:
session_regenerate_id(true)dipanggil sebelum menulis$_SESSION['user_id']— auto-login setara login baru, sehingga perlindungan session fixation dari chapter Session-Based Authentication berlaku juga di sini. - Urutan pemeriksaan: verifikasi token dilakukan setelah pemeriksaan “sudah login?” gagal — pengguna dengan session aktif tidak perlu menyentuh tabel token sama sekali.
Masa Berlaku Token vs Masa Berlaku Session #
Remember-me memelihara dua jam yang berbeda, dan keduanya tidak boleh tertukar. Cookie session hidup sesuai siklus browser dan gc_maxlifetime; token remember-me hidup menurut kolom expires_at di database plus atribut expires pada cookienya. Perbandingan lengkapnya:
| Aspek | Cookie session (PHPSESSID) | Cookie remember-me |
|---|---|---|
| Isi | ID session acak | pasangan selector:validator |
| Umur cookie | session cookie — hilang saat browser ditutup | biasanya 30 hari lewat atribut expires |
| Umur data | session.gc_maxlifetime (bawaan 1440 detik idle) | kolom expires_at di database |
| Cara dicabut | tidak praktis — tidak ada daftar session per user | hapus baris token di database |
| Verifikasi tiap request | keberadaan data session di penyimpanan | lookup selector + hash_equals() |
Konsekuensi praktisnya: angka 30 hari pada cookie harus sama dengan angka di expires_at. Bila cookie berumur lebih pendek dari baris database, pengguna “kehilangan” ingatan lebih cepat dari seharusnya; bila lebih panjang, browser terus mengirim token yang server tolak — percuma tapi berisiko bising. Perbandingan expires_at pada contoh memakai string waktu berformat ISO yang urutan leksikografisnya sama dengan urutan kronologis.
Invalidasi Semua Token saat Password Berubah #
Password yang diganti harus mematikan semua pintu masuk lama — termasuk token remember-me di semua perangkat. Kalau tidak, siapa pun yang sempat menyalin cookie tetap bisa masuk selama 30 hari ke depan meskipun password sudah bukan rahasianya lagi. Fungsi changePassword() berikut menunjukkan urutan kerjanya:
<?php
$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec('CREATE TABLE users (
id INTEGER PRIMARY KEY,
username TEXT NOT NULL,
password_hash TEXT NOT NULL
)');
$pdo->exec('CREATE TABLE remember_tokens (
selector TEXT PRIMARY KEY,
user_id INTEGER NOT NULL
)');
$pdo->prepare('INSERT INTO users (id, username, password_hash) VALUES (?, ?, ?)')
->execute([42, 'budi', password_hash('password-lama-123', PASSWORD_DEFAULT)]);
// Tiga token di tiga "perangkat" milik Budi, satu token milik user lain
$stmt = $pdo->prepare('INSERT INTO remember_tokens (selector, user_id) VALUES (?, 42)');
foreach (['token-laptop', 'token-ponsel', 'token-tablet'] as $selector) {
$stmt->execute([$selector]);
}
$pdo->prepare('INSERT INTO remember_tokens (selector, user_id) VALUES (?, ?)')
->execute(['token-siti', 7]);
function changePassword(PDO $pdo, int $userId, string $passwordLama, string $passwordBaru): array
{
$stmt = $pdo->prepare('SELECT password_hash FROM users WHERE id = ?');
$stmt->execute([$userId]);
$hashLama = $stmt->fetchColumn();
if (!password_verify($passwordLama, $hashLama)) {
return ['sukses' => false, 'pesan' => 'Password lama salah.'];
}
// Hash password baru, lalu cabut SELURUH token remember-me user ini
$hashBaru = password_hash($passwordBaru, PASSWORD_DEFAULT);
$pdo->prepare('UPDATE users SET password_hash = ? WHERE id = ?')->execute([$hashBaru, $userId]);
$pdo->prepare('DELETE FROM remember_tokens WHERE user_id = ?')->execute([$userId]);
return ['sukses' => true];
}
echo 'Token Budi sebelum ganti password: '
. $pdo->query('SELECT COUNT(*) FROM remember_tokens WHERE user_id = 42')->fetchColumn() . "\n";
$hasil = changePassword($pdo, 42, 'password-lama-123', 'password-baru-456');
echo 'Ganti password: ' . var_export($hasil['sukses'], true) . "\n";
$stmt = $pdo->prepare('SELECT password_hash FROM users WHERE id = ?');
$stmt->execute([42]);
echo 'Password baru terverifikasi: '
. var_export(password_verify('password-baru-456', $stmt->fetchColumn()), true) . "\n";
echo 'Token Budi tersisa: '
. $pdo->query('SELECT COUNT(*) FROM remember_tokens WHERE user_id = 42')->fetchColumn() . "\n";
echo 'Token user lain tetap utuh: '
. $pdo->query('SELECT COUNT(*) FROM remember_tokens WHERE user_id = 7')->fetchColumn() . "\n";Output yang Dihasilkan:
Token Budi sebelum ganti password: 3
Ganti password: true
Password baru terverifikasi: true
Token Budi tersisa: 0
Token user lain tetap utuh: 1Verifikasi password lama di awal mencegah orang yang membuka laptop yang belum terkunci mengganti password tanpa tahu kredensialnya. DELETE ... WHERE user_id = ? menunjukkan pencabutan yang presisi: tiga token milik Budi lenyap, token milik pengguna lain tidak tersentuh. Password baru diverifikasi dengan password_verify() — hash mentahnya tidak pernah dicetak karena berbeda tiap eksekusi.
Session yang sedang aktif di perangkat lain juga perlu dimatikan. Kombinasi lengkapnya adalah dua langkah dalam satu transaksi: DELETE seluruh token remember-me (seperti di atas) plus UPDATE sessions_invalid_before untuk mematikan session di mana pun, persis pola logout-semua-perangkat dari chapter Login dan Logout
. Perangkat tempat pengguna mengganti password sendiri biasanya dikecualikan — aplikasi menimpa session lama dengan login yang baru.
Batasan Keamanan Remember-Me #
Setiap kenyamanan dibayar dengan permukaan serangan, dan remember-me memperpanjang umur pintu masuk hingga berhari-hari. Batasan berikut wajib ditegakkan bersamaan:
- Cookie hanya dikirim lewat HTTPS (
secure), tidak terbaca JavaScript (httponly), dansamesitedipasang untuk meredam CSRF — kombinasi flag yang dibahas pada chapter Cookie Options . - Database menyimpan hash validator, bukan validator mentah; kebocoran database tidak langsung menjadi tiket masuk.
- Token dirotasi setiap kali dipakai auto-login, sehingga pemakaian token lama terdeteksi sebagai indikasi pencurian — pola token series dari chapter Remember-Me Patterns .
- Umur token dibatasi wajar (7-30 hari) dan fitur ini bersifat opt-in; perangkat umum atau komputer kantor sebaiknya tidak mencentangnya.
- Logout mencabut baris token di samping membersihkan session — meninggalkan token hidup berarti pengguna login lagi tepat setelah logout.
Ringkasan #
Remember-me mengintegrasikan token jangka panjang ke alur autentikasi: cookie selector:validator diverifikasi lewat lookup database dan hash_equals(), lalu auto-login membuat session segar dengan session_regenerate_id(true). Masa berlaku token dijaga di dua tempat — atribut expires cookie dan kolom expires_at — dan pergantian password mencabut seluruh token milik user sambil mematikan session lainnya. Satu skenario masih menyisakan masalah: pengguna yang lupa password sama sekali tidak bisa masuk untuk menggantinya. Chapter Password Reset
menutup celah itu dengan token sekali pakai yang dikirim lewat email.