Unit 13 ยท E-commerce and Web-Related Components

13.6Stay Signed-In ("Remember me")

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

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.