:". O selector (32 hex) indexa a linha; só o // SHA-256 do validator (64 hex) fica na BD, por isso um dump da tabela não dá // para forjar um cookie. Cada utilização gasta o validator e emite outro — // um cookie roubado só vale até a próxima visita do dono; quando o dono volta // com um validator que já não bate certo, é a assinatura de uma cópia em // circulação, e todos os tokens da conta são revogados. const REMEMBER_COOKIE = 'fit_remember'; const REMEMBER_DAYS = 30; function remember_set_cookie(?string $value, int $expires = 0): void { if (headers_sent()) return; setcookie(REMEMBER_COOKIE, (string) $value, [ 'expires' => $expires, 'path' => '/', 'secure' => request_is_https(), 'httponly' => true, // Lax, não Strict: um link de fora (email) não traz um cookie Strict, // e a pessoa via o login outra vez apesar de ter pedido para ficar. 'samesite' => 'Lax', ]); } function remember_forget_cookie(): void { remember_set_cookie('', time() - 3600); } function remember_parse_cookie(): ?array { $raw = (string) ($_COOKIE[REMEMBER_COOKIE] ?? ''); if ($raw === '' || substr_count($raw, ':') !== 1) return null; [$selector, $validator] = explode(':', $raw, 2); if (strlen($selector) !== 32 || !ctype_xdigit($selector)) return null; if (strlen($validator) !== 64 || !ctype_xdigit($validator)) return null; return ['selector' => $selector, 'validator' => $validator]; } // Chamar depois de um login bem sucedido, só quando a pessoa pediu para ficar. function remember_issue(PDO $pdo, int $userId): void { if ($userId <= 0) return; $selector = bin2hex(random_bytes(16)); $validator = bin2hex(random_bytes(32)); $expires = time() + REMEMBER_DAYS * 86400; try { $pdo->prepare( "INSERT INTO remember_tokens (user_id, selector, validator_hash, expires_at, last_ip, user_agent) VALUES (?, ?, ?, ?, ?, ?)" )->execute([ $userId, $selector, hash('sha256', $validator), date('Y-m-d H:i:s', $expires), client_ip(), mb_substr((string) ($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 255), ]); } catch (Throwable $e) { // Um login válido nunca deve falhar por causa disto. error_log('[remember] issue failed: ' . $e->getMessage()); return; } remember_set_cookie($selector . ':' . $validator, $expires); if (random_int(0, 19) === 0) remember_prune($pdo); } // Repõe a sessão a partir do cookie, se houver um válido. Não redirecciona. function remember_resume(PDO $pdo): void { if (!empty($_SESSION['user_id'])) return; $cookie = remember_parse_cookie(); if ($cookie === null) return; try { $st = $pdo->prepare("SELECT * FROM remember_tokens WHERE selector = ? LIMIT 1"); $st->execute([$cookie['selector']]); $row = $st->fetch(PDO::FETCH_ASSOC) ?: null; } catch (Throwable $e) { error_log('[remember] lookup failed: ' . $e->getMessage()); return; } if ($row === null) { remember_forget_cookie(); return; } if (!hash_equals((string) $row['validator_hash'], hash('sha256', $cookie['validator']))) { error_log('[remember] validator mismatch for user ' . (int) $row['user_id'] . ' — a revogar todos os tokens da conta'); remember_forget_user($pdo, (int) $row['user_id']); remember_forget_cookie(); return; } if (strtotime((string) $row['expires_at']) <= time()) { remember_delete($pdo, (string) $row['selector']); remember_forget_cookie(); return; } $st = $pdo->prepare("SELECT * FROM users WHERE id = ? AND is_active = 1 LIMIT 1"); $st->execute([(int) $row['user_id']]); $user = $st->fetch(PDO::FETCH_ASSOC) ?: null; if ($user === null) { remember_forget_user($pdo, (int) $row['user_id']); remember_forget_cookie(); return; } auth_set_session($user); $_SESSION['_remembered'] = true; remember_rotate($pdo, (string) $row['selector']); } function remember_rotate(PDO $pdo, string $selector): void { $validator = bin2hex(random_bytes(32)); $expires = time() + REMEMBER_DAYS * 86400; try { $pdo->prepare("UPDATE remember_tokens SET validator_hash = ?, expires_at = ?, last_ip = ? WHERE selector = ?") ->execute([hash('sha256', $validator), date('Y-m-d H:i:s', $expires), client_ip(), $selector]); } catch (Throwable $e) { error_log('[remember] rotate failed: ' . $e->getMessage()); return; } remember_set_cookie($selector . ':' . $validator, $expires); } function remember_delete(PDO $pdo, string $selector): void { try { $pdo->prepare("DELETE FROM remember_tokens WHERE selector = ?")->execute([$selector]); } catch (Throwable $e) { error_log('[remember] delete failed: ' . $e->getMessage()); } } // Chamar no logout e ao mudar a palavra-passe — corta qualquer cookie roubado. function remember_forget_user(PDO $pdo, int $userId): void { try { $pdo->prepare("DELETE FROM remember_tokens WHERE user_id = ?")->execute([$userId]); } catch (Throwable $e) { error_log('[remember] forget_user failed: ' . $e->getMessage()); } } function remember_prune(PDO $pdo): void { try { $pdo->exec("DELETE FROM remember_tokens WHERE expires_at < NOW()"); } catch (Throwable $e) { /* accessório */ } }