From 963342f858ac367bc70948f7d7b6378e431ba3b4 Mon Sep 17 00:00:00 2001 From: Niek Rabelink Date: Wed, 9 Sep 2026 19:18:42 +0200 Subject: [PATCH] Enhance session management and user authentication flow; implement day session tokens, improve cookie handling, and streamline date handling in planner --- assets/js/notifications.js | 2 +- auth/login.php | 5 +- includes/auth.php | 152 +++++++++++++++++++++++-------------- includes/header.php | 26 +++---- planner.php | 50 ++++++++---- 5 files changed, 146 insertions(+), 89 deletions(-) diff --git a/assets/js/notifications.js b/assets/js/notifications.js index 884c8ac..cc41147 100644 --- a/assets/js/notifications.js +++ b/assets/js/notifications.js @@ -255,7 +255,7 @@ try { const res = await fetch('/api/notifications.php?action=get_events', { cache: 'no-store' }); if (res.status === 401) { - window.location.href = '/auth/login.php?expired=1'; + console.warn('Notificatiesessie niet geauthenticeerd.'); return; } if (res.ok) { diff --git a/auth/login.php b/auth/login.php index 8a5b0e1..eb8282b 100644 --- a/auth/login.php +++ b/auth/login.php @@ -112,9 +112,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $_SESSION['is_2fa_verified'] = true; $_SESSION['auth_date'] = date('Y-m-d'); $_SESSION['session_expires_at'] = strtotime('today 23:59:59'); + $_SESSION['last_activity'] = time(); - // Zorg dat er geen persistente remember cookies worden gebruikt - clear_remember_cookie(); + // Dagsessie-token uitgeven (strikt geldig tot 23:59:59 van vandaag) + issue_day_session_token((int)$user['id']); unset($_SESSION['pending_2fa_user_id'], $_SESSION['pending_2fa_user_email'], $_SESSION['pending_2fa_user_name']); diff --git a/includes/auth.php b/includes/auth.php index ee6b905..c11738d 100644 --- a/includes/auth.php +++ b/includes/auth.php @@ -35,17 +35,15 @@ function is_https_request(): bool { function start_secure_session(): void { if (session_status() === PHP_SESSION_NONE) { - // Dagsessie: verloopt iedere nacht om 23:59:59 (blijft niet meerdere dagen staan) - $secondsUntilMidnight = max(60, strtotime('today 23:59:59') - time()); - - ini_set('session.gc_maxlifetime', (string)$secondsUntilMidnight); - ini_set('session.cookie_lifetime', (string)$secondsUntilMidnight); + // 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'); ini_set('session.use_strict_mode', '1'); ini_set('session.use_only_cookies', '1'); $isSecure = is_https_request(); session_set_cookie_params([ - 'lifetime' => $secondsUntilMidnight, // Verloopt vannacht om 23:59:59 + 'lifetime' => 86400, 'path' => '/', 'domain' => '', 'secure' => $isSecure, @@ -82,8 +80,8 @@ function ensure_remember_token_columns(PDO $db): void { if (!$result || $result->rowCount() === 0) { $db->exec("ALTER TABLE `users` ADD COLUMN `remember_token` VARCHAR(64) NULL AFTER `backup_codes`, ADD COLUMN `remember_token_expires_at` DATETIME NULL AFTER `remember_token`"); } else { - // Maak alle bestaande actieve tokens direct leeg zodat geen enkele oude persistente browser-sessie blijft bestaan - $db->exec("UPDATE `users` SET `remember_token` = NULL, `remember_token_expires_at` = NULL WHERE `remember_token` IS NOT NULL"); + // Schoon uitsluitend verlopen tokens van gisteren op, wis NOOIT actieve tokens van vandaag + $db->exec("UPDATE `users` SET `remember_token` = NULL, `remember_token_expires_at` = NULL WHERE `remember_token_expires_at` IS NOT NULL AND `remember_token_expires_at` < NOW()"); } } catch (Throwable $e) { // Stil negeren indien al aanwezig of tijdens migraties @@ -127,42 +125,70 @@ function ensure_theme_column(PDO $db): void { } /** - * Verouderde functie voor persistent login. - * Bewust geneutraliseerd: cookies worden direct gewist en geen tokens meer aangemaakt, - * zodat gebruikers iedere dag om 23:59:59 worden uitgelogd en daadwerkelijk opnieuw moeten inloggen. + * Genereert een cryptografisch veilig dagsessie-token dat strikt geldig is tot 23:59:59 van vandaag. + * Voorkomt dat gebruikers tussentijds worden uitgelogd bij idle timeouts of browserherstart. */ -function issue_remember_token(int $userId): string { - clear_remember_cookie(); +function issue_day_session_token(int $userId): string { + clear_day_session_cookie(); try { + $token = bin2hex(random_bytes(32)); + $expiresAt = date('Y-m-d 23:59:59'); + $secondsUntilMidnight = max(60, strtotime('today 23:59:59') - time()); + $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([$userId]); - } catch (Throwable $e) {} - return ''; -} + $stmt = $db->prepare("UPDATE users SET remember_token = ?, remember_token_expires_at = ? WHERE id = ?"); + $stmt->execute([$token, $expiresAt, $userId]); -/** - * Verwijdert de persistente remember cookie uit de browser - */ -function clear_remember_cookie(): void { - $isSecure = is_https_request(); - setcookie('fociz_remember', '', [ - 'expires' => time() - 86400, - 'path' => '/', - 'secure' => $isSecure, - 'httponly' => true, - 'samesite' => 'Lax' - ]); - if (isset($_COOKIE['fociz_remember'])) { - unset($_COOKIE['fociz_remember']); + $isSecure = is_https_request(); + setcookie('fociz_day_session', $token, [ + 'expires' => time() + $secondsUntilMidnight, + 'path' => '/', + 'secure' => $isSecure, + 'httponly' => true, + 'samesite' => 'Lax' + ]); + + return $token; + } catch (Throwable $e) { + error_log("Fout bij aanmaken dagsessie token: " . $e->getMessage()); + return ''; } } +/** + * Backward compatibility helper + */ +function issue_remember_token(int $userId): string { + return issue_day_session_token($userId); +} + +/** + * Verwijdert de dagsessie cookies uit de browser + */ +function clear_day_session_cookie(): void { + $isSecure = is_https_request(); + foreach (['fociz_day_session', 'fociz_remember'] as $cookieName) { + setcookie($cookieName, '', [ + 'expires' => time() - 86400, + 'path' => '/', + 'secure' => $isSecure, + 'httponly' => true, + 'samesite' => 'Lax' + ]); + if (isset($_COOKIE[$cookieName])) { + unset($_COOKIE[$cookieName]); + } + } +} + +function clear_remember_cookie(): void { + clear_day_session_cookie(); +} + /** * Haalt de huidige ingelogde gebruiker op uit de database. - * Garandeert dat de sessie strikt om 23:59:59 gereset wordt en nooit meerdere dagen blijft staan. - * Bevat GEEN persistente cookies of auto-login bypass: de gebruiker moet iedere dag daadwerkelijk opnieuw inloggen. + * Garandeert dat de sessie strikt tot 23:59:59 van vandaag actief blijft en morgen gegarandeerd verloopt. */ function get_logged_in_user(): ?array { start_secure_session(); @@ -170,44 +196,56 @@ function get_logged_in_user(): ?array { $today = date('Y-m-d'); $now = time(); - // Verwijder altijd eventuele oude of achtergebleven fociz_remember cookies - if (isset($_COOKIE['fociz_remember'])) { - clear_remember_cookie(); - } - - // Controleer of de sessie van een eerdere dag is of voorbij 23:59:59 - $isExpired = false; + // 1. Controleer of de huidige sessie van een eerdere dag is (dagwissel om middernacht) if (!empty($_SESSION['auth_date']) && $_SESSION['auth_date'] !== $today) { - $isExpired = true; - } - if (!empty($_SESSION['session_expires_at']) && $now > (int)$_SESSION['session_expires_at']) { - $isExpired = true; - } - - if ($isExpired) { $_SESSION = ['session_was_expired' => true]; - clear_remember_cookie(); + clear_day_session_cookie(); if (session_status() === PHP_SESSION_ACTIVE) { @session_regenerate_id(true); } return null; } - // Controleer actieve PHP sessie (uitsluitend de sessie van vandaag is geldig) + $db = get_db_connection(); + ensure_remember_token_columns($db); + + // 2. Controleer actieve PHP sessie (mits behorend bij vandaag) if (!empty($_SESSION['user_id']) && !empty($_SESSION['is_2fa_verified'])) { - $db = get_db_connection(); - ensure_remember_token_columns($db); $stmt = $db->prepare("SELECT id, name, email, role, two_factor_enabled, created_at FROM users WHERE id = ?"); $stmt->execute([$_SESSION['user_id']]); $user = $stmt->fetch(); if ($user) { $_SESSION['auth_date'] = $today; $_SESSION['session_expires_at'] = strtotime('today 23:59:59'); + $_SESSION['last_activity'] = $now; return $user; } - // Gebruiker bestaat niet meer in database + // Gebruiker bestaat niet meer $_SESSION = []; - clear_remember_cookie(); + clear_day_session_cookie(); + return null; + } + + // 3. Fallback dagsessie-token: herstelt sessie na browserherstart of server idle timeout (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, created_at FROM users WHERE remember_token = ? AND remember_token_expires_at >= NOW() AND DATE(remember_token_expires_at) = ?"); + $stmt->execute([$dayToken, $today]); + $user = $stmt->fetch(); + if ($user) { + // Herstel sessie voor vandaag + $_SESSION['user_id'] = $user['id']; + $_SESSION['user_name'] = $user['name']; + $_SESSION['user_email'] = $user['email']; + $_SESSION['user_role'] = $user['role']; + $_SESSION['is_2fa_verified'] = true; + $_SESSION['auth_date'] = $today; + $_SESSION['session_expires_at'] = strtotime('today 23:59:59'); + $_SESSION['last_activity'] = $now; + return $user; + } + // Token is ongeldig of van gisteren + clear_day_session_cookie(); } return null; @@ -219,7 +257,11 @@ function get_logged_in_user(): ?array { function require_auth(bool $json = false): array { start_secure_session(); - $isAjax = $json || + $requestUri = $_SERVER['REQUEST_URI'] ?? ''; + $scriptName = $_SERVER['SCRIPT_NAME'] ?? ''; + $isApiCall = str_contains($requestUri, '/api/') || str_contains($scriptName, '/api/'); + + $isAjax = $json || $isApiCall || (isset($_SERVER['HTTP_ACCEPT']) && str_contains($_SERVER['HTTP_ACCEPT'], 'application/json')) || (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'); diff --git a/includes/header.php b/includes/header.php index f1155e1..0ecaa22 100644 --- a/includes/header.php +++ b/includes/header.php @@ -61,33 +61,25 @@ $currentUserTheme = !empty($currentUser['id']) ? get_user_theme((int)$currentUse diff --git a/planner.php b/planner.php index 407dba2..d869415 100644 --- a/planner.php +++ b/planner.php @@ -21,6 +21,13 @@ require_once __DIR__ . '/includes/integrations/jira.php'; $db = get_db_connection(); $userId = (int)($currentUser['id'] ?? 0); $today = date('Y-m-d'); + +// Indien de datum van vandaag via de URL is meegegeven bij een normale pagina-aanroep, schoon de URL op naar zuiver 'planner.php' +if (!$isAjaxRequest && isset($_GET['date']) && $_GET['date'] === $today) { + header('Location: planner.php', true, 302); + exit; +} + $selectedDate = (!empty($_GET['date']) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $_GET['date'])) ? $_GET['date'] : $today; $isTodaySelected = ($selectedDate === $today); $selectedTs = strtotime($selectedDate); @@ -298,13 +305,16 @@ for ($h = $startHour; $h <= $endHour; $h++) { $hours[] = sprintf('%02d:00', $h); } ?> +
- + + +
@@ -392,16 +402,16 @@ for ($h = $startHour; $h <= $endHour; $h++) {
- + - + Vandaag - + @@ -424,7 +434,7 @@ for ($h = $startHour; $h <= $endHour; $h++) { type="date" id="jumpDateInput" value="" - onchange="if(this.value) switchPlannerDate(this.value, event);" + onchange="if(this.value) { switchPlannerDate(this.value, event); }" class="bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-1.5 text-xs text-slate-800 dark:text-slate-200 focus:outline-none focus:ring-2 focus:ring-indigo-500/30 font-medium cursor-pointer shadow-2xs" >
@@ -434,8 +444,8 @@ for ($h = $startHour; $h <= $endHour; $h++) { +