Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8095a63f31 | |||
| 987bcf5684 |
+74
-43
@@ -1,9 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* Fociz Session Guard (Release v1.6)
|
* Fociz Session Guard (Release v1.6.1)
|
||||||
* Realtime beveiligingsbeheer voor sessie-inactiviteit conform NIST SP 800-63B / ISO 27001
|
* Realtime beveiligingsbeheer voor sessie-inactiviteit conform NIST SP 800-63B / ISO 27001
|
||||||
* - Waarschuwt de gebruiker 2 minuten voor het verlopen met aftelklok
|
* - Waarschuwt de gebruiker 2 minuten voor het verlopen met aftelklok
|
||||||
* - Throttled keepalive pings tijdens actieve interactie
|
* - Throttled keepalive pings tijdens actieve interactie
|
||||||
* - Synchronisatie over meerdere open browsertabbladen via localStorage
|
* - Synchronisatie over meerdere open browsertabbladen via localStorage
|
||||||
|
* - Beschermd tegen oneindige herlaad- en redirect-loops
|
||||||
*/
|
*/
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
@@ -17,20 +18,40 @@
|
|||||||
// Waarschuwing 120 seconden voor afloop (of de helft indien timeout kort is)
|
// Waarschuwing 120 seconden voor afloop (of de helft indien timeout kort is)
|
||||||
var warningSeconds = idleTimeout > 240 ? 120 : Math.max(10, Math.floor(idleTimeout / 2));
|
var warningSeconds = idleTimeout > 240 ? 120 : Math.max(10, Math.floor(idleTimeout / 2));
|
||||||
|
|
||||||
var lastActivity = Date.now();
|
var now = Date.now();
|
||||||
var lastPingTime = Date.now();
|
var lastActivity = now;
|
||||||
|
var lastPingTime = now;
|
||||||
var isWarningShowing = false;
|
var isWarningShowing = false;
|
||||||
var keepaliveInFlight = false;
|
var keepaliveInFlight = false;
|
||||||
|
var hasRedirected = false;
|
||||||
|
|
||||||
// Synchronisatie met andere tabbladen
|
// Synchronisatie met andere tabbladen:
|
||||||
|
// Alleen een timestamp overnemen als deze GELDIG is en NIET al verlopen is!
|
||||||
|
// Aangezien DEZE pagina zojuist succesvol geladen is, is de sessie NU actief.
|
||||||
var storedActivity = localStorage.getItem('fociz_session_last_activity');
|
var storedActivity = localStorage.getItem('fociz_session_last_activity');
|
||||||
if (storedActivity) {
|
if (storedActivity) {
|
||||||
var parsed = parseInt(storedActivity, 10);
|
var parsed = parseInt(storedActivity, 10);
|
||||||
if (!isNaN(parsed) && parsed > 0 && parsed <= Date.now()) {
|
if (!isNaN(parsed) && parsed > 0 && parsed <= now && (now - parsed) < (idleTimeout * 1000)) {
|
||||||
lastActivity = parsed;
|
lastActivity = Math.max(now, parsed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Direct opslaan zodat alle tabbladen weten dat er zojuist activiteit was
|
||||||
|
try {
|
||||||
|
localStorage.setItem('fociz_session_last_activity', lastActivity.toString());
|
||||||
|
localStorage.removeItem('fociz_session_logged_out');
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
// Veilige eenmalige redirect helper om redirect loops te voorkomen
|
||||||
|
function redirectToExpired() {
|
||||||
|
if (hasRedirected) return;
|
||||||
|
hasRedirected = true;
|
||||||
|
try {
|
||||||
|
localStorage.setItem('fociz_session_logged_out', Date.now().toString());
|
||||||
|
} catch (e) {}
|
||||||
|
window.location.href = '/auth/login.php?expired=1';
|
||||||
|
}
|
||||||
|
|
||||||
// Modal DOM elementen
|
// Modal DOM elementen
|
||||||
var modalEl = null;
|
var modalEl = null;
|
||||||
var countdownEl = null;
|
var countdownEl = null;
|
||||||
@@ -69,7 +90,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (e.key === 'fociz_session_logged_out') {
|
} else if (e.key === 'fociz_session_logged_out') {
|
||||||
window.location.href = '/auth/login.php?expired=1';
|
if (!hasRedirected) {
|
||||||
|
var loggedOutTime = parseInt(e.newValue, 10);
|
||||||
|
if (!isNaN(loggedOutTime) && Math.abs(Date.now() - loggedOutTime) < 10000) {
|
||||||
|
hasRedirected = true;
|
||||||
|
window.location.href = '/auth/login.php?expired=1';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -104,7 +131,7 @@
|
|||||||
|
|
||||||
// Verzend keepalive naar de backend
|
// Verzend keepalive naar de backend
|
||||||
function sendKeepalive(manualClick) {
|
function sendKeepalive(manualClick) {
|
||||||
if (keepaliveInFlight) return;
|
if (hasRedirected || keepaliveInFlight) return;
|
||||||
keepaliveInFlight = true;
|
keepaliveInFlight = true;
|
||||||
|
|
||||||
if (manualClick && keepaliveBtn) {
|
if (manualClick && keepaliveBtn) {
|
||||||
@@ -121,9 +148,8 @@
|
|||||||
})
|
})
|
||||||
.then(function (res) {
|
.then(function (res) {
|
||||||
if (res.status === 401) {
|
if (res.status === 401) {
|
||||||
// Reeds verlopen
|
// Reeds verlopen op de server
|
||||||
localStorage.setItem('fociz_session_logged_out', Date.now().toString());
|
redirectToExpired();
|
||||||
window.location.href = '/auth/login.php?expired=1';
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return res.json();
|
return res.json();
|
||||||
@@ -136,9 +162,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (data && data.success) {
|
if (data && data.success) {
|
||||||
lastActivity = Date.now();
|
var currentNow = Date.now();
|
||||||
lastPingTime = Date.now();
|
lastActivity = currentNow;
|
||||||
localStorage.setItem('fociz_session_last_activity', lastActivity.toString());
|
lastPingTime = currentNow;
|
||||||
|
try {
|
||||||
|
localStorage.setItem('fociz_session_last_activity', currentNow.toString());
|
||||||
|
} catch (e) {}
|
||||||
hideModal();
|
hideModal();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -154,7 +183,11 @@
|
|||||||
|
|
||||||
// Direct uitloggen
|
// Direct uitloggen
|
||||||
function performLogout() {
|
function performLogout() {
|
||||||
localStorage.setItem('fociz_session_logged_out', Date.now().toString());
|
if (hasRedirected) return;
|
||||||
|
hasRedirected = true;
|
||||||
|
try {
|
||||||
|
localStorage.setItem('fociz_session_logged_out', Date.now().toString());
|
||||||
|
} catch (e) {}
|
||||||
fetch('/api/session.php?action=logout', {
|
fetch('/api/session.php?action=logout', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||||
@@ -165,49 +198,49 @@
|
|||||||
|
|
||||||
// Registreer gebruikersactiviteit (throttled)
|
// Registreer gebruikersactiviteit (throttled)
|
||||||
function registerUserActivity() {
|
function registerUserActivity() {
|
||||||
var now = Date.now();
|
if (hasRedirected) return;
|
||||||
|
var currentNow = Date.now();
|
||||||
|
|
||||||
// Controleer ALTIJD eerst of de sessie al verlopen was terwijl de gebruiker weg was (bijv. laptop slaapstand of inactiviteit)
|
// Controleer of de sessie al verlopen is (bijv. laptop uit slaapstand)
|
||||||
var elapsedSec = Math.floor((now - lastActivity) / 1000);
|
var elapsedSec = Math.floor((currentNow - lastActivity) / 1000);
|
||||||
if (elapsedSec >= idleTimeout) {
|
if (elapsedSec >= idleTimeout) {
|
||||||
localStorage.setItem('fociz_session_logged_out', now.toString());
|
redirectToExpired();
|
||||||
window.location.href = '/auth/login.php?expired=1';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Als de waarschuwingsmodal actief is, negeren we vage muisbewegingen;
|
// Als de waarschuwingsmodal actief is, vereisen we expliciete klik op 'Blijf Ingelogd'
|
||||||
// de gebruiker moet expliciet op 'Blijf Ingelogd' klikken conform beveiligingsnormen.
|
|
||||||
if (isWarningShowing) return;
|
if (isWarningShowing) return;
|
||||||
|
|
||||||
// Lokale activiteit bijwerken (maximaal eens per 3 seconden wegschrijven)
|
// Lokale activiteit bijwerken (maximaal eens per 3 seconden wegschrijven)
|
||||||
if (now - lastActivity > 3000) {
|
if (currentNow - lastActivity > 3000) {
|
||||||
lastActivity = now;
|
lastActivity = currentNow;
|
||||||
localStorage.setItem('fociz_session_last_activity', now.toString());
|
try {
|
||||||
|
localStorage.setItem('fociz_session_last_activity', currentNow.toString());
|
||||||
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server keepalive: stuur op de achtergrond een ping als er activiteit is
|
// Server keepalive ping (maximaal eens per 60 seconden)
|
||||||
// én er minimaal 60 seconden (of idleTimeout / 4) sinds de vorige serverping is verstreken.
|
|
||||||
var minPingInterval = Math.min(60000, Math.max(15000, (idleTimeout * 1000) / 4));
|
var minPingInterval = Math.min(60000, Math.max(15000, (idleTimeout * 1000) / 4));
|
||||||
if (now - lastPingTime > minPingInterval) {
|
if (currentNow - lastPingTime > minPingInterval) {
|
||||||
lastPingTime = now;
|
lastPingTime = currentNow;
|
||||||
sendKeepalive(false);
|
sendKeepalive(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Direct controleren bij terugkeer naar tabblad of ontwaken uit slaapstand
|
// Direct controleren bij terugkeer naar tabblad of ontwaken uit slaapstand
|
||||||
function checkIdleOnWakeup() {
|
function checkIdleOnWakeup() {
|
||||||
var now = Date.now();
|
if (hasRedirected) return;
|
||||||
var storedActivity = localStorage.getItem('fociz_session_last_activity');
|
var currentNow = Date.now();
|
||||||
if (storedActivity) {
|
var stored = localStorage.getItem('fociz_session_last_activity');
|
||||||
var parsed = parseInt(storedActivity, 10);
|
if (stored) {
|
||||||
if (!isNaN(parsed) && parsed > 0) {
|
var parsed = parseInt(stored, 10);
|
||||||
|
if (!isNaN(parsed) && parsed > 0 && parsed <= currentNow) {
|
||||||
lastActivity = Math.max(lastActivity, parsed);
|
lastActivity = Math.max(lastActivity, parsed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var elapsedSec = Math.floor((now - lastActivity) / 1000);
|
var elapsedSec = Math.floor((currentNow - lastActivity) / 1000);
|
||||||
if (elapsedSec >= idleTimeout) {
|
if (elapsedSec >= idleTimeout) {
|
||||||
localStorage.setItem('fociz_session_logged_out', now.toString());
|
redirectToExpired();
|
||||||
window.location.href = '/auth/login.php?expired=1';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,14 +259,13 @@
|
|||||||
|
|
||||||
// Hoofdtimer interval (elke seconde)
|
// Hoofdtimer interval (elke seconde)
|
||||||
function tick() {
|
function tick() {
|
||||||
var now = Date.now();
|
if (hasRedirected) return;
|
||||||
var elapsedSec = Math.floor((now - lastActivity) / 1000);
|
var currentNow = Date.now();
|
||||||
|
var elapsedSec = Math.floor((currentNow - lastActivity) / 1000);
|
||||||
var remainingSec = idleTimeout - elapsedSec;
|
var remainingSec = idleTimeout - elapsedSec;
|
||||||
|
|
||||||
if (remainingSec <= 0) {
|
if (remainingSec <= 0) {
|
||||||
// Sessie officieel verlopen
|
redirectToExpired();
|
||||||
localStorage.setItem('fociz_session_logged_out', now.toString());
|
|
||||||
window.location.href = '/auth/login.php?expired=1';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,4 +289,3 @@
|
|||||||
setInterval(tick, 1000);
|
setInterval(tick, 1000);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,18 @@ if (!has_admin_user()) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Als de sessie verlopen, ingetrokken of gewijzigd is: beëindig deze direct en wis cookies
|
||||||
|
if (isset($_GET['expired']) || isset($_GET['revoked']) || isset($_GET['ip_changed'])) {
|
||||||
|
if (!empty($_SESSION['user_id'])) {
|
||||||
|
fociz_terminate_session(session_id());
|
||||||
|
}
|
||||||
|
clear_day_session_cookie();
|
||||||
|
$_SESSION = [];
|
||||||
|
if (session_status() === PHP_SESSION_ACTIVE) {
|
||||||
|
@session_regenerate_id(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Als al ingelogd (via actieve sessie of persistente cookie), ga direct naar planner
|
// Als al ingelogd (via actieve sessie of persistente cookie), ga direct naar planner
|
||||||
$currentUser = get_logged_in_user();
|
$currentUser = get_logged_in_user();
|
||||||
if ($currentUser) {
|
if ($currentUser) {
|
||||||
@@ -338,6 +350,10 @@ $pageTitle = "Fociz — Inloggen";
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
try {
|
||||||
|
localStorage.removeItem('fociz_session_last_activity');
|
||||||
|
localStorage.removeItem('fociz_session_logged_out');
|
||||||
|
} catch(e) {}
|
||||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+8
-7
@@ -28,9 +28,6 @@ function is_https_request(): bool {
|
|||||||
if (isset($_SERVER['SERVER_PORT']) && (int)$_SERVER['SERVER_PORT'] === 443) {
|
if (isset($_SERVER['SERVER_PORT']) && (int)$_SERVER['SERVER_PORT'] === 443) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (defined('APP_URL') && str_starts_with(APP_URL, 'https://')) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,9 +303,8 @@ function start_secure_session(): void {
|
|||||||
ini_set('session.gc_maxlifetime', '86400');
|
ini_set('session.gc_maxlifetime', '86400');
|
||||||
ini_set('session.cookie_httponly', '1');
|
ini_set('session.cookie_httponly', '1');
|
||||||
|
|
||||||
// Cookie prefix: __Host- garandeert dat cookies niet door subdomeinen kunnen worden overschreven
|
// Sessienaam instellen (veilig en uniform)
|
||||||
$sessionName = $isSecure ? '__Host-fociz_session' : 'fociz_session';
|
session_name('fociz_session');
|
||||||
session_name($sessionName);
|
|
||||||
|
|
||||||
session_set_cookie_params([
|
session_set_cookie_params([
|
||||||
'lifetime' => 0, // Browser sessiecookie: vervalt direct bij afsluiten browser conform ISO 27001 / NIST SP 800-63B
|
'lifetime' => 0, // Browser sessiecookie: vervalt direct bij afsluiten browser conform ISO 27001 / NIST SP 800-63B
|
||||||
@@ -501,7 +497,12 @@ function fociz_is_session_active(string $sessionId, int $userId): bool {
|
|||||||
$stmt = $db->prepare("SELECT is_active, expires_at FROM user_sessions WHERE session_id_hash = ? AND user_id = ?");
|
$stmt = $db->prepare("SELECT is_active, expires_at FROM user_sessions WHERE session_id_hash = ? AND user_id = ?");
|
||||||
$stmt->execute([$hash, $userId]);
|
$stmt->execute([$hash, $userId]);
|
||||||
$row = $stmt->fetch();
|
$row = $stmt->fetch();
|
||||||
if (!$row) return true; // Als sessie nog niet geregistreerd staat, laat het door (wordt geregistreerd)
|
if (!$row) {
|
||||||
|
// Als sessie nog niet geregistreerd staat, registreer direct om tracking consistent te houden
|
||||||
|
$maxIdleSeconds = function_exists('get_session_idle_timeout') ? get_session_idle_timeout() : 1800;
|
||||||
|
fociz_register_active_session($userId, $sessionId, $maxIdleSeconds);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return (!empty($row['is_active']));
|
return (!empty($row['is_active']));
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
return true; // Fallback op toegestaan bij databaseverstoring
|
return true; // Fallback op toegestaan bij databaseverstoring
|
||||||
|
|||||||
@@ -64,9 +64,12 @@ $currentUserTheme = !empty($currentUser['id']) ? get_user_theme((int)$currentUse
|
|||||||
<script>
|
<script>
|
||||||
(function() {
|
(function() {
|
||||||
try {
|
try {
|
||||||
|
let hasRedirected = false;
|
||||||
const sessionExpiresAt = <?= strtotime('today 23:59:59') ?> * 1000;
|
const sessionExpiresAt = <?= strtotime('today 23:59:59') ?> * 1000;
|
||||||
const checkExpiry = function() {
|
const checkExpiry = function() {
|
||||||
|
if (hasRedirected) return;
|
||||||
if (Date.now() > sessionExpiresAt + 1000) {
|
if (Date.now() > sessionExpiresAt + 1000) {
|
||||||
|
hasRedirected = true;
|
||||||
window.location.href = '/auth/login.php?expired=1';
|
window.location.href = '/auth/login.php?expired=1';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user