diff --git a/assets/js/session-guard.js b/assets/js/session-guard.js index 966711b..f75e9fd 100644 --- a/assets/js/session-guard.js +++ b/assets/js/session-guard.js @@ -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 * - Waarschuwt de gebruiker 2 minuten voor het verlopen met aftelklok * - Throttled keepalive pings tijdens actieve interactie * - Synchronisatie over meerdere open browsertabbladen via localStorage + * - Beschermd tegen oneindige herlaad- en redirect-loops */ (function () { @@ -17,20 +18,40 @@ // 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 lastActivity = Date.now(); - var lastPingTime = Date.now(); + var now = Date.now(); + var lastActivity = now; + var lastPingTime = now; var isWarningShowing = 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'); if (storedActivity) { var parsed = parseInt(storedActivity, 10); - if (!isNaN(parsed) && parsed > 0 && parsed <= Date.now()) { - lastActivity = parsed; + if (!isNaN(parsed) && parsed > 0 && parsed <= now && (now - parsed) < (idleTimeout * 1000)) { + 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 var modalEl = null; var countdownEl = null; @@ -69,7 +90,13 @@ } } } 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 function sendKeepalive(manualClick) { - if (keepaliveInFlight) return; + if (hasRedirected || keepaliveInFlight) return; keepaliveInFlight = true; if (manualClick && keepaliveBtn) { @@ -121,9 +148,8 @@ }) .then(function (res) { if (res.status === 401) { - // Reeds verlopen - localStorage.setItem('fociz_session_logged_out', Date.now().toString()); - window.location.href = '/auth/login.php?expired=1'; + // Reeds verlopen op de server + redirectToExpired(); return null; } return res.json(); @@ -136,9 +162,12 @@ } if (data && data.success) { - lastActivity = Date.now(); - lastPingTime = Date.now(); - localStorage.setItem('fociz_session_last_activity', lastActivity.toString()); + var currentNow = Date.now(); + lastActivity = currentNow; + lastPingTime = currentNow; + try { + localStorage.setItem('fociz_session_last_activity', currentNow.toString()); + } catch (e) {} hideModal(); } }) @@ -154,7 +183,11 @@ // Direct uitloggen 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', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' } @@ -165,49 +198,49 @@ // Registreer gebruikersactiviteit (throttled) 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) - var elapsedSec = Math.floor((now - lastActivity) / 1000); + // Controleer of de sessie al verlopen is (bijv. laptop uit slaapstand) + var elapsedSec = Math.floor((currentNow - lastActivity) / 1000); if (elapsedSec >= idleTimeout) { - localStorage.setItem('fociz_session_logged_out', now.toString()); - window.location.href = '/auth/login.php?expired=1'; + redirectToExpired(); return; } - // Als de waarschuwingsmodal actief is, negeren we vage muisbewegingen; - // de gebruiker moet expliciet op 'Blijf Ingelogd' klikken conform beveiligingsnormen. + // Als de waarschuwingsmodal actief is, vereisen we expliciete klik op 'Blijf Ingelogd' if (isWarningShowing) return; // Lokale activiteit bijwerken (maximaal eens per 3 seconden wegschrijven) - if (now - lastActivity > 3000) { - lastActivity = now; - localStorage.setItem('fociz_session_last_activity', now.toString()); + if (currentNow - lastActivity > 3000) { + lastActivity = currentNow; + try { + localStorage.setItem('fociz_session_last_activity', currentNow.toString()); + } catch (e) {} } - // Server keepalive: stuur op de achtergrond een ping als er activiteit is - // én er minimaal 60 seconden (of idleTimeout / 4) sinds de vorige serverping is verstreken. + // Server keepalive ping (maximaal eens per 60 seconden) var minPingInterval = Math.min(60000, Math.max(15000, (idleTimeout * 1000) / 4)); - if (now - lastPingTime > minPingInterval) { - lastPingTime = now; + if (currentNow - lastPingTime > minPingInterval) { + lastPingTime = currentNow; sendKeepalive(false); } } // Direct controleren bij terugkeer naar tabblad of ontwaken uit slaapstand function checkIdleOnWakeup() { - var now = Date.now(); - var storedActivity = localStorage.getItem('fociz_session_last_activity'); - if (storedActivity) { - var parsed = parseInt(storedActivity, 10); - if (!isNaN(parsed) && parsed > 0) { + if (hasRedirected) return; + var currentNow = Date.now(); + var stored = localStorage.getItem('fociz_session_last_activity'); + if (stored) { + var parsed = parseInt(stored, 10); + if (!isNaN(parsed) && parsed > 0 && parsed <= currentNow) { lastActivity = Math.max(lastActivity, parsed); } } - var elapsedSec = Math.floor((now - lastActivity) / 1000); + var elapsedSec = Math.floor((currentNow - lastActivity) / 1000); if (elapsedSec >= idleTimeout) { - localStorage.setItem('fociz_session_logged_out', now.toString()); - window.location.href = '/auth/login.php?expired=1'; + redirectToExpired(); } } @@ -226,14 +259,13 @@ // Hoofdtimer interval (elke seconde) function tick() { - var now = Date.now(); - var elapsedSec = Math.floor((now - lastActivity) / 1000); + if (hasRedirected) return; + var currentNow = Date.now(); + var elapsedSec = Math.floor((currentNow - lastActivity) / 1000); var remainingSec = idleTimeout - elapsedSec; if (remainingSec <= 0) { - // Sessie officieel verlopen - localStorage.setItem('fociz_session_logged_out', now.toString()); - window.location.href = '/auth/login.php?expired=1'; + redirectToExpired(); return; } @@ -257,4 +289,3 @@ setInterval(tick, 1000); } })(); - diff --git a/auth/login.php b/auth/login.php index 94f8292..a0508ec 100644 --- a/auth/login.php +++ b/auth/login.php @@ -12,6 +12,18 @@ if (!has_admin_user()) { 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 $currentUser = get_logged_in_user(); if ($currentUser) { @@ -338,6 +350,10 @@ $pageTitle = "Fociz — Inloggen"; diff --git a/includes/auth.php b/includes/auth.php index 7effed8..47a4423 100644 --- a/includes/auth.php +++ b/includes/auth.php @@ -28,9 +28,6 @@ function is_https_request(): bool { if (isset($_SERVER['SERVER_PORT']) && (int)$_SERVER['SERVER_PORT'] === 443) { return true; } - if (defined('APP_URL') && str_starts_with(APP_URL, 'https://')) { - return true; - } return false; } @@ -306,9 +303,8 @@ function start_secure_session(): void { ini_set('session.gc_maxlifetime', '86400'); ini_set('session.cookie_httponly', '1'); - // Cookie prefix: __Host- garandeert dat cookies niet door subdomeinen kunnen worden overschreven - $sessionName = $isSecure ? '__Host-fociz_session' : 'fociz_session'; - session_name($sessionName); + // Sessienaam instellen (veilig en uniform) + session_name('fociz_session'); session_set_cookie_params([ '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->execute([$hash, $userId]); $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'])); } catch (Throwable $e) { return true; // Fallback op toegestaan bij databaseverstoring diff --git a/includes/header.php b/includes/header.php index 81a1ccd..598e17b 100644 --- a/includes/header.php +++ b/includes/header.php @@ -64,9 +64,12 @@ $currentUserTheme = !empty($currentUser['id']) ? get_user_theme((int)$currentUse