Replace auth_user+auth_pass_b64 cookies with secure opaque remember token

Co-authored-by: naielv <109038805+naielv@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-07 19:40:33 +00:00
parent 868b8477e0
commit 6f0ada0713
5 changed files with 82 additions and 35 deletions

View File

@@ -773,26 +773,53 @@ function init_active_centro(?array $auth_data = null): void
// ── User session helpers (Dispositivos conectados) ────────────────────────────
/**
* Register or refresh a session record in user_sessions.
* Register a new session record in user_sessions.
* $remember_token_hash is the SHA-256 hash of the raw remember-me cookie value.
* Stores a SHA-256 hash of the PHP session_id so the raw token never reaches the DB.
*/
function db_register_session(string $username): void
function db_register_session(string $username, string $remember_token_hash = ''): void
{
$token = hash('sha256', session_id());
$ip = $_SERVER['HTTP_X_FORWARDED_FOR']
?? $_SERVER['REMOTE_ADDR']
?? '';
// Only keep the first IP if X-Forwarded-For contains a list
$ip = trim(explode(',', $ip)[0]);
$ua = substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 512);
db()->prepare(
"INSERT INTO user_sessions (session_token, username, ip_address, user_agent)
VALUES (?, ?, ?, ?)
"INSERT INTO user_sessions (session_token, username, ip_address, user_agent, remember_token_hash)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(session_token) DO UPDATE SET
last_active = datetime('now'),
username = excluded.username"
)->execute([$token, strtolower($username), $ip, $ua]);
last_active = datetime('now'),
remember_token_hash = COALESCE(excluded.remember_token_hash, remember_token_hash)"
)->execute([$token, strtolower($username), $ip, $ua, $remember_token_hash ?: null]);
}
/**
* Restore a session from a remember-me token hash.
* Updates the session_token to the current PHP session_id so revocation
* continues to work after the PHP session was re-created.
* Returns the session row (including username) on success, or null if not found.
*/
function db_restore_session_by_remember_token(string $token_hash): ?array
{
$pdo = db();
$stmt = $pdo->prepare(
'SELECT * FROM user_sessions WHERE remember_token_hash = ? LIMIT 1'
);
$stmt->execute([$token_hash]);
$row = $stmt->fetch();
if (!$row) {
return null;
}
// Migrate the row to the new PHP session_id
$new_session_token = hash('sha256', session_id());
$pdo->prepare(
"UPDATE user_sessions
SET session_token = ?, last_active = datetime('now')
WHERE remember_token_hash = ?"
)->execute([$new_session_token, $token_hash]);
return $row;
}
/** Update last_active for the current session. */

View File

@@ -5,8 +5,7 @@ require_once "db.php";
$redir = safe_redir($_GET["redir"] ?? "/");
$cookie_options_expired = ["expires" => time() - 3600, "path" => "/", "httponly" => true, "secure" => true, "samesite" => "Lax"];
setcookie("auth_user", "", $cookie_options_expired);
setcookie("auth_pass_b64", "", $cookie_options_expired);
setcookie("auth_token", "", $cookie_options_expired);
db_delete_session();
session_unset();
session_destroy();

View File

@@ -0,0 +1,12 @@
-- Axia4 Migration 005: Add remember_token_hash to user_sessions
-- Replaces the auth_user + auth_pass_b64 cookie pair with a secure opaque token.
-- The raw token lives only in the browser cookie; only its SHA-256 hash is stored.
PRAGMA journal_mode = WAL;
PRAGMA foreign_keys = ON;
ALTER TABLE user_sessions ADD COLUMN remember_token_hash TEXT DEFAULT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS idx_user_sessions_remember
ON user_sessions (remember_token_hash)
WHERE remember_token_hash IS NOT NULL;

View File

@@ -22,28 +22,36 @@ if (str_starts_with($ua, "Axia4Auth/")) {
$_SESSION["auth_user"] = $username;
$_SESSION["auth_data"] = db_build_auth_data($row);
$_SESSION["auth_ok"] = true;
$_COOKIE["auth_user"] = $username;
$_COOKIE["auth_pass_b64"] = base64_encode($userpass);
$_SESSION["auth_external_lock"] = "header";
init_active_org($_SESSION["auth_data"]);
}
// ── Cookie-based auto-login ───────────────────────────────────────────────────
if (($_SESSION["auth_ok"] ?? false) != true
&& isset($_COOKIE["auth_user"], $_COOKIE["auth_pass_b64"])
) {
$username = $_COOKIE["auth_user"];
$userpass = base64_decode($_COOKIE["auth_pass_b64"]);
$row = db_get_user($username);
if ($row && password_verify($userpass, $row['password_hash'])) {
$_SESSION["auth_user"] = $username;
$_SESSION["auth_data"] = db_build_auth_data($row);
$_SESSION["auth_ok"] = true;
if (empty($_SESSION["session_created"])) {
$_SESSION["session_created"] = time();
// ── Remember-token auto-login ─────────────────────────────────────────────────
// Restores the session from the opaque auth_token cookie (no password stored).
if (($_SESSION["auth_ok"] ?? false) != true && isset($_COOKIE["auth_token"])) {
$expired = ["expires" => time() - 3600, "path" => "/", "httponly" => true,
"secure" => true, "samesite" => "Lax"];
$raw_token = $_COOKIE["auth_token"];
$token_hash = hash('sha256', $raw_token);
$sess_row = db_restore_session_by_remember_token($token_hash);
if ($sess_row) {
$username = $sess_row['username'];
$row = db_get_user($username);
if ($row) {
$_SESSION["auth_user"] = $username;
$_SESSION["auth_data"] = db_build_auth_data($row);
$_SESSION["auth_ok"] = true;
if (empty($_SESSION["session_created"])) {
$_SESSION["session_created"] = time();
}
init_active_org($_SESSION["auth_data"]);
} else {
// User no longer exists — clear the stale cookie
setcookie("auth_token", "", $expired);
}
init_active_org($_SESSION["auth_data"]);
db_register_session($username);
} else {
// Token not found (revoked or expired) — clear the stale cookie
setcookie("auth_token", "", $expired);
}
}

View File

@@ -100,10 +100,11 @@ if (($_GET["google_callback"] ?? "") === "1") {
$_SESSION['auth_ok'] = true;
$_SESSION['session_created'] = time();
init_active_org($_SESSION['auth_data']);
db_register_session($username);
$remember_token = bin2hex(random_bytes(32));
$remember_token_hash = hash('sha256', $remember_token);
db_register_session($username, $remember_token_hash);
$cookie_options = ["expires" => time() + (86400 * 30), "path" => "/", "httponly" => true, "secure" => true, "samesite" => "Lax"];
setcookie("auth_user", $username, $cookie_options);
setcookie("auth_pass_b64", base64_encode($password), $cookie_options);
setcookie("auth_token", $remember_token, $cookie_options);
$redir = safe_redir($state["redir"] ?? "/");
@@ -141,8 +142,7 @@ if (($_GET["google"] ?? "") === "1") {
if (($_GET["logout"] ?? "") === "1") {
$redir = safe_redir($_GET["redir"] ?? "/");
$cookie_options_expired = ["expires" => time() - 3600, "path" => "/", "httponly" => true, "secure" => true, "samesite" => "Lax"];
setcookie("auth_user", "", $cookie_options_expired);
setcookie("auth_pass_b64", "", $cookie_options_expired);
setcookie("auth_token", "", $cookie_options_expired);
db_delete_session();
session_unset();
session_destroy();
@@ -168,16 +168,17 @@ if (isset($_POST["user"])) {
if (!$row || !isset($row["password_hash"])) {
$_GET["_result"] = "El usuario no existe.";
} elseif (password_verify($password, $row["password_hash"])) {
$remember_token = bin2hex(random_bytes(32));
$remember_token_hash = hash('sha256', $remember_token);
session_regenerate_id(true);
$_SESSION['auth_user'] = $user;
$_SESSION['auth_data'] = db_build_auth_data($row);
$_SESSION['auth_ok'] = true;
$_SESSION['session_created'] = time();
init_active_org($_SESSION['auth_data']);
db_register_session($user);
db_register_session($user, $remember_token_hash);
$cookie_options = ["expires" => time() + (86400 * 30), "path" => "/", "httponly" => true, "secure" => true, "samesite" => "Lax"];
setcookie("auth_user", $user, $cookie_options);
setcookie("auth_pass_b64", base64_encode($password), $cookie_options);
setcookie("auth_token", $remember_token, $cookie_options);
$redir = safe_redir($_GET["redir"] ?? "/");
header("Location: $redir");
die();