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
|
||||
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']]);
|
||||
}
|
||||
|
||||
|
||||
@@ -95,29 +95,7 @@ $otp_diff_minutes = $otp_diff_seconds / 60.0;
|
||||
$_SESSION["now"] = $now;
|
||||
$_SESSION["diff"] = $otp_diff_minutes;
|
||||
|
||||
// ── Step 4: Block login if another session is already active ─────────────────
|
||||
// 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 ────────────────────────────────────
|
||||
// ── Step 4: Validate OTP value and expiry ─────────────────────────────────────
|
||||
// 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.
|
||||
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));
|
||||
$pdo1->prepare("UPDATE user SET session_token = :token, session_token_at = NOW() WHERE user_id = :uid")
|
||||
->execute([':token' => $session_token, ':uid' => $user_id]);
|
||||
$sth_claim = $pdo1->prepare(
|
||||
"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 ────────────────────────────────────────────
|
||||
// session_regenerate_id(true) issues a brand-new session ID and deletes the old
|
||||
|
||||
Reference in New Issue
Block a user