Sessions end when the browser closes. A remember-me feature keeps the user signed in for days using a long-lived cookie with a random token. Do not put the user id or password in the cookie.
Pattern: selector and validator
- Selector: random public id used to look up the row.
- Validator: random secret; only its hash is stored. This prevents stolen database dumps from being used to log in.
SQL
CREATE TABLE auth_tokens (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
selector CHAR(24) NOT NULL UNIQUE,
validator_hash CHAR(64) NOT NULL,
expires_at DATETIME NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id)
);On login, when the checkbox remember is ticked:
PHP
if (!empty($_POST['remember'])) {
$selector = bin2hex(random_bytes(12)); // 24 hex characters
$validator = bin2hex(random_bytes(32));
$expires = time() + 60 * 60 * 24 * 30; // 30 days
$pdo->prepare('INSERT INTO auth_tokens (user_id, selector, validator_hash, expires_at) VALUES (?, ?, ?, FROM_UNIXTIME(?))')
->execute([$user['id'], $selector, hash('sha256', $validator), $expires]);
setcookie('remember', $selector . ':' . $validator, [
'expires' => $expires, 'path' => '/',
'secure' => true, 'httponly' => true, 'samesite' => 'Lax'
]);
}At the top of every request (in auth.php), if there is no session but there is a cookie:
PHP
function auto_login(PDO $pdo): void {
if (isset($_SESSION['user_id']) || empty($_COOKIE['remember'])) return;
$parts = explode(':', $_COOKIE['remember']);
if (count($parts) !== 2) return;
[$selector, $validator] = $parts;
$stmt = $pdo->prepare('SELECT t.id, t.user_id, t.validator_hash, u.role
FROM auth_tokens t JOIN users u ON u.id = t.user_id
WHERE t.selector = ? AND t.expires_at > NOW()');
$stmt->execute([$selector]);
$row = $stmt->fetch();
if ($row && hash_equals($row['validator_hash'], hash('sha256', $validator))) {
session_regenerate_id(true);
$_SESSION['user_id'] = $row['user_id'];
$_SESSION['role'] = $row['role'];
// Optional: rotate the token here for extra safety
} else {
setcookie('remember', '', time() - 3600, '/'); // bad cookie: remove it
}
}On logout: delete the matching auth_tokens row, clear the cookie, and destroy the session. For sensitive actions (change password, payment), ask for the password again even if signed in by cookie.