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) ────────────────────────────
|
||||
|
||||
/**
|
||||
* 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. */
|
||||
|
||||
@@ -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();
|
||||
|
||||
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_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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user