diff --git a/public_html/_incl/db.php b/public_html/_incl/db.php
index e728caa..7968cfd 100644
--- a/public_html/_incl/db.php
+++ b/public_html/_incl/db.php
@@ -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. */
diff --git a/public_html/_incl/logout.php b/public_html/_incl/logout.php
index dcf590c..7b1b2a2 100644
--- a/public_html/_incl/logout.php
+++ b/public_html/_incl/logout.php
@@ -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();
diff --git a/public_html/_incl/migrations/005_remember_token.sql b/public_html/_incl/migrations/005_remember_token.sql
new file mode 100644
index 0000000..4502e24
--- /dev/null
+++ b/public_html/_incl/migrations/005_remember_token.sql
@@ -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;
diff --git a/public_html/_incl/tools.auth.php b/public_html/_incl/tools.auth.php
index eee350d..04a84fc 100644
--- a/public_html/_incl/tools.auth.php
+++ b/public_html/_incl/tools.auth.php
@@ -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);
}
}
diff --git a/public_html/_login.php b/public_html/_login.php
index a6c2eb4..21d6cad 100644
--- a/public_html/_login.php
+++ b/public_html/_login.php
@@ -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();