Enhance session management and user authentication flow; implement day session tokens, improve cookie handling, and streamline date handling in planner

This commit is contained in:
2026-09-09 19:18:42 +02:00
parent 2062fd065a
commit 963342f858
5 changed files with 146 additions and 89 deletions
+97 -55
View File
@@ -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');
+9 -17
View File
@@ -61,33 +61,25 @@ $currentUserTheme = !empty($currentUser['id']) ? get_user_theme((int)$currentUse
<script>
(function() {
try {
const sessionDate = "<?= date('Y-m-d') ?>";
const checkMidnight = function() {
const now = new Date();
const midnight = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999);
const todayStr = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0') + '-' + String(now.getDate()).padStart(2, '0');
if (todayStr !== sessionDate || now.getTime() > midnight.getTime()) {
const sessionExpiresAt = <?= strtotime('today 23:59:59') ?> * 1000;
const checkExpiry = function() {
if (Date.now() > sessionExpiresAt + 1000) {
window.location.href = '/auth/login.php?expired=1';
}
};
const now = new Date();
const midnight = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999);
const msUntilMidnight = midnight.getTime() - now.getTime();
if (msUntilMidnight > 0) {
setTimeout(function() {
window.location.href = '/auth/login.php?expired=1';
}, msUntilMidnight);
const msUntilExpiry = sessionExpiresAt - Date.now();
if (msUntilExpiry > 0) {
setTimeout(checkExpiry, msUntilExpiry + 500);
}
// Controleer direct zodra de gebruiker de browser weer activeert (bij bijv. standby / slaapstand overnacht)
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'visible') {
checkMidnight();
checkExpiry();
}
});
window.addEventListener('focus', checkMidnight);
setInterval(checkMidnight, 30000);
window.addEventListener('focus', checkExpiry);
setInterval(checkExpiry, 60000);
} catch(e) {}
})();
</script>