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:
@@ -773,26 +773,53 @@ function init_active_centro(?array $auth_data = null): void
|
|||||||
// ── User session helpers (Dispositivos conectados) ────────────────────────────
|
// ── 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.
|
* 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());
|
$token = hash('sha256', session_id());
|
||||||
$ip = $_SERVER['HTTP_X_FORWARDED_FOR']
|
$ip = $_SERVER['HTTP_X_FORWARDED_FOR']
|
||||||
?? $_SERVER['REMOTE_ADDR']
|
?? $_SERVER['REMOTE_ADDR']
|
||||||
?? '';
|
?? '';
|
||||||
// Only keep the first IP if X-Forwarded-For contains a list
|
|
||||||
$ip = trim(explode(',', $ip)[0]);
|
$ip = trim(explode(',', $ip)[0]);
|
||||||
$ua = substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 512);
|
$ua = substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 512);
|
||||||
|
|
||||||
db()->prepare(
|
db()->prepare(
|
||||||
"INSERT INTO user_sessions (session_token, username, ip_address, user_agent)
|
"INSERT INTO user_sessions (session_token, username, ip_address, user_agent, remember_token_hash)
|
||||||
VALUES (?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?)
|
||||||
ON CONFLICT(session_token) DO UPDATE SET
|
ON CONFLICT(session_token) DO UPDATE SET
|
||||||
last_active = datetime('now'),
|
last_active = datetime('now'),
|
||||||
username = excluded.username"
|
remember_token_hash = COALESCE(excluded.remember_token_hash, remember_token_hash)"
|
||||||
)->execute([$token, strtolower($username), $ip, $ua]);
|
)->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. */
|
/** Update last_active for the current session. */
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ require_once "db.php";
|
|||||||
|
|
||||||
$redir = safe_redir($_GET["redir"] ?? "/");
|
$redir = safe_redir($_GET["redir"] ?? "/");
|
||||||
$cookie_options_expired = ["expires" => time() - 3600, "path" => "/", "httponly" => true, "secure" => true, "samesite" => "Lax"];
|
$cookie_options_expired = ["expires" => time() - 3600, "path" => "/", "httponly" => true, "secure" => true, "samesite" => "Lax"];
|
||||||
setcookie("auth_user", "", $cookie_options_expired);
|
setcookie("auth_token", "", $cookie_options_expired);
|
||||||
setcookie("auth_pass_b64", "", $cookie_options_expired);
|
|
||||||
db_delete_session();
|
db_delete_session();
|
||||||
session_unset();
|
session_unset();
|
||||||
session_destroy();
|
session_destroy();
|
||||||
|
|||||||
12
public_html/_incl/migrations/005_remember_token.sql
Normal file
12
public_html/_incl/migrations/005_remember_token.sql
Normal 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;
|
||||||
@@ -22,28 +22,36 @@ if (str_starts_with($ua, "Axia4Auth/")) {
|
|||||||
$_SESSION["auth_user"] = $username;
|
$_SESSION["auth_user"] = $username;
|
||||||
$_SESSION["auth_data"] = db_build_auth_data($row);
|
$_SESSION["auth_data"] = db_build_auth_data($row);
|
||||||
$_SESSION["auth_ok"] = true;
|
$_SESSION["auth_ok"] = true;
|
||||||
$_COOKIE["auth_user"] = $username;
|
|
||||||
$_COOKIE["auth_pass_b64"] = base64_encode($userpass);
|
|
||||||
$_SESSION["auth_external_lock"] = "header";
|
$_SESSION["auth_external_lock"] = "header";
|
||||||
init_active_org($_SESSION["auth_data"]);
|
init_active_org($_SESSION["auth_data"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Cookie-based auto-login ───────────────────────────────────────────────────
|
// ── Remember-token auto-login ─────────────────────────────────────────────────
|
||||||
if (($_SESSION["auth_ok"] ?? false) != true
|
// Restores the session from the opaque auth_token cookie (no password stored).
|
||||||
&& isset($_COOKIE["auth_user"], $_COOKIE["auth_pass_b64"])
|
if (($_SESSION["auth_ok"] ?? false) != true && isset($_COOKIE["auth_token"])) {
|
||||||
) {
|
$expired = ["expires" => time() - 3600, "path" => "/", "httponly" => true,
|
||||||
$username = $_COOKIE["auth_user"];
|
"secure" => true, "samesite" => "Lax"];
|
||||||
$userpass = base64_decode($_COOKIE["auth_pass_b64"]);
|
$raw_token = $_COOKIE["auth_token"];
|
||||||
$row = db_get_user($username);
|
$token_hash = hash('sha256', $raw_token);
|
||||||
if ($row && password_verify($userpass, $row['password_hash'])) {
|
$sess_row = db_restore_session_by_remember_token($token_hash);
|
||||||
$_SESSION["auth_user"] = $username;
|
if ($sess_row) {
|
||||||
$_SESSION["auth_data"] = db_build_auth_data($row);
|
$username = $sess_row['username'];
|
||||||
$_SESSION["auth_ok"] = true;
|
$row = db_get_user($username);
|
||||||
if (empty($_SESSION["session_created"])) {
|
if ($row) {
|
||||||
$_SESSION["session_created"] = time();
|
$_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"]);
|
} else {
|
||||||
db_register_session($username);
|
// Token not found (revoked or expired) — clear the stale cookie
|
||||||
|
setcookie("auth_token", "", $expired);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -100,10 +100,11 @@ if (($_GET["google_callback"] ?? "") === "1") {
|
|||||||
$_SESSION['auth_ok'] = true;
|
$_SESSION['auth_ok'] = true;
|
||||||
$_SESSION['session_created'] = time();
|
$_SESSION['session_created'] = time();
|
||||||
init_active_org($_SESSION['auth_data']);
|
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"];
|
$cookie_options = ["expires" => time() + (86400 * 30), "path" => "/", "httponly" => true, "secure" => true, "samesite" => "Lax"];
|
||||||
setcookie("auth_user", $username, $cookie_options);
|
setcookie("auth_token", $remember_token, $cookie_options);
|
||||||
setcookie("auth_pass_b64", base64_encode($password), $cookie_options);
|
|
||||||
|
|
||||||
$redir = safe_redir($state["redir"] ?? "/");
|
$redir = safe_redir($state["redir"] ?? "/");
|
||||||
|
|
||||||
@@ -141,8 +142,7 @@ if (($_GET["google"] ?? "") === "1") {
|
|||||||
if (($_GET["logout"] ?? "") === "1") {
|
if (($_GET["logout"] ?? "") === "1") {
|
||||||
$redir = safe_redir($_GET["redir"] ?? "/");
|
$redir = safe_redir($_GET["redir"] ?? "/");
|
||||||
$cookie_options_expired = ["expires" => time() - 3600, "path" => "/", "httponly" => true, "secure" => true, "samesite" => "Lax"];
|
$cookie_options_expired = ["expires" => time() - 3600, "path" => "/", "httponly" => true, "secure" => true, "samesite" => "Lax"];
|
||||||
setcookie("auth_user", "", $cookie_options_expired);
|
setcookie("auth_token", "", $cookie_options_expired);
|
||||||
setcookie("auth_pass_b64", "", $cookie_options_expired);
|
|
||||||
db_delete_session();
|
db_delete_session();
|
||||||
session_unset();
|
session_unset();
|
||||||
session_destroy();
|
session_destroy();
|
||||||
@@ -168,16 +168,17 @@ if (isset($_POST["user"])) {
|
|||||||
if (!$row || !isset($row["password_hash"])) {
|
if (!$row || !isset($row["password_hash"])) {
|
||||||
$_GET["_result"] = "El usuario no existe.";
|
$_GET["_result"] = "El usuario no existe.";
|
||||||
} elseif (password_verify($password, $row["password_hash"])) {
|
} 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_regenerate_id(true);
|
||||||
$_SESSION['auth_user'] = $user;
|
$_SESSION['auth_user'] = $user;
|
||||||
$_SESSION['auth_data'] = db_build_auth_data($row);
|
$_SESSION['auth_data'] = db_build_auth_data($row);
|
||||||
$_SESSION['auth_ok'] = true;
|
$_SESSION['auth_ok'] = true;
|
||||||
$_SESSION['session_created'] = time();
|
$_SESSION['session_created'] = time();
|
||||||
init_active_org($_SESSION['auth_data']);
|
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"];
|
$cookie_options = ["expires" => time() + (86400 * 30), "path" => "/", "httponly" => true, "secure" => true, "samesite" => "Lax"];
|
||||||
setcookie("auth_user", $user, $cookie_options);
|
setcookie("auth_token", $remember_token, $cookie_options);
|
||||||
setcookie("auth_pass_b64", base64_encode($password), $cookie_options);
|
|
||||||
$redir = safe_redir($_GET["redir"] ?? "/");
|
$redir = safe_redir($_GET["redir"] ?? "/");
|
||||||
header("Location: $redir");
|
header("Location: $redir");
|
||||||
die();
|
die();
|
||||||
|
|||||||
Reference in New Issue
Block a user