Close login gap
This commit is contained in:
@@ -27,7 +27,7 @@ require_once '../../../assets/utils/db_auth.php';
|
|||||||
|
|
||||||
// Clear session token so the account is free to log in elsewhere immediately
|
// Clear session token so the account is free to log in elsewhere immediately
|
||||||
if (!empty($_SESSION['login_user_id'])) {
|
if (!empty($_SESSION['login_user_id'])) {
|
||||||
$pdo1->prepare("UPDATE user SET session_token = NULL, session_token_at = NULL WHERE user_id = :uid")
|
$pdo1->prepare("UPDATE user SET session_token = NULL, session_token_at = NULL, session_last_seen = NULL WHERE user_id = :uid")
|
||||||
->execute([':uid' => (int)$_SESSION['login_user_id']]);
|
->execute([':uid' => (int)$_SESSION['login_user_id']]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -95,29 +95,7 @@ $otp_diff_minutes = $otp_diff_seconds / 60.0;
|
|||||||
$_SESSION["now"] = $now;
|
$_SESSION["now"] = $now;
|
||||||
$_SESSION["diff"] = $otp_diff_minutes;
|
$_SESSION["diff"] = $otp_diff_minutes;
|
||||||
|
|
||||||
// ── Step 4: Block login if another session is already active ─────────────────
|
// ── Step 4: Validate OTP value and expiry ─────────────────────────────────────
|
||||||
// If session_token is non-NULL AND was set within the last 8 hours, another
|
|
||||||
// session is active — reject. Tokens older than 8 hours are treated as
|
|
||||||
// abandoned (browser crash, PHP GC expiry, etc.) and cleared automatically.
|
|
||||||
$sth_token = $pdo1->prepare("SELECT session_token, session_last_seen FROM user WHERE user_id = :uid LIMIT 1");
|
|
||||||
$sth_token->execute([':uid' => $user_id]);
|
|
||||||
$token_row = $sth_token->fetch(PDO::FETCH_ASSOC);
|
|
||||||
$existing_token = $token_row['session_token'] ?? null;
|
|
||||||
if (!empty($existing_token)) {
|
|
||||||
$idle_seconds = PHP_INT_MAX;
|
|
||||||
if (!empty($token_row['session_last_seen'])) {
|
|
||||||
$idle_seconds = time() - strtotime($token_row['session_last_seen']);
|
|
||||||
}
|
|
||||||
if ($idle_seconds < (int)ini_get('session.gc_maxlifetime')) {
|
|
||||||
$answer["message"] = "This account is currently logged in on another device. Please wait for the other session to end.";
|
|
||||||
exit(json_encode($answer));
|
|
||||||
}
|
|
||||||
// PHP GC has expired this session — clear token and allow login
|
|
||||||
$pdo1->prepare("UPDATE user SET session_token = NULL, session_token_at = NULL, session_last_seen = NULL WHERE user_id = :uid")
|
|
||||||
->execute([':uid' => $user_id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Step 4b: Validate OTP value and expiry ────────────────────────────────────
|
|
||||||
// Skipped for staff/viewer roles — login_otp.php sets skip_otp=true in session
|
// Skipped for staff/viewer roles — login_otp.php sets skip_otp=true in session
|
||||||
// so they never receive or enter an OTP. Admin/owner always go through this check.
|
// so they never receive or enter an OTP. Admin/owner always go through this check.
|
||||||
if (empty($_SESSION['skip_otp'])) {
|
if (empty($_SESSION['skip_otp'])) {
|
||||||
@@ -127,10 +105,33 @@ if (empty($_SESSION['skip_otp'])) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Step 4c: Claim session — write token so no one else can log in ────────────
|
// ── Step 4b: Claim session atomically ─────────────────────────────────────────
|
||||||
|
// The WHERE clause is the concurrency gate: only a free or stale token can be
|
||||||
|
// replaced. session_last_seen is set immediately so a fresh login is live before
|
||||||
|
// the first authenticated heartbeat in db_auth.php.
|
||||||
$session_token = bin2hex(random_bytes(32));
|
$session_token = bin2hex(random_bytes(32));
|
||||||
$pdo1->prepare("UPDATE user SET session_token = :token, session_token_at = NOW() WHERE user_id = :uid")
|
$sth_claim = $pdo1->prepare(
|
||||||
->execute([':token' => $session_token, ':uid' => $user_id]);
|
"UPDATE user
|
||||||
|
SET session_token = :token,
|
||||||
|
session_token_at = NOW(),
|
||||||
|
session_last_seen = NOW()
|
||||||
|
WHERE user_id = :uid
|
||||||
|
AND (
|
||||||
|
session_token IS NULL
|
||||||
|
OR session_last_seen IS NULL
|
||||||
|
OR TIMESTAMPDIFF(SECOND, session_last_seen, NOW()) >= :gc_maxlifetime
|
||||||
|
)"
|
||||||
|
);
|
||||||
|
$sth_claim->execute([
|
||||||
|
':token' => $session_token,
|
||||||
|
':uid' => $user_id,
|
||||||
|
':gc_maxlifetime' => (int)ini_get('session.gc_maxlifetime'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($sth_claim->rowCount() !== 1) {
|
||||||
|
$answer["message"] = "This account is currently logged in on another device. Please wait for the other session to end.";
|
||||||
|
exit(json_encode($answer));
|
||||||
|
}
|
||||||
|
|
||||||
// ── Step 5a: Regenerate session ID ────────────────────────────────────────────
|
// ── Step 5a: Regenerate session ID ────────────────────────────────────────────
|
||||||
// session_regenerate_id(true) issues a brand-new session ID and deletes the old
|
// session_regenerate_id(true) issues a brand-new session ID and deletes the old
|
||||||
|
|||||||
Reference in New Issue
Block a user