Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 660f196c60 | |||
| 32974cdb31 |
+82
-9
@@ -49,11 +49,40 @@ $step = isset($_SESSION['pending_2fa_user_id']) ? 2 : 1;
|
|||||||
|
|
||||||
// Terug naar stap 1
|
// Terug naar stap 1
|
||||||
if (isset($_GET['action']) && $_GET['action'] === 'restart') {
|
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');
|
header('Location: /auth/login.php');
|
||||||
exit;
|
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
|
// POST Verwerking
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
$db = get_db_connection();
|
$db = get_db_connection();
|
||||||
@@ -87,15 +116,24 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$isIpKnown = fociz_is_ip_known_for_user((int)$user['id'], $clientIp);
|
$isIpKnown = fociz_is_ip_known_for_user((int)$user['id'], $clientIp);
|
||||||
|
|
||||||
if (!$isIpKnown) {
|
if (!$isIpKnown) {
|
||||||
// Stuur verificatie-mail en informeer gebruiker
|
// Stuur verificatie-mail en log audit challenge
|
||||||
fociz_send_new_ip_verification((int)$user['id'], $user['email'], $user['name'], $clientIp);
|
$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], 'PENDING', $user['email']);
|
fociz_log_audit((int)$user['id'], 'LOGIN_NEW_IP_CHALLENGE', 'user', (int)$user['id'], ['ip' => $clientIp, 'mail_sent' => $mailSent], '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 {
|
// Laat legitieme gebruiker doorgaan naar Stap 2 (2FA TOTP).
|
||||||
// Succesvol stap 1: zet tijdelijke pending sessie voor 2FA
|
// Conform NIST SP 800-63B autoriseert de fysieke Authenticator code de nieuwe locatie.
|
||||||
$_SESSION['pending_2fa_user_id'] = $user['id'];
|
$_SESSION['pending_2fa_user_id'] = $user['id'];
|
||||||
$_SESSION['pending_2fa_user_email'] = $user['email'];
|
$_SESSION['pending_2fa_user_email'] = $user['email'];
|
||||||
$_SESSION['pending_2fa_user_name'] = $user['name'];
|
$_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;
|
$step = 2;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -161,8 +199,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
fociz_clear_rate_limit($ipRateKey);
|
fociz_clear_rate_limit($ipRateKey);
|
||||||
|
|
||||||
// Volledige login succesvol (geldig tot 23:59:59 van vandaag)
|
// 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);
|
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):
|
// 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
|
// Genereer direct een nieuw cryptografisch sessie-ID en vernietig de pre-auth sessie
|
||||||
if (session_status() === PHP_SESSION_ACTIVE) {
|
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;
|
$maxIdleSeconds = function_exists('get_session_idle_timeout') ? get_session_idle_timeout() : 1800;
|
||||||
fociz_register_active_session((int)$user['id'], $newSessionId, $maxIdleSeconds);
|
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');
|
header('Location: /planner.php');
|
||||||
exit;
|
exit;
|
||||||
@@ -315,6 +365,29 @@ $pageTitle = "Fociz — Inloggen";
|
|||||||
<!-- STAP 2: 2FA TOTP Code -->
|
<!-- STAP 2: 2FA TOTP Code -->
|
||||||
<form method="POST" action="/auth/login.php" class="space-y-5">
|
<form method="POST" action="/auth/login.php" class="space-y-5">
|
||||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
|
||||||
|
|
||||||
|
<?php if (!empty($_SESSION['pending_new_ip'])): ?>
|
||||||
|
<div class="p-3.5 rounded-xl bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-800/60 text-amber-900 dark:text-amber-200 text-xs space-y-2 text-left">
|
||||||
|
<div class="flex items-center space-x-2 font-bold text-amber-800 dark:text-amber-300">
|
||||||
|
<i data-lucide="shield-alert" class="w-4 h-4 flex-shrink-0"></i>
|
||||||
|
<span>Nieuwe inloglocatie gedetecteerd</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-[11px] leading-relaxed text-amber-700 dark:text-amber-300/90">
|
||||||
|
U logt in vanaf een nieuw netwerkadres (<code class="font-mono bg-amber-100 dark:bg-amber-900/50 px-1 py-0.5 rounded text-[10px]"><?= htmlspecialchars($_SESSION['pending_new_ip']) ?></code>).
|
||||||
|
Voer uw 2FA Authenticator code in om uw identiteit te bevestigen en deze locatie automatisch te autoriseren.
|
||||||
|
</p>
|
||||||
|
<div class="pt-1.5 flex items-center justify-between text-[11px] border-t border-amber-200/60 dark:border-amber-800/40 text-amber-700 dark:text-amber-400">
|
||||||
|
<span class="flex items-center space-x-1">
|
||||||
|
<i data-lucide="mail-check" class="w-3.5 h-3.5"></i>
|
||||||
|
<span><?= !empty($_SESSION['pending_new_ip_mail_sent']) ? 'Verificatiemail verzonden' : '2FA verificatie actief' ?></span>
|
||||||
|
</span>
|
||||||
|
<a href="/auth/login.php?action=resend_ip_mail" class="font-bold underline hover:text-amber-900 dark:hover:text-amber-200 cursor-pointer">
|
||||||
|
Mail opnieuw sturen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="inline-flex items-center justify-center w-12 h-12 rounded-xl bg-indigo-50 dark:bg-indigo-950/40 text-indigo-600 dark:text-indigo-400 mb-2">
|
<div class="inline-flex items-center justify-center w-12 h-12 rounded-xl bg-indigo-50 dark:bg-indigo-950/40 text-indigo-600 dark:text-indigo-400 mb-2">
|
||||||
<i data-lucide="key" class="w-6 h-6"></i>
|
<i data-lucide="key" class="w-6 h-6"></i>
|
||||||
|
|||||||
@@ -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);
|
$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']);
|
return !empty($res['success']);
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
error_log("fociz_send_new_ip_verification error: " . $e->getMessage());
|
error_log("fociz_send_new_ip_verification error: " . $e->getMessage());
|
||||||
|
|||||||
+52
-12
@@ -131,15 +131,58 @@ class FocizMailer {
|
|||||||
throw new Exception("Ongeldige server banner (verwacht 220, kreeg {$resp['code']}): {$resp['text']}");
|
throw new Exception("Ongeldige server banner (verwacht 220, kreeg {$resp['code']}): {$resp['text']}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. EHLO Handshake
|
// 4. EHLO Handshake conform RFC 5321 §4.1.4
|
||||||
$clientHost = !empty($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'fociz.nl';
|
// Voorkom impersonation van het lokale domein (bijv. 'fociz.nl' tegen 'mail.fociz.nl')
|
||||||
$resp = $sendCommand("EHLO {$clientHost}");
|
// Antagonist/Exim weigert 'EHLO fociz.nl' van externe hosts met '550 Bad HELO - Host impersonating domain name'
|
||||||
if ($resp['code'] !== 250) {
|
$resolveHeloHost = function() use ($host): string {
|
||||||
$resp = $sendCommand("HELO {$clientHost}");
|
// 1. Mailserver hostnaam indien beschikbaar en niet een lokaal IP
|
||||||
if ($resp['code'] !== 250) {
|
if (!empty($host) && str_contains($host, '.') && !str_starts_with($host, '192.168.') && !str_starts_with($host, '127.')) {
|
||||||
throw new Exception("EHLO/HELO geweigerd door server: {$resp['text']}");
|
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')
|
// 5. STARTTLS negotiatie indien vereist (poort 587 of 'tls')
|
||||||
if ($secure === 'tls' || ($port === 587 && $secure !== 'none')) {
|
if ($secure === 'tls' || ($port === 587 && $secure !== 'none')) {
|
||||||
@@ -165,10 +208,7 @@ class FocizMailer {
|
|||||||
$log("TLS encryptie succesvol geactiveerd.");
|
$log("TLS encryptie succesvol geactiveerd.");
|
||||||
|
|
||||||
// Na STARTTLS moet EHLO opnieuw worden gestuurd (RFC 3207)
|
// Na STARTTLS moet EHLO opnieuw worden gestuurd (RFC 3207)
|
||||||
$resp = $sendCommand("EHLO {$clientHost}");
|
$clientHost = $performHeloHandshake($clientHost);
|
||||||
if ($resp['code'] !== 250) {
|
|
||||||
throw new Exception("EHLO na TLS handshake geweigerd: {$resp['text']}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. SMTP Authenticatie
|
// 6. SMTP Authenticatie
|
||||||
|
|||||||
Reference in New Issue
Block a user