feat(security): implement session management features including idle timeout, keepalive, and secure logout

This commit is contained in:
2026-09-11 15:47:09 +02:00
parent dc740fb94d
commit e012d6d21c
7 changed files with 515 additions and 5 deletions
+38 -4
View File
@@ -10,6 +10,7 @@ if (basename($_SERVER['SCRIPT_FILENAME'] ?? '') === 'auth.php') {
require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/totp.php';
require_once __DIR__ . '/settings.php';
/**
* Bepaalt of de huidige verbinding HTTPS gebruikt (inclusief reverse proxies / OpenResty / Docker)
@@ -472,10 +473,13 @@ function get_logged_in_user(): ?array {
// 2. Controleer actieve PHP sessie (mits behorend bij vandaag én hetzelfde IP/fingerprint)
if (!empty($_SESSION['user_id']) && !empty($_SESSION['is_2fa_verified'])) {
// Inactiviteitstimeout (ISO 27001 / SOC 2): 120 minuten (7200s)
$maxIdleSeconds = 7200;
// Dynamische inactiviteitstimeout conform NIST SP 800-63B / ISO 27001 / OWASP ASVS v4.0
$maxIdleSeconds = function_exists('get_session_idle_timeout') ? get_session_idle_timeout() : 1800;
if (!empty($_SESSION['last_activity']) && ($now - (int)$_SESSION['last_activity']) > $maxIdleSeconds) {
fociz_log_audit((int)$_SESSION['user_id'], 'SESSION_IDLE_TIMEOUT', 'users', (int)$_SESSION['user_id'], ['idle_seconds' => $now - (int)$_SESSION['last_activity']], 'WARNING');
fociz_log_audit((int)$_SESSION['user_id'], 'SESSION_IDLE_TIMEOUT', 'users', (int)$_SESSION['user_id'], ['idle_seconds' => $now - (int)$_SESSION['last_activity'], 'max_idle' => $maxIdleSeconds], 'WARNING');
// Invalideer dagsessie token in database zodat herstel via cookie uitgesloten is
$stmtClear = $db->prepare("UPDATE users SET remember_token = NULL, remember_token_expires_at = NULL WHERE id = ?");
$stmtClear->execute([$_SESSION['user_id']]);
$_SESSION = ['session_was_expired' => true, 'idle_timeout' => true];
clear_day_session_cookie();
if (session_status() === PHP_SESSION_ACTIVE) {
@@ -484,6 +488,20 @@ function get_logged_in_user(): ?array {
return null;
}
// Absolute sessielimiet (NIST SP 800-63B): max 10 uur aaneengesloten sessie per dag
$maxSessionDuration = 36000;
if (!empty($_SESSION['login_time']) && ($now - (int)$_SESSION['login_time']) > $maxSessionDuration) {
fociz_log_audit((int)$_SESSION['user_id'], 'SESSION_ABSOLUTE_TIMEOUT', 'users', (int)$_SESSION['user_id'], ['duration' => $now - (int)$_SESSION['login_time']], 'WARNING');
$stmtClear = $db->prepare("UPDATE users SET remember_token = NULL, remember_token_expires_at = NULL WHERE id = ?");
$stmtClear->execute([$_SESSION['user_id']]);
$_SESSION = ['session_was_expired' => true, 'absolute_timeout' => true];
clear_day_session_cookie();
if (session_status() === PHP_SESSION_ACTIVE) {
@session_regenerate_id(true);
}
return null;
}
// IP-verificatie: als het IP/locatie tussentijds verandert, moet er opnieuw worden ingelogd
if (!empty($_SESSION['auth_fingerprint']) && $_SESSION['auth_fingerprint'] !== $clientFingerprint) {
$_SESSION = ['session_was_expired' => true, 'ip_changed' => true];
@@ -498,6 +516,20 @@ function get_logged_in_user(): ?array {
$stmt->execute([$_SESSION['user_id']]);
$user = $stmt->fetch();
if ($user) {
// Periodieke sessie-ID rotatie (OWASP ASVS v4.0 §3.3): elke 30 minuten ter voorkoming van session hijacking
if (empty($_SESSION['last_rotated'])) {
$_SESSION['last_rotated'] = $now;
} elseif (($now - (int)$_SESSION['last_rotated']) > 1800) {
if (session_status() === PHP_SESSION_ACTIVE) {
@session_regenerate_id(true);
}
$_SESSION['last_rotated'] = $now;
}
if (empty($_SESSION['login_time'])) {
$_SESSION['login_time'] = $now;
}
$_SESSION['auth_date'] = $today;
$_SESSION['auth_fingerprint'] = $clientFingerprint;
$_SESSION['session_expires_at'] = strtotime('today 23:59:59');
@@ -510,7 +542,7 @@ function get_logged_in_user(): ?array {
return null;
}
// 3. Fallback dagsessie-token: herstelt sessie na browserherstart of server idle timeout (uitsluitend vandaag!)
// 3. Fallback dagsessie-token: herstelt sessie na browserherstart (uitsluitend vandaag!)
$dayToken = $_COOKIE['fociz_day_session'] ?? ($_COOKIE['fociz_remember'] ?? null);
if (!empty($dayToken) && is_string($dayToken)) {
$stmt = $db->prepare("SELECT id, name, email, role, two_factor_enabled, avatar_url, theme, notification_settings, default_jira_filter_code, fociz_widget_layout, created_at FROM users WHERE remember_token = ? AND remember_token_expires_at >= NOW() AND DATE(remember_token_expires_at) = ?");
@@ -534,6 +566,8 @@ function get_logged_in_user(): ?array {
$_SESSION['auth_fingerprint'] = $clientFingerprint;
$_SESSION['session_expires_at'] = strtotime('today 23:59:59');
$_SESSION['last_activity'] = $now;
$_SESSION['login_time'] = $now;
$_SESSION['last_rotated'] = $now;
return $user;
}
// Token is ongeldig of van gisteren
+43
View File
@@ -16,6 +16,49 @@
<!-- Fociz Agenda Notificaties Engine -->
<script src="/assets/js/notifications.js"></script>
<?php
$sessionIdleTimeout = function_exists('get_session_idle_timeout') ? get_session_idle_timeout() : 1800;
?>
<!-- Fociz Inactiviteitswaarschuwing Modal (ISO 27001 / NIST SP 800-63B) -->
<div id="focizSessionModal" class="fixed inset-0 z-[9999] bg-slate-950/80 backdrop-blur-md hidden items-center justify-center p-4">
<div class="bg-white dark:bg-[#111622] border border-amber-500/30 dark:border-amber-500/30 rounded-3xl w-full max-w-md shadow-2xl p-6 sm:p-7 text-center relative overflow-hidden">
<div class="w-14 h-14 rounded-2xl bg-amber-500/10 dark:bg-amber-500/20 border border-amber-500/30 text-amber-600 dark:text-amber-400 flex items-center justify-center mx-auto mb-4">
<svg class="w-7 h-7 animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
</svg>
</div>
<h3 class="text-lg font-bold text-slate-900 dark:text-white tracking-tight mb-2">
Sessie verloopt bijna
</h3>
<p class="text-xs text-slate-500 dark:text-slate-400 mb-5 leading-relaxed">
Wegens inactiviteit wordt u binnenkort automatisch afgemeld conform het beveiligingsbeleid. Klik op <strong class="text-slate-800 dark:text-slate-200">Blijf Ingelogd</strong> om uw sessie te verlengen.
</p>
<div class="inline-flex items-center space-x-2 px-4 py-2 rounded-2xl bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-800/60 text-amber-700 dark:text-amber-300 font-mono text-xs font-bold mb-6">
<svg class="w-4 h-4 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke-width="2"></circle>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6l4 2"></path>
</svg>
<span>Verloopt over:</span>
<span id="focizSessionCountdown" class="text-amber-600 dark:text-amber-400 font-black tracking-wider">02:00</span>
</div>
<div class="grid grid-cols-2 gap-3">
<button type="button" id="focizSessionLogoutBtn" class="w-full py-2.5 px-4 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 text-xs font-semibold transition cursor-pointer">
Nu Uitloggen
</button>
<button type="button" id="focizSessionKeepaliveBtn" class="w-full py-2.5 px-4 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold transition shadow-xs cursor-pointer">
Blijf Ingelogd
</button>
</div>
</div>
</div>
<!-- Fociz Session Guard Script (NIST SP 800-63B / ISO 27001) -->
<script src="/assets/js/session-guard.js" data-timeout="<?= $sessionIdleTimeout ?>"></script>
<script>
// Lucide Icons initialiseren
if (typeof lucide !== 'undefined') {
+10
View File
@@ -129,3 +129,13 @@ function get_ai_settings(): array {
];
}
/**
* Haalt geconfigureerde sessie idle timeout in seconden op conform NIST SP 800-63B / ISO 27001
* Toegestane waarden: 900 (15m), 1800 (30m - default), 3600 (60m), 7200 (120m)
*/
function get_session_idle_timeout(): int {
$timeout = (int)get_system_setting('session_idle_timeout', 1800);
$validTimeouts = [900, 1800, 3600, 7200];
return in_array($timeout, $validTimeouts, true) ? $timeout : 1800;
}