SyntaxStudy
Sign Up
PHP Advanced 11 min read

"Remember Me" Pattern

A "Remember Me" feature keeps users logged in across browser restarts by storing a long-lived authentication token in a cookie. Implementing it securely requires more than just extending the session lifetime.

  1. Generate a cryptographically random token and store its hash in the database.
  2. Store the raw token in a long-lived cookie.
  3. On each request, look up the hashed token, validate it, and re-authenticate the user.
  4. Rotate the token on each use (rolling tokens) to detect theft.
Example
<?php
// Pseudo-code — assumes $db is a PDO instance

function setRememberMe(int $userId, PDO $db): void
{
    $token    = bin2hex(random_bytes(32)); // 64-char hex token
    $hash     = hash('sha256', $token);    // store hash, not raw token
    $selector = bin2hex(random_bytes(6));  // short identifier to look up row

    // Store selector + hash in DB with expiry (30 days)
    $db->prepare('INSERT INTO remember_tokens (selector, hash, user_id, expires_at)
                  VALUES (?, ?, ?, ?)')
       ->execute([$selector, $hash, $userId, date('Y-m-d H:i:s', time() + 86400 * 30)]);

    // Send selector:token in cookie
    setcookie('remember', $selector . ':' . $token, [
        'expires'  => time() + 86400 * 30,
        'path'     => '/',
        'httponly' => true,
        'secure'   => true,
        'samesite' => 'Lax',
    ]);
}

function loginFromRememberMe(PDO $db): ?int
{
    if (empty($_COOKIE['remember'])) return null;

    [$selector, $token] = explode(':', $_COOKIE['remember'], 2) + ['', ''];

    $row = $db->prepare('SELECT * FROM remember_tokens WHERE selector = ? AND expires_at > NOW()')
              ->execute([$selector]) ? null : null; // simplified

    if (!$row || !hash_equals($row['hash'], hash('sha256', $token))) {
        return null; // invalid or stolen
    }

    // Rotate token
    setRememberMe($row['user_id'], $db);
    $db->prepare('DELETE FROM remember_tokens WHERE selector = ?')->execute([$selector]);

    return (int) $row['user_id'];
}
Pro Tip

Tip: Use a selector:token split. The selector (stored plain) identifies the database row without a full-table scan. The token (stored as a hash) is the secret. This limits damage if the database is breached — attackers get hashes, not valid tokens.