Merge pull request #17 from Axia4/copilot/add-session-management-login
Add proper session management: secure cookies, CSRF, connected devices, opaque remember token
This commit is contained in:
@@ -769,3 +769,115 @@ function init_active_centro(?array $auth_data = null): void
|
||||
{
|
||||
init_active_org($auth_data);
|
||||
}
|
||||
|
||||
// ── User session helpers (Dispositivos conectados) ────────────────────────────
|
||||
|
||||
/**
|
||||
* 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, string $remember_token_hash = ''): void
|
||||
{
|
||||
$token = hash('sha256', session_id());
|
||||
$ip = $_SERVER['HTTP_X_FORWARDED_FOR']
|
||||
?? $_SERVER['REMOTE_ADDR']
|
||||
?? '';
|
||||
$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, remember_token_hash)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(session_token) DO UPDATE SET
|
||||
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. */
|
||||
function db_touch_session(): void
|
||||
{
|
||||
$token = hash('sha256', session_id());
|
||||
db()->prepare(
|
||||
"UPDATE user_sessions SET last_active = datetime('now') WHERE session_token = ?"
|
||||
)->execute([$token]);
|
||||
}
|
||||
|
||||
/** Delete the current session record from the DB. */
|
||||
function db_delete_session(): void
|
||||
{
|
||||
$token = hash('sha256', session_id());
|
||||
db()->prepare('DELETE FROM user_sessions WHERE session_token = ?')->execute([$token]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a specific session by token for a given user.
|
||||
* Enforces that the session belongs to the requesting user.
|
||||
*/
|
||||
function db_revoke_session(string $token, string $username): void
|
||||
{
|
||||
db()->prepare('DELETE FROM user_sessions WHERE session_token = ? AND username = ?')
|
||||
->execute([$token, strtolower($username)]);
|
||||
}
|
||||
|
||||
/** Delete all session records for a user (e.g. on password change or account wipe). */
|
||||
function db_delete_user_sessions(string $username): void
|
||||
{
|
||||
db()->prepare('DELETE FROM user_sessions WHERE username = ?')
|
||||
->execute([strtolower($username)]);
|
||||
}
|
||||
|
||||
/** Return all session rows for a user, newest first. */
|
||||
function db_get_user_sessions(string $username): array
|
||||
{
|
||||
$stmt = db()->prepare(
|
||||
'SELECT session_token, ip_address, user_agent, created_at, last_active
|
||||
FROM user_sessions
|
||||
WHERE username = ?
|
||||
ORDER BY last_active DESC'
|
||||
);
|
||||
$stmt->execute([strtolower($username)]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the current PHP session has a valid record in user_sessions.
|
||||
* Returns false if the session was revoked or was never registered.
|
||||
*/
|
||||
function db_session_is_valid(string $username): bool
|
||||
{
|
||||
$token = hash('sha256', session_id());
|
||||
$stmt = db()->prepare(
|
||||
'SELECT 1 FROM user_sessions WHERE session_token = ? AND username = ? LIMIT 1'
|
||||
);
|
||||
$stmt->execute([$token, strtolower($username)]);
|
||||
return $stmt->fetchColumn() !== false;
|
||||
}
|
||||
|
||||
13
public_html/_incl/logout.php
Normal file
13
public_html/_incl/logout.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
require_once "tools.session.php";
|
||||
require_once "tools.security.php";
|
||||
require_once "db.php";
|
||||
|
||||
$redir = safe_redir($_GET["redir"] ?? "/");
|
||||
$cookie_options_expired = ["expires" => time() - 3600, "path" => "/", "httponly" => true, "secure" => true, "samesite" => "Lax"];
|
||||
setcookie("auth_token", "", $cookie_options_expired);
|
||||
db_delete_session();
|
||||
session_unset();
|
||||
session_destroy();
|
||||
header("Location: $redir");
|
||||
die();
|
||||
17
public_html/_incl/migrations/004_user_sessions.sql
Normal file
17
public_html/_incl/migrations/004_user_sessions.sql
Normal file
@@ -0,0 +1,17 @@
|
||||
-- Axia4 Migration 004: User Sessions (Dispositivos conectados)
|
||||
-- Tracks active authenticated sessions so users can see and revoke connected devices.
|
||||
|
||||
PRAGMA journal_mode = WAL;
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_token TEXT UNIQUE NOT NULL, -- SHA-256 hash of the PHP session_id()
|
||||
username TEXT NOT NULL,
|
||||
ip_address TEXT NOT NULL DEFAULT '',
|
||||
user_agent TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
last_active TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_sessions_username ON user_sessions (username);
|
||||
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,24 +22,48 @@ 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"]);
|
||||
// ── 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 && password_verify($userpass, $row['password_hash'])) {
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
// Token not found (revoked or expired) — clear the stale cookie
|
||||
setcookie("auth_token", "", $expired);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Validate session is still active in user_sessions (enables revocation) ───
|
||||
if (!empty($_SESSION["auth_ok"]) && !empty($_SESSION["auth_user"])
|
||||
&& empty($_SESSION["auth_external_lock"])
|
||||
) {
|
||||
if (!db_session_is_valid($_SESSION["auth_user"])) {
|
||||
session_unset();
|
||||
session_destroy();
|
||||
header("Location: /_login.php?_result=" . urlencode("Tu sesión fue revocada. Inicia sesión de nuevo."));
|
||||
die();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +77,7 @@ if (!empty($_SESSION["auth_ok"]) && !empty($_SESSION["auth_user"])) {
|
||||
init_active_org($_SESSION["auth_data"]);
|
||||
}
|
||||
$_SESSION["last_reload_time"] = time();
|
||||
db_touch_session();
|
||||
} elseif ($load_mode !== "never") {
|
||||
$last = $_SESSION["last_reload_time"] ?? 0;
|
||||
if (time() - $last > 300) {
|
||||
@@ -62,6 +87,7 @@ if (!empty($_SESSION["auth_ok"]) && !empty($_SESSION["auth_user"])) {
|
||||
init_active_org($_SESSION["auth_data"]);
|
||||
}
|
||||
$_SESSION["last_reload_time"] = time();
|
||||
db_touch_session();
|
||||
}
|
||||
if (!isset($_SESSION["last_reload_time"])) {
|
||||
$_SESSION["last_reload_time"] = time();
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<?php
|
||||
session_start([ 'cookie_lifetime' => 604800 ]);
|
||||
ini_set("session.use_only_cookies", "true");
|
||||
ini_set("session.use_trans_sid", "false");
|
||||
ini_set("session.use_only_cookies", "1");
|
||||
ini_set("session.use_trans_sid", "0");
|
||||
session_start([
|
||||
'cookie_lifetime' => 604800,
|
||||
'cookie_httponly' => true,
|
||||
'cookie_secure' => true,
|
||||
'cookie_samesite' => 'Lax',
|
||||
]);
|
||||
|
||||
@@ -98,10 +98,13 @@ if (($_GET["google_callback"] ?? "") === "1") {
|
||||
$_SESSION['auth_user'] = $username;
|
||||
$_SESSION['auth_data'] = db_build_auth_data($user_row);
|
||||
$_SESSION['auth_ok'] = true;
|
||||
$_SESSION['session_created'] = time();
|
||||
init_active_org($_SESSION['auth_data']);
|
||||
$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"] ?? "/");
|
||||
|
||||
@@ -139,13 +142,15 @@ 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();
|
||||
header("Location: $redir");
|
||||
die();
|
||||
}
|
||||
if (($_GET["clear_session"] ?? "") === "1") {
|
||||
session_unset();
|
||||
session_destroy();
|
||||
$redir = safe_redir($_GET["redir"] ?? "/");
|
||||
header("Location: $redir");
|
||||
@@ -154,33 +159,46 @@ if (($_GET["clear_session"] ?? "") === "1") {
|
||||
if (isset($_POST["user"])) {
|
||||
$user = trim(strtolower($_POST["user"]));
|
||||
$password = $_POST["password"];
|
||||
// Validate CSRF token
|
||||
$csrf_token = $_POST["_csrf"] ?? "";
|
||||
if (!$csrf_token || !isset($_SESSION["login_csrf"]) || !hash_equals($_SESSION["login_csrf"], $csrf_token)) {
|
||||
$_GET["_result"] = "Token de seguridad inválido. Por favor, recarga la página e inténtalo de nuevo.";
|
||||
} else {
|
||||
$row = db_get_user($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, $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();
|
||||
} else {
|
||||
$_GET["_result"] = "La contraseña no es correcta.";
|
||||
}
|
||||
}
|
||||
}
|
||||
if (strval(db_get_config('installed')) !== '1') {
|
||||
header("Location: /_install.php");
|
||||
die();
|
||||
}
|
||||
if (empty($_SESSION["login_csrf"])) {
|
||||
$_SESSION["login_csrf"] = bin2hex(random_bytes(32));
|
||||
}
|
||||
require_once "_incl/pre-body.php";
|
||||
?>
|
||||
|
||||
<form method="post" action="?redir=<?= urlencode($_GET["redir"] ?? "/") ?>">
|
||||
<input type="hidden" name="_csrf" value="<?= htmlspecialchars($_SESSION["login_csrf"]) ?>">
|
||||
<div class="card pad" style="max-width: 500px;">
|
||||
<h1 style="text-align: center;">Iniciar sesión en Axia4</h1>
|
||||
<div>
|
||||
|
||||
@@ -23,6 +23,51 @@ $initials = mb_strtoupper(mb_substr($parts[0] ?? '', 0, 1) . mb_substr($parts[1]
|
||||
if ($initials === '') {
|
||||
$initials = '?';
|
||||
}
|
||||
|
||||
// Connected devices
|
||||
$connectedSessions = db_get_user_sessions($username);
|
||||
$currentToken = hash('sha256', session_id());
|
||||
|
||||
/**
|
||||
* Parse a raw User-Agent string into a human-readable browser + OS label.
|
||||
* Returns an array ['browser' => string, 'os' => string, 'icon' => string].
|
||||
*/
|
||||
function parse_ua(string $ua): array
|
||||
{
|
||||
// OS
|
||||
$os = 'Desconocido';
|
||||
if (stripos($ua, 'Android') !== false) $os = 'Android';
|
||||
elseif (stripos($ua, 'iPhone') !== false
|
||||
|| stripos($ua, 'iPad') !== false) $os = 'iOS';
|
||||
elseif (stripos($ua, 'Windows') !== false) $os = 'Windows';
|
||||
elseif (stripos($ua, 'Macintosh') !== false
|
||||
|| stripos($ua, 'Mac OS') !== false) $os = 'macOS';
|
||||
elseif (stripos($ua, 'Linux') !== false) $os = 'Linux';
|
||||
elseif (stripos($ua, 'CrOS') !== false) $os = 'ChromeOS';
|
||||
|
||||
// Browser
|
||||
$browser = 'Desconocido';
|
||||
if (str_starts_with($ua, 'Axia4Auth/')) $browser = 'Axia4 App';
|
||||
elseif (stripos($ua, 'Edg/') !== false) $browser = 'Edge';
|
||||
elseif (stripos($ua, 'OPR/') !== false
|
||||
|| stripos($ua, 'Opera') !== false) $browser = 'Opera';
|
||||
elseif (stripos($ua, 'Chrome') !== false) $browser = 'Chrome';
|
||||
elseif (stripos($ua, 'Firefox') !== false) $browser = 'Firefox';
|
||||
elseif (stripos($ua, 'Safari') !== false) $browser = 'Safari';
|
||||
|
||||
// Emoji icon for a quick visual cue
|
||||
$icon = match ($browser) {
|
||||
'Chrome' => '🌐',
|
||||
'Firefox' => '🦊',
|
||||
'Safari' => '🧭',
|
||||
'Edge' => '🔷',
|
||||
'Opera' => '🔴',
|
||||
'Axia4 App' => '📱',
|
||||
default => '💻',
|
||||
};
|
||||
|
||||
return ['browser' => $browser, 'os' => $os, 'icon' => $icon];
|
||||
}
|
||||
?>
|
||||
<style>
|
||||
.account-grid { display: flex; flex-wrap: wrap; gap: 16px; padding: 16px; }
|
||||
@@ -38,6 +83,13 @@ if ($initials === '') {
|
||||
.tenant-btn { display: block; width: 100%; text-align: left; padding: 8px 12px; border: 1px solid #e0e0e0; border-radius: 8px; margin-bottom: 8px; background: #f8f9fa; cursor: pointer; font-size: .9rem; transition: background .15s; }
|
||||
.tenant-btn:hover { background: #e8f0fe; }
|
||||
.tenant-btn.active-tenant { border-color: var(--gw-blue, #1a73e8); background: #e8f0fe; font-weight: 600; }
|
||||
.device-row { display: flex; align-items: center; gap: 12px; padding: 10px 0; border-bottom: 1px solid #f1f3f4; }
|
||||
.device-row:last-child { border-bottom: none; }
|
||||
.device-icon { font-size: 1.6rem; flex-shrink: 0; }
|
||||
.device-info { flex: 1; min-width: 0; }
|
||||
.device-name { font-weight: 600; font-size: .9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.device-meta { font-size: .78rem; color: var(--gw-text-secondary, #5f6368); margin-top: 2px; }
|
||||
.device-current { font-size: .75rem; color: #137333; font-weight: 600; background: #e6f4ea; border-radius: 99px; padding: 1px 8px; }
|
||||
</style>
|
||||
|
||||
<div class="account-grid">
|
||||
@@ -122,11 +174,58 @@ if ($initials === '') {
|
||||
<div class="info-row"><span class="label">ID Sesión</span><span style="font-family:monospace;font-size:.75rem;"><?= htmlspecialchars(substr(session_id(), 0, 12)) ?>…</span></div>
|
||||
<div class="info-row"><span class="label">Org. activa</span><span><?= htmlspecialchars($activeOrganization ?: '–') ?></span></div>
|
||||
<div class="info-row"><span class="label">Autenticación</span><span><?= empty($authData['google_auth']) ? 'Contraseña' : 'Google' ?></span></div>
|
||||
<?php if (!empty($_SESSION['session_created'])): ?>
|
||||
<div class="info-row"><span class="label">Sesión iniciada</span><span><?= htmlspecialchars(date('d/m/Y H:i', $_SESSION['session_created'])) ?></span></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($_SESSION['last_reload_time'])): ?>
|
||||
<div class="info-row"><span class="label">Última actividad</span><span><?= htmlspecialchars(date('d/m/Y H:i', $_SESSION['last_reload_time'])) ?></span></div>
|
||||
<?php endif; ?>
|
||||
<div style="margin-top:16px;">
|
||||
<a href="/_incl/logout.php" class="btn btn-danger btn-sm">Cerrar sesión</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Connected Devices Card -->
|
||||
<?php if (!empty($connectedSessions)): ?>
|
||||
<div class="account-card" style="flex-basis:100%;">
|
||||
<h2>Dispositivos conectados</h2>
|
||||
<?php foreach ($connectedSessions as $sess):
|
||||
$isCurrent = hash_equals($currentToken, $sess['session_token']);
|
||||
$ua_info = parse_ua($sess['user_agent']);
|
||||
$label = $ua_info['browser'] . ' — ' . $ua_info['os'];
|
||||
?>
|
||||
<div class="device-row">
|
||||
<div class="device-icon"><?= $ua_info['icon'] ?></div>
|
||||
<div class="device-info">
|
||||
<div class="device-name">
|
||||
<?= htmlspecialchars($label) ?>
|
||||
<?php if ($isCurrent): ?>
|
||||
<span class="device-current">Este dispositivo</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="device-meta">
|
||||
IP: <?= htmlspecialchars($sess['ip_address'] ?: '–') ?>
|
||||
·
|
||||
Inicio: <?= htmlspecialchars(substr($sess['created_at'], 0, 16)) ?>
|
||||
·
|
||||
Activo: <?= htmlspecialchars(substr($sess['last_active'], 0, 16)) ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php if (!$isCurrent): ?>
|
||||
<form method="post" action="/account/revoke_session.php" style="margin:0;">
|
||||
<input type="hidden" name="token" value="<?= htmlspecialchars($sess['session_token']) ?>">
|
||||
<input type="hidden" name="redir" value="/account/">
|
||||
<button type="submit" class="btn btn-outline-danger btn-sm"
|
||||
onclick="return confirm('¿Cerrar sesión en este dispositivo?')">
|
||||
Revocar
|
||||
</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
|
||||
<?php require_once "_incl/post-body.php"; ?>
|
||||
|
||||
42
public_html/account/revoke_session.php
Normal file
42
public_html/account/revoke_session.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/**
|
||||
* Revoke a connected device session.
|
||||
* POST-only. Requires the user to be authenticated.
|
||||
* Accepts: token (session_token hash), redir (safe redirect URL).
|
||||
*/
|
||||
require_once "_incl/auth_redir.php";
|
||||
require_once "../_incl/db.php";
|
||||
require_once "../_incl/tools.security.php";
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('HTTP/1.1 405 Method Not Allowed');
|
||||
die();
|
||||
}
|
||||
|
||||
$username = $_SESSION['auth_user'] ?? '';
|
||||
if ($username === '') {
|
||||
header('HTTP/1.1 401 Unauthorized');
|
||||
die();
|
||||
}
|
||||
|
||||
$token = preg_replace('/[^a-f0-9]/', '', strtolower($_POST['token'] ?? ''));
|
||||
$redir = safe_redir($_POST['redir'] ?? '/account/');
|
||||
|
||||
if ($token === '') {
|
||||
header("Location: $redir");
|
||||
die();
|
||||
}
|
||||
|
||||
$current_token = hash('sha256', session_id());
|
||||
|
||||
// Prevent revoking the current session through this endpoint
|
||||
// (users should use the regular logout for that)
|
||||
if (hash_equals($current_token, $token)) {
|
||||
header("Location: $redir");
|
||||
die();
|
||||
}
|
||||
|
||||
db_revoke_session($token, $username);
|
||||
|
||||
header("Location: $redir");
|
||||
die();
|
||||
Reference in New Issue
Block a user