diff --git a/auth/login.php b/auth/login.php index 078d228..c96a580 100644 --- a/auth/login.php +++ b/auth/login.php @@ -49,11 +49,40 @@ $step = isset($_SESSION['pending_2fa_user_id']) ? 2 : 1; // Terug naar stap 1 if (isset($_GET['action']) && $_GET['action'] === 'restart') { - unset($_SESSION['pending_2fa_user_id'], $_SESSION['pending_2fa_user_email'], $_SESSION['pending_2fa_user_name'], $_SESSION['pending_new_ip']); + unset( + $_SESSION['pending_2fa_user_id'], + $_SESSION['pending_2fa_user_email'], + $_SESSION['pending_2fa_user_name'], + $_SESSION['pending_new_ip'], + $_SESSION['pending_new_ip_mail_sent'] + ); header('Location: /auth/login.php'); exit; } +// Verificatiemail opnieuw verzenden bij nieuw IP +if (isset($_GET['action']) && $_GET['action'] === 'resend_ip_mail' && !empty($_SESSION['pending_2fa_user_id']) && !empty($_SESSION['pending_new_ip'])) { + $resendUserId = (int)$_SESSION['pending_2fa_user_id']; + $resendRateKey = "resend_ip_mail_{$resendUserId}"; + $resendLimit = fociz_check_rate_limit($resendRateKey, 2, 60); + + if (!$resendLimit['allowed']) { + $error = "Wacht even voordat u opnieuw een e-mail aanvraagt (over " . ceil($resendLimit['retry_after']) . " seconden)."; + } else { + fociz_hit_rate_limit($resendRateKey, 60); + $resendEmail = $_SESSION['pending_2fa_user_email'] ?? ''; + $resendName = $_SESSION['pending_2fa_user_name'] ?? ''; + $resendIp = $_SESSION['pending_new_ip']; + $sent = fociz_send_new_ip_verification($resendUserId, $resendEmail, $resendName, $resendIp); + if ($sent) { + $infoMessage = "Er is een nieuwe verificatielink verstuurd naar " . htmlspecialchars($resendEmail) . "."; + $_SESSION['pending_new_ip_mail_sent'] = true; + } else { + $error = "De mailserver kon de e-mail niet direct afleveren. U kunt echter direct hieronder inloggen met uw 2FA Authenticator code om deze locatie te autoriseren."; + } + } +} + // POST Verwerking if ($_SERVER['REQUEST_METHOD'] === 'POST') { $db = get_db_connection(); @@ -87,15 +116,24 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $isIpKnown = fociz_is_ip_known_for_user((int)$user['id'], $clientIp); if (!$isIpKnown) { - // Stuur verificatie-mail en informeer gebruiker - fociz_send_new_ip_verification((int)$user['id'], $user['email'], $user['name'], $clientIp); - fociz_log_audit((int)$user['id'], 'LOGIN_NEW_IP_CHALLENGE', 'user', (int)$user['id'], ['ip' => $clientIp], 'PENDING', $user['email']); - $error = "Nieuwe inloglocatie gedetecteerd ({$clientIp}). Er is een verificatie-e-mail gestuurd naar {$user['email']}. Klik op de link in de e-mail om deze locatie toe te staan."; - } else { - // Succesvol stap 1: zet tijdelijke pending sessie voor 2FA + // Stuur verificatie-mail en log audit challenge + $mailSent = fociz_send_new_ip_verification((int)$user['id'], $user['email'], $user['name'], $clientIp); + fociz_log_audit((int)$user['id'], 'LOGIN_NEW_IP_CHALLENGE', 'user', (int)$user['id'], ['ip' => $clientIp, 'mail_sent' => $mailSent], 'PENDING', $user['email']); + + // Laat legitieme gebruiker doorgaan naar Stap 2 (2FA TOTP). + // Conform NIST SP 800-63B autoriseert de fysieke Authenticator code de nieuwe locatie. $_SESSION['pending_2fa_user_id'] = $user['id']; $_SESSION['pending_2fa_user_email'] = $user['email']; $_SESSION['pending_2fa_user_name'] = $user['name']; + $_SESSION['pending_new_ip'] = $clientIp; + $_SESSION['pending_new_ip_mail_sent'] = $mailSent; + $step = 2; + } else { + // Bekende locatie: direct door naar 2FA + $_SESSION['pending_2fa_user_id'] = $user['id']; + $_SESSION['pending_2fa_user_email'] = $user['email']; + $_SESSION['pending_2fa_user_name'] = $user['name']; + unset($_SESSION['pending_new_ip'], $_SESSION['pending_new_ip_mail_sent']); $step = 2; } } else { @@ -161,8 +199,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { fociz_clear_rate_limit($ipRateKey); // Volledige login succesvol (geldig tot 23:59:59 van vandaag) + // Autoriseert het IP-adres definitief in user_known_ips fociz_register_user_ip((int)$user['id'], $clientIp, true); + $wasNewIp = !empty($_SESSION['pending_new_ip']); + if ($wasNewIp) { + fociz_log_audit((int)$user['id'], 'LOGIN_NEW_IP_VERIFIED_VIA_2FA', 'user_known_ips', (int)$user['id'], ['ip' => $clientIp], 'SUCCESS', $user['email']); + } + // 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) { @@ -189,9 +233,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $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']); + fociz_log_audit((int)$user['id'], 'LOGIN_SUCCESS', 'user', (int)$user['id'], ['ip' => $clientIp, 'auth_method' => 'PASSWORD+2FA', 'was_new_ip' => $wasNewIp], 'SUCCESS', $user['email']); - unset($_SESSION['pending_2fa_user_id'], $_SESSION['pending_2fa_user_email'], $_SESSION['pending_2fa_user_name']); + unset( + $_SESSION['pending_2fa_user_id'], + $_SESSION['pending_2fa_user_email'], + $_SESSION['pending_2fa_user_name'], + $_SESSION['pending_new_ip'], + $_SESSION['pending_new_ip_mail_sent'] + ); header('Location: /planner.php'); exit; @@ -315,6 +365,29 @@ $pageTitle = "Fociz — Inloggen";
+ + +
+
+ + Nieuwe inloglocatie gedetecteerd +
+

+ U logt in vanaf een nieuw netwerkadres (). + Voer uw 2FA Authenticator code in om uw identiteit te bevestigen en deze locatie automatisch te autoriseren. +

+ +
+ +
diff --git a/includes/auth.php b/includes/auth.php index 8d1635e..c3768d2 100644 --- a/includes/auth.php +++ b/includes/auth.php @@ -291,6 +291,9 @@ function fociz_send_new_ip_verification(int $userId, string $email, string $name ]); $res = FocizMailer::send($email, $name, "Bevestig nieuwe inloglocatie ({$ip}) — Fociz", $html); + if (empty($res['success'])) { + error_log("fociz_send_new_ip_verification: Mail verzenden mislukt naar {$email}: " . ($res['error'] ?? 'onbekende fout')); + } return !empty($res['success']); } catch (Throwable $e) { error_log("fociz_send_new_ip_verification error: " . $e->getMessage()); diff --git a/includes/mailer.php b/includes/mailer.php index b7a52dc..eae5388 100644 --- a/includes/mailer.php +++ b/includes/mailer.php @@ -131,15 +131,58 @@ class FocizMailer { throw new Exception("Ongeldige server banner (verwacht 220, kreeg {$resp['code']}): {$resp['text']}"); } - // 4. EHLO Handshake - $clientHost = !empty($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'fociz.nl'; - $resp = $sendCommand("EHLO {$clientHost}"); - if ($resp['code'] !== 250) { - $resp = $sendCommand("HELO {$clientHost}"); - if ($resp['code'] !== 250) { - throw new Exception("EHLO/HELO geweigerd door server: {$resp['text']}"); + // 4. EHLO Handshake conform RFC 5321 §4.1.4 + // Voorkom impersonation van het lokale domein (bijv. 'fociz.nl' tegen 'mail.fociz.nl') + // Antagonist/Exim weigert 'EHLO fociz.nl' van externe hosts met '550 Bad HELO - Host impersonating domain name' + $resolveHeloHost = function() use ($host): string { + // 1. Mailserver hostnaam indien beschikbaar en niet een lokaal IP + if (!empty($host) && str_contains($host, '.') && !str_starts_with($host, '192.168.') && !str_starts_with($host, '127.')) { + return $host; } - } + // 2. SERVER_NAME mits het niet het bare hoofddomein is + if (!empty($_SERVER['SERVER_NAME'])) { + $sn = preg_replace('/:[0-9]+$/', '', trim($_SERVER['SERVER_NAME'])); + if (!in_array(strtolower($sn), ['fociz.nl', 'localhost', '127.0.0.1'])) { + return $sn; + } + } + // 3. Systeemhostnaam + if (function_exists('gethostname')) { + $gh = gethostname(); + if (!empty($gh) && strtolower($gh) !== 'fociz.nl') { + return $gh; + } + } + return 'mail.fociz.nl'; + }; + + $performHeloHandshake = function(string $preferredHost) use ($sendCommand, $log): string { + $candidates = array_unique([$preferredHost, 'mail.fociz.nl', 'localhost', '127.0.0.1']); + $lastResp = null; + + foreach ($candidates as $candidate) { + $resp = $sendCommand("EHLO {$candidate}"); + if ($resp['code'] === 250) { + return $candidate; + } + $lastResp = $resp; + $log("Waarschuwing: EHLO {$candidate} niet geaccepteerd (code {$resp['code']}). Probeert alternatief..."); + } + + // Fallback naar HELO indien EHLO op alle kandidaten niet werkt + foreach ($candidates as $candidate) { + $resp = $sendCommand("HELO {$candidate}"); + if ($resp['code'] === 250) { + return $candidate; + } + $lastResp = $resp; + } + + throw new Exception("EHLO/HELO geweigerd door server voor alle kandidaten. Laatste reactie: " . ($lastResp ? "({$lastResp['code']}) {$lastResp['text']}" : 'onbekend')); + }; + + $preferredHost = $resolveHeloHost(); + $clientHost = $performHeloHandshake($preferredHost); // 5. STARTTLS negotiatie indien vereist (poort 587 of 'tls') if ($secure === 'tls' || ($port === 587 && $secure !== 'none')) { @@ -165,10 +208,7 @@ class FocizMailer { $log("TLS encryptie succesvol geactiveerd."); // Na STARTTLS moet EHLO opnieuw worden gestuurd (RFC 3207) - $resp = $sendCommand("EHLO {$clientHost}"); - if ($resp['code'] !== 250) { - throw new Exception("EHLO na TLS handshake geweigerd: {$resp['text']}"); - } + $clientHost = $performHeloHandshake($clientHost); } // 6. SMTP Authenticatie