diff --git a/api/session.php b/api/session.php index ca385db..ad8e212 100644 --- a/api/session.php +++ b/api/session.php @@ -56,6 +56,7 @@ if ($action === 'logout') { $stmt = $db->prepare("UPDATE users SET remember_token = NULL, remember_token_expires_at = NULL WHERE id = ?"); $stmt->execute([$userId]); + fociz_terminate_session(session_id()); fociz_log_audit($userId, 'LOGOUT_SESSION_GUARD', 'users', $userId, ['ip' => fociz_get_client_ip()], 'SUCCESS', $currentUser['email'] ?? null); } catch (Throwable $e) { error_log("Session guard logout error: " . $e->getMessage()); @@ -72,6 +73,51 @@ if ($action === 'logout') { exit; } +// Haal actieve apparaten en sessies op (SOC 2 CC6.1 / ISO 27001 A.9.4.2) +if ($action === 'get_active_sessions') { + $sessions = fociz_get_user_active_sessions($userId, session_id()); + echo json_encode([ + 'success' => true, + 'sessions' => $sessions, + 'timestamp' => $now + ], JSON_UNESCAPED_UNICODE); + exit; +} + +// Beëindig alle overige actieve sessies (SOC 2 CC6.1) +if ($action === 'revoke_other_sessions') { + if (!verify_csrf_token($_POST['csrf_token'] ?? null)) { + http_response_code(403); + echo json_encode(['success' => false, 'error' => 'Ongeldig CSRF beveiligingstoken.'], JSON_UNESCAPED_UNICODE); + exit; + } + + $count = fociz_revoke_other_sessions($userId, session_id()); + echo json_encode([ + 'success' => true, + 'revoked_count' => $count, + 'message' => "Succesvol {$count} andere actieve sessie(s) op afstand beëindigd." + ], JSON_UNESCAPED_UNICODE); + exit; +} + +// Beëindig specifieke sessie op afstand +if ($action === 'revoke_session') { + if (!verify_csrf_token($_POST['csrf_token'] ?? null)) { + http_response_code(403); + echo json_encode(['success' => false, 'error' => 'Ongeldig CSRF beveiligingstoken.'], JSON_UNESCAPED_UNICODE); + exit; + } + + $sessionDbId = (int)($_POST['session_id'] ?? 0); + $ok = fociz_revoke_session_by_id($userId, $sessionDbId); + echo json_encode([ + 'success' => $ok, + 'message' => $ok ? 'Sessie succesvol op afstand beëindigd.' : 'Kan sessie niet intrekken.' + ], JSON_UNESCAPED_UNICODE); + exit; +} + http_response_code(400); echo json_encode(['success' => false, 'error' => 'Onbekende sessieactie.'], JSON_UNESCAPED_UNICODE); exit; diff --git a/auth/invite.php b/auth/invite.php index 728991b..fa0bd98 100644 --- a/auth/invite.php +++ b/auth/invite.php @@ -77,15 +77,27 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { fociz_log_audit($newUserId, 'USER_REGISTERED_VIA_INVITE', 'user', $newUserId, ['email' => $invite['email'], 'role' => $invite['role']], 'SUCCESS', $invite['email']); - // Start dagsessie (geldig tot 23:59:59) + // Start dagsessie met verse sessie ID (OWASP ASVS v4.0 §3.3.1 / SOC 2 CC6.6) + if (session_status() === PHP_SESSION_ACTIVE) { + session_regenerate_id(true); + } + $newSessionId = session_id(); + $_SESSION['user_id'] = $newUserId; $_SESSION['user_name'] = $name; $_SESSION['user_email'] = $invite['email']; $_SESSION['user_role'] = $invite['role']; $_SESSION['is_2fa_verified'] = true; $_SESSION['auth_date'] = date('Y-m-d'); + $_SESSION['auth_fingerprint'] = fociz_get_client_fingerprint(); $_SESSION['session_expires_at'] = strtotime('today 23:59:59'); + $_SESSION['last_activity'] = time(); + $_SESSION['login_time'] = time(); + $_SESSION['last_rotated'] = time(); clear_remember_cookie(); + issue_day_session_token($newUserId); + $maxIdleSeconds = function_exists('get_session_idle_timeout') ? get_session_idle_timeout() : 1800; + fociz_register_active_session($newUserId, $newSessionId, $maxIdleSeconds); unset($_SESSION['invite_totp_secret']); header('Location: /planner.php'); diff --git a/auth/login.php b/auth/login.php index 9c158ab..94f8292 100644 --- a/auth/login.php +++ b/auth/login.php @@ -24,6 +24,9 @@ $infoMessage = null; if (isset($_GET['expired'])) { $infoMessage = "Uw sessie is beëindigd wegens inactiviteit of een dagwissel. Log opnieuw in om verder te gaan."; } +if (isset($_GET['revoked'])) { + $infoMessage = "Uw sessie is op afstand beëindigd of ingetrokken. Log opnieuw in om verder te gaan."; +} if (isset($_GET['ip_changed'])) { $infoMessage = "U bent ingelogd vanaf een andere netwerklocatie. Uit veiligheidsoverwegingen dient u opnieuw in te loggen met 2FA."; } @@ -148,6 +151,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { // Volledige login succesvol (geldig tot 23:59:59 van vandaag) fociz_register_user_ip((int)$user['id'], $clientIp, true); + // Session Fixation Beveiliging (OWASP ASVS v4.0 §3.3.1 / SOC 2 CC6.6): + // Genereer direct een nieuw cryptografisch sessie-ID en vernietig de pre-auth sessie + if (session_status() === PHP_SESSION_ACTIVE) { + session_regenerate_id(true); + } + $newSessionId = session_id(); + $_SESSION['user_id'] = $user['id']; $_SESSION['user_name'] = $user['name']; $_SESSION['user_email'] = $user['email']; @@ -163,6 +173,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { // Dagsessie-token uitgeven (strikt geldig tot 23:59:59 van vandaag) issue_day_session_token((int)$user['id']); + // Registreer actieve sessie in user_sessions (SOC 2 / ISO 27001) + $maxIdleSeconds = function_exists('get_session_idle_timeout') ? get_session_idle_timeout() : 1800; + fociz_register_active_session((int)$user['id'], $newSessionId, $maxIdleSeconds); + fociz_log_audit((int)$user['id'], 'LOGIN_SUCCESS', 'user', (int)$user['id'], ['ip' => $clientIp, 'auth_method' => 'PASSWORD+2FA'], 'SUCCESS', $user['email']); unset($_SESSION['pending_2fa_user_id'], $_SESSION['pending_2fa_user_email'], $_SESSION['pending_2fa_user_name']); diff --git a/auth/logout.php b/auth/logout.php index c1ef092..58ba5fc 100644 --- a/auth/logout.php +++ b/auth/logout.php @@ -6,13 +6,17 @@ require_once __DIR__ . '/../includes/auth.php'; start_secure_session(); -// Vernietig persistente login in database +// Vernietig persistente login en sessie in database (SOC 2 CC6.1 / ISO 27001 A.9.4.2) if (!empty($_SESSION['user_id'])) { + $uid = (int)$_SESSION['user_id']; try { $db = get_db_connection(); ensure_remember_token_columns($db); $stmt = $db->prepare("UPDATE users SET remember_token = NULL, remember_token_expires_at = NULL WHERE id = ?"); - $stmt->execute([$_SESSION['user_id']]); + $stmt->execute([$uid]); + + fociz_terminate_session(session_id()); + fociz_log_audit($uid, 'SESSION_TERMINATED_LOGOUT', 'users', $uid, ['ip' => fociz_get_client_ip()], 'SUCCESS', $_SESSION['user_email'] ?? null); } catch (Throwable $e) { // Stil negeren bij uitloggen } diff --git a/auth/setup.php b/auth/setup.php index c0d6daf..3a75171 100644 --- a/auth/setup.php +++ b/auth/setup.php @@ -62,15 +62,27 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $adminId = (int)$db->lastInsertId(); - // Automatische dagsessie starten (geldig tot 23:59:59) + // Automatische dagsessie starten met verse sessie ID (OWASP ASVS v4.0 §3.3.1) + if (session_status() === PHP_SESSION_ACTIVE) { + session_regenerate_id(true); + } + $newSessionId = session_id(); + $_SESSION['user_id'] = $adminId; $_SESSION['user_name'] = $name; $_SESSION['user_email'] = $email; $_SESSION['user_role'] = 'ADMIN'; $_SESSION['is_2fa_verified'] = true; $_SESSION['auth_date'] = date('Y-m-d'); + $_SESSION['auth_fingerprint'] = fociz_get_client_fingerprint(); $_SESSION['session_expires_at'] = strtotime('today 23:59:59'); + $_SESSION['last_activity'] = time(); + $_SESSION['login_time'] = time(); + $_SESSION['last_rotated'] = time(); clear_remember_cookie(); + issue_day_session_token($adminId); + $maxIdleSeconds = function_exists('get_session_idle_timeout') ? get_session_idle_timeout() : 1800; + fociz_register_active_session($adminId, $newSessionId, $maxIdleSeconds); unset($_SESSION['setup_totp_secret']); fociz_log_audit($adminId, 'INITIAL_ADMIN_SETUP', 'user', $adminId, ['email' => $email, 'name' => $name], 'SUCCESS', $email); diff --git a/config/schema.sql b/config/schema.sql index 00d67f8..a164fff 100644 --- a/config/schema.sql +++ b/config/schema.sql @@ -293,3 +293,23 @@ CREATE TABLE IF NOT EXISTS `audit_logs` ( INDEX (`action`), INDEX (`created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Fociz SOC 2 CC6.1 & ISO 27001 A.9.4.2: Actieve Sessietracking & Remote Revocation +CREATE TABLE IF NOT EXISTS `user_sessions` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `user_id` INT NOT NULL, + `session_id_hash` VARCHAR(64) NOT NULL UNIQUE, + `ip_address` VARCHAR(45) NOT NULL, + `user_agent` VARCHAR(255) NULL, + `device_name` VARCHAR(100) NULL, + `is_active` TINYINT(1) NOT NULL DEFAULT 1, + `last_activity_at` DATETIME NOT NULL, + `expires_at` DATETIME NOT NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX (`user_id`), + INDEX (`session_id_hash`), + INDEX (`expires_at`), + INDEX (`is_active`), + CONSTRAINT `fk_user_sessions_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + diff --git a/includes/auth.php b/includes/auth.php index e7ff8b3..7effed8 100644 --- a/includes/auth.php +++ b/includes/auth.php @@ -295,15 +295,23 @@ function fociz_send_new_ip_verification(int $userId, string $email, string $name function start_secure_session(): void { if (session_status() === PHP_SESSION_NONE) { - // Dagsessie: 24 uur (86400s) zodat server-garbage collectors de sessie niet tussentijds wissen - ini_set('session.gc_maxlifetime', '86400'); - ini_set('session.cookie_lifetime', '86400'); + $isSecure = is_https_request(); + + // OWASP ASVS v4.0 §3.4 & SOC 2 CC6.6 Cryptografische Hardening + ini_set('session.sid_length', '48'); + ini_set('session.sid_bits_per_character', '6'); ini_set('session.use_strict_mode', '1'); ini_set('session.use_only_cookies', '1'); + ini_set('session.use_trans_sid', '0'); + 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); - $isSecure = is_https_request(); session_set_cookie_params([ - 'lifetime' => 86400, + 'lifetime' => 0, // Browser sessiecookie: vervalt direct bij afsluiten browser conform ISO 27001 / NIST SP 800-63B 'path' => '/', 'domain' => '', 'secure' => $isSecure, @@ -353,6 +361,230 @@ function ensure_remember_token_columns(PDO $db): void { } } +/** + * SOC 2 & ISO 27001 Actieve Sessietabel + * Garandeert dat de tabel user_sessions aanwezig is voor multi-session tracking en remote revocation + */ +function ensure_sessions_table(PDO $db): void { + static $sessionsTableChecked = false; + if ($sessionsTableChecked) return; + $sessionsTableChecked = true; + + try { + $db->exec(" + CREATE TABLE IF NOT EXISTS `user_sessions` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `user_id` INT NOT NULL, + `session_id_hash` VARCHAR(64) NOT NULL UNIQUE, + `ip_address` VARCHAR(45) NOT NULL, + `user_agent` VARCHAR(255) NULL, + `device_name` VARCHAR(100) NULL, + `is_active` TINYINT(1) NOT NULL DEFAULT 1, + `last_activity_at` DATETIME NOT NULL, + `expires_at` DATETIME NOT NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX (`user_id`), + INDEX (`session_id_hash`), + INDEX (`expires_at`), + INDEX (`is_active`), + CONSTRAINT `fk_user_sessions_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + "); + } catch (Throwable $e) { + error_log("ensure_sessions_table notice: " . $e->getMessage()); + } +} + +/** + * Zet User-Agent om naar een herkenbare apparaatnaam (SOC 2 / ISO 27001 UI) + */ +function fociz_get_device_name(?string $userAgent): string { + if (empty($userAgent)) return 'Onbekend apparaat'; + $ua = $userAgent; + + $platform = 'Onbekend OS'; + if (stripos($ua, 'Windows NT 10.0') !== false) $platform = 'Windows 10/11'; + elseif (stripos($ua, 'Windows') !== false) $platform = 'Windows'; + elseif (stripos($ua, 'Macintosh') !== false || stripos($ua, 'Mac OS X') !== false) $platform = 'macOS'; + elseif (stripos($ua, 'iPhone') !== false) $platform = 'iPhone'; + elseif (stripos($ua, 'iPad') !== false) $platform = 'iPad'; + elseif (stripos($ua, 'Android') !== false) $platform = 'Android'; + elseif (stripos($ua, 'Linux') !== false) $platform = 'Linux'; + + $browser = 'Browser'; + if (stripos($ua, 'Edg/') !== false) $browser = 'Edge'; + elseif (stripos($ua, 'Chrome/') !== false && stripos($ua, 'Chromium') === false) $browser = 'Chrome'; + elseif (stripos($ua, 'Safari/') !== false && stripos($ua, 'Chrome') === false) $browser = 'Safari'; + elseif (stripos($ua, 'Firefox/') !== false) $browser = 'Firefox'; + elseif (stripos($ua, 'Opera') !== false || stripos($ua, 'OPR/') !== false) $browser = 'Opera'; + + return "{$browser} op {$platform}"; +} + +/** + * Registreert of vernieuwt een actieve sessie in user_sessions (SOC 2 CC6.1 / ISO 27001 A.9.4.2) + */ +function fociz_register_active_session(int $userId, string $sessionId, int $maxIdleSeconds): void { + try { + if (empty($sessionId) || $userId <= 0) return; + $db = get_db_connection(); + ensure_sessions_table($db); + + $hash = hash('sha256', $sessionId); + $ip = fociz_get_client_ip(); + $ua = substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 255); + $deviceName = fociz_get_device_name($ua); + $expiresAt = date('Y-m-d H:i:s', time() + $maxIdleSeconds); + + $stmt = $db->prepare(" + INSERT INTO user_sessions (user_id, session_id_hash, ip_address, user_agent, device_name, is_active, last_activity_at, expires_at, created_at) + VALUES (?, ?, ?, ?, ?, 1, NOW(), ?, NOW()) + ON DUPLICATE KEY UPDATE + ip_address = VALUES(ip_address), + user_agent = VALUES(user_agent), + device_name = VALUES(device_name), + is_active = 1, + last_activity_at = NOW(), + expires_at = VALUES(expires_at) + "); + $stmt->execute([$userId, $hash, $ip, $ua, $deviceName, $expiresAt]); + + // Verwijder oude verlopen sessies (> 7 dagen geleden verlopen) + $db->exec("DELETE FROM user_sessions WHERE expires_at < NOW() - INTERVAL 7 DAY"); + } catch (Throwable $e) { + error_log("fociz_register_active_session error: " . $e->getMessage()); + } +} + +/** + * Werkt de activiteitstijd en expiratiedatum bij van een actieve sessie + */ +function fociz_touch_session(string $sessionId, int $maxIdleSeconds): void { + try { + if (empty($sessionId)) return; + $db = get_db_connection(); + $hash = hash('sha256', $sessionId); + $expiresAt = date('Y-m-d H:i:s', time() + $maxIdleSeconds); + $stmt = $db->prepare("UPDATE user_sessions SET last_activity_at = NOW(), expires_at = ? WHERE session_id_hash = ? AND is_active = 1"); + $stmt->execute([$expiresAt, $hash]); + } catch (Throwable $e) { + error_log("fociz_touch_session error: " . $e->getMessage()); + } +} + +/** + * Roteert een sessie-ID in de database (OWASP ASVS v4.0 §3.3) + */ +function fociz_rotate_session(string $oldSessionId, string $newSessionId): void { + try { + if (empty($oldSessionId) || empty($newSessionId) || $oldSessionId === $newSessionId) return; + $db = get_db_connection(); + $oldHash = hash('sha256', $oldSessionId); + $newHash = hash('sha256', $newSessionId); + $stmt = $db->prepare("UPDATE user_sessions SET session_id_hash = ? WHERE session_id_hash = ?"); + $stmt->execute([$newHash, $oldHash]); + } catch (Throwable $e) { + error_log("fociz_rotate_session error: " . $e->getMessage()); + } +} + +/** + * Controleert of de huidige sessie nog actief is (of ingetrokken op afstand) + */ +function fociz_is_session_active(string $sessionId, int $userId): bool { + try { + if (empty($sessionId) || $userId <= 0) return false; + $db = get_db_connection(); + ensure_sessions_table($db); + + $hash = hash('sha256', $sessionId); + $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) + return (!empty($row['is_active'])); + } catch (Throwable $e) { + return true; // Fallback op toegestaan bij databaseverstoring + } +} + +/** + * Beëindigt een sessie in de database (bij uitloggen of time-out) + */ +function fociz_terminate_session(string $sessionId): void { + try { + if (empty($sessionId)) return; + $db = get_db_connection(); + $hash = hash('sha256', $sessionId); + $stmt = $db->prepare("UPDATE user_sessions SET is_active = 0 WHERE session_id_hash = ?"); + $stmt->execute([$hash]); + } catch (Throwable $e) { + error_log("fociz_terminate_session error: " . $e->getMessage()); + } +} + +/** + * Trekt alle overige sessies van een gebruiker in (SOC 2 CC6.1 / ISO 27001 A.9.4.2) + */ +function fociz_revoke_other_sessions(int $userId, string $currentSessionId): int { + try { + $db = get_db_connection(); + ensure_sessions_table($db); + $currentHash = hash('sha256', $currentSessionId); + + $stmt = $db->prepare("UPDATE user_sessions SET is_active = 0 WHERE user_id = ? AND session_id_hash != ? AND is_active = 1"); + $stmt->execute([$userId, $currentHash]); + $revokedCount = $stmt->rowCount(); + + // Invalideer ook het dagsessietoken in de database zodat herstel op andere browsers direct stopt + $stmtClear = $db->prepare("UPDATE users SET remember_token = NULL, remember_token_expires_at = NULL WHERE id = ?"); + $stmtClear->execute([$userId]); + + fociz_log_audit($userId, 'REVOKE_OTHER_SESSIONS', 'user_sessions', $userId, ['revoked_count' => $revokedCount], 'SUCCESS'); + return $revokedCount; + } catch (Throwable $e) { + error_log("fociz_revoke_other_sessions error: " . $e->getMessage()); + return 0; + } +} + +/** + * Trekt een specifieke sessie in op basis van ID + */ +function fociz_revoke_session_by_id(int $userId, int $sessionDbId): bool { + try { + $db = get_db_connection(); + $stmt = $db->prepare("UPDATE user_sessions SET is_active = 0 WHERE id = ? AND user_id = ?"); + $stmt->execute([$sessionDbId, $userId]); + fociz_log_audit($userId, 'SESSION_TERMINATED_REVOKED', 'user_sessions', $sessionDbId, ['session_db_id' => $sessionDbId], 'SUCCESS'); + return true; + } catch (Throwable $e) { + return false; + } +} + +/** + * Haalt alle actieve sessies van een gebruiker op voor de profielpagina + */ +function fociz_get_user_active_sessions(int $userId, string $currentSessionId): array { + try { + $db = get_db_connection(); + ensure_sessions_table($db); + $currentHash = hash('sha256', $currentSessionId); + + $stmt = $db->prepare(" + SELECT id, ip_address, user_agent, device_name, last_activity_at, created_at, (session_id_hash = ?) AS is_current + FROM user_sessions + WHERE user_id = ? AND is_active = 1 + ORDER BY is_current DESC, last_activity_at DESC + "); + $stmt->execute([$currentHash, $userId]); + return $stmt->fetchAll(PDO::FETCH_ASSOC) ?: []; + } catch (Throwable $e) { + return []; + } +} + /** * Garandeert dat de kolom voor notificatie-instellingen aanwezig is in MariaDB */ @@ -621,10 +853,22 @@ 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'])) { + // Controleer of sessie op afstand is ingetrokken (SOC 2 CC6.1 / ISO 27001 A.9.4.2) + if (!fociz_is_session_active(session_id(), (int)$_SESSION['user_id'])) { + fociz_log_audit((int)$_SESSION['user_id'], 'SESSION_TERMINATED_REVOKED', 'user_sessions', (int)$_SESSION['user_id'], ['reason' => 'remote_revocation'], 'WARNING'); + $_SESSION = ['session_was_expired' => true, 'session_revoked' => true]; + clear_day_session_cookie(); + if (session_status() === PHP_SESSION_ACTIVE) { + @session_regenerate_id(true); + } + return null; + } + // 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'], 'max_idle' => $maxIdleSeconds], 'WARNING'); + fociz_terminate_session(session_id()); // 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']]); @@ -640,6 +884,7 @@ function get_logged_in_user(): ?array { $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'); + fociz_terminate_session(session_id()); $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]; @@ -668,8 +913,10 @@ function get_logged_in_user(): ?array { if (empty($_SESSION['last_rotated'])) { $_SESSION['last_rotated'] = $now; } elseif (($now - (int)$_SESSION['last_rotated']) > 1800) { + $oldSid = session_id(); if (session_status() === PHP_SESSION_ACTIVE) { @session_regenerate_id(true); + fociz_rotate_session($oldSid, session_id()); } $_SESSION['last_rotated'] = $now; } @@ -690,6 +937,7 @@ function get_logged_in_user(): ?array { try { $stmtAct = $db->prepare("UPDATE users SET last_activity_at = NOW() WHERE id = ?"); $stmtAct->execute([$_SESSION['user_id']]); + fociz_touch_session(session_id(), $maxIdleSeconds); } catch (Throwable $e) {} } } @@ -748,6 +996,8 @@ function get_logged_in_user(): ?array { $stmtAct->execute([$user['id']]); } catch (Throwable $e) {} + fociz_register_active_session((int)$user['id'], session_id(), $maxIdleSeconds); + $user['avatar_url'] = fociz_get_valid_avatar_url($user['avatar_url'] ?? null, (int)$user['id']); $_SESSION['user_avatar_url'] = $user['avatar_url']; return $user; @@ -796,9 +1046,12 @@ function require_auth(bool $json = false): array { } $wasExpired = !empty($_SESSION['session_was_expired']); $ipChanged = !empty($_SESSION['ip_changed']); - unset($_SESSION['session_was_expired'], $_SESSION['ip_changed']); + $wasRevoked = !empty($_SESSION['session_revoked']); + unset($_SESSION['session_was_expired'], $_SESSION['ip_changed'], $_SESSION['session_revoked']); if ($ipChanged) { header('Location: /auth/login.php?ip_changed=1'); + } elseif ($wasRevoked) { + header('Location: /auth/login.php?revoked=1'); } elseif ($wasExpired) { header('Location: /auth/login.php?expired=1'); } else { diff --git a/profile.php b/profile.php index dc7cdda..7ab2eeb 100644 --- a/profile.php +++ b/profile.php @@ -223,16 +223,26 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } elseif ($newPassword !== $confirmPassword) { $message = "Het nieuwe wachtwoord en de bevestiging komen niet overeen."; $messageType = 'error'; - } else { $newHash = password_hash($newPassword, PASSWORD_DEFAULT); $stmtUp = $db->prepare("UPDATE users SET password_hash = ? WHERE id = ?"); $stmtUp->execute([$newHash, $userId]); + + // SOC 2 CC6.1 / ISO 27001 A.9.4.2: Trek overige sessies in bij wachtwoordwijziging + fociz_revoke_other_sessions($userId, session_id()); + fociz_log_audit($userId, 'PASSWORD_CHANGED', 'user', $userId, ['method' => 'profile'], 'SUCCESS', $user['email']); - $message = "Uw wachtwoord is succesvol gewijzigd."; + $message = "Uw wachtwoord is succesvol gewijzigd. Alle overige sessies op andere apparaten zijn uit veiligheidsoverwegingen beëindigd."; $messageType = 'success'; } } + // 7b. Alle overige sessies op afstand beëindigen (SOC 2 / ISO 27001) + elseif ($action === 'revoke_other_sessions') { + $count = fociz_revoke_other_sessions($userId, session_id()); + $message = "Succesvol {$count} andere actieve sessie(s) op afstand beëindigd."; + $messageType = 'success'; + } + // 8. Standaard avatar kiezen uit bibliotheek elseif ($action === 'select_preset_avatar') { $preset = trim($_POST['avatar_preset'] ?? ''); @@ -331,6 +341,9 @@ $userAiRowSettings = json_decode($userAiIntRow['settings'] ?? '{}', true) ?: []; // Thema ophalen $userTheme = get_user_theme($userId); +// Actieve apparaten & sessies ophalen (SOC 2 / ISO 27001) +$activeSessions = fociz_get_user_active_sessions($userId, session_id()); + $pageTitle = 'Mijn Profiel & Koppelingen — Fociz'; require_once __DIR__ . '/includes/header.php'; ?> @@ -1255,6 +1268,72 @@ require_once __DIR__ . '/includes/header.php'; + + +
+
+
+
+ +
+
+
+

Actieve Apparaten & Sessies

+ + + +
+

+ Centraal overzicht van alle actieve inlogapparaten conform ISO 27001 en SOC 2 richtlijnen. +

+
+
+ + 1): ?> +
+ + + +
+ +
+ +
+ +
+
+
+ +
+
+
+ + + + Dit apparaat (Huidig) + + +
+
+ + • + Laatst actief: +
+
+
+ + + + +
+ +
+
diff --git a/users.php b/users.php index bb3ddf0..0832db3 100644 --- a/users.php +++ b/users.php @@ -81,6 +81,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $stmtDelUser = $db->prepare("DELETE FROM users WHERE id = ?"); $stmtDelUser->execute([$delUserId]); + try { + $db->prepare("DELETE FROM user_sessions WHERE user_id = ?")->execute([$delUserId]); + } catch (Throwable $e) {} fociz_log_audit((int)$adminUser['id'], 'USER_DELETED', 'user', $delUserId, ['deleted_email' => $delUser['email'] ?? null, 'deleted_name' => $delUser['name'] ?? null], 'SUCCESS', $adminUser['email']); }