login: block concurrent login, single-factor auth for staff/viewer
This commit is contained in:
@@ -23,10 +23,12 @@
|
||||
* b. The elapsed time since otpTime is ≤ 5 minutes.
|
||||
* Fail either → return "Wrong OTP! Please try again."
|
||||
* 5. On success:
|
||||
* a. session_regenerate_id(true) — prevents session fixation attack by
|
||||
* a. Check session_token in DB — if non-NULL, another session is active;
|
||||
* reject login with "account already logged in" message.
|
||||
* b. session_regenerate_id(true) — prevents session fixation attack by
|
||||
* issuing a new session ID and deleting the old one.
|
||||
* b. Generate a fresh CSRF token and store in session.
|
||||
* c. Write the authenticated login session keys:
|
||||
* c. Generate a fresh CSRF token and store in session.
|
||||
* d. Write the authenticated login session keys:
|
||||
* login_status=1, login_username, login_name, login_surname,
|
||||
* login_company_id (from user's default_company).
|
||||
* 6. Return { success: 1, message: "Login Complete!" }.
|
||||
@@ -93,15 +95,43 @@ $otp_diff_minutes = $otp_diff_seconds / 60.0;
|
||||
$_SESSION["now"] = $now;
|
||||
$_SESSION["diff"] = $otp_diff_minutes;
|
||||
|
||||
// ── Step 4: Validate OTP value and expiry ─────────────────────────────────────
|
||||
// Fails if either the code doesn't match OR more than 5 minutes have elapsed
|
||||
// since the OTP was issued. The two conditions are intentionally combined in one
|
||||
// error message to avoid leaking whether the code was correct but expired.
|
||||
if ($data["otp"] != $otp || $otp_diff_minutes > 5) {
|
||||
$answer["message"] = "Wrong OTP! Please try again. (Our OTP is valid for 5 minute)";
|
||||
exit(json_encode($answer));
|
||||
// ── 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_token_at 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)) {
|
||||
$token_age_hours = PHP_INT_MAX;
|
||||
if (!empty($token_row['session_token_at'])) {
|
||||
$token_age_hours = (time() - strtotime($token_row['session_token_at'])) / 3600;
|
||||
}
|
||||
if ($token_age_hours < 8) {
|
||||
$answer["message"] = "This account is currently logged in on another device. Please wait for the other session to end.";
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
// Stale token — clear it and proceed with login
|
||||
$pdo1->prepare("UPDATE user SET session_token = NULL, session_token_at = 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
|
||||
// so they never receive or enter an OTP. Admin/owner always go through this check.
|
||||
if (empty($_SESSION['skip_otp'])) {
|
||||
if ($data["otp"] != $otp || $otp_diff_minutes > 5) {
|
||||
$answer["message"] = "Wrong OTP! Please try again. (Our OTP is valid for 5 minute)";
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Step 4c: Claim session — write token so no one else can log in ────────────
|
||||
$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]);
|
||||
|
||||
// ── Step 5a: Regenerate session ID ────────────────────────────────────────────
|
||||
// session_regenerate_id(true) issues a brand-new session ID and deletes the old
|
||||
// session file, preventing session fixation attacks where an attacker pre-sets
|
||||
@@ -118,6 +148,7 @@ $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
// These keys are read by db_auth.php on every subsequent request to gate access.
|
||||
// login_company_id is the user's default_company — used to scope all DB queries.
|
||||
$_SESSION["login_status"] = 1;
|
||||
$_SESSION['session_token'] = $session_token;
|
||||
$_SESSION["login_user_id"] = (int)$temp["user_id"];
|
||||
$_SESSION["login_username"] = $temp["username"];
|
||||
$_SESSION["login_name"] = $temp["name"];
|
||||
@@ -125,6 +156,9 @@ $_SESSION["login_surname"] = $temp["surname"];
|
||||
$_SESSION["login_company_id"] = $temp["default_company"];
|
||||
$_SESSION["login_profile_picture"] = $temp["profile_picture"] ?? '';
|
||||
$_SESSION["login_license"] = $temp["license"] ?? 'user';
|
||||
// Required by db_auth.php's per-request OTP integrity check. For skip_otp users
|
||||
// (staff/viewer) this was never written by login_otp.php, so we set it here.
|
||||
$_SESSION["otp"] = $otp;
|
||||
// license='owner' means the user holds their own subscription — use user.app_access.
|
||||
// license='user' means they were invited — use company_map_user.app_access instead.
|
||||
$_SESSION["login_app_access"] = $temp["app_access"] ?? 'wms';
|
||||
|
||||
Reference in New Issue
Block a user