feat(security): implement IP verification and encryption for sensitive tokens

This commit is contained in:
2026-09-11 13:17:12 +02:00
parent c3001cdd0a
commit 55618ffaed
10 changed files with 437 additions and 20 deletions
+6 -1
View File
@@ -137,7 +137,12 @@ $saveStmt = $db->prepare("
settings = VALUES(settings),
last_sync_at = NOW()
");
$saveStmt->execute([$user['id'], $accessToken, $refreshToken, json_encode($newSettings)]);
$saveStmt->execute([
$user['id'],
fociz_encrypt($accessToken),
fociz_encrypt($refreshToken),
json_encode($newSettings)
]);
// 8. Succesvol terugkeren naar de gewenste pagina of het Persoonlijk Profiel
$returnTo = $_SESSION['ms_oauth_return_to'] ?? '/profile.php';
+27 -7
View File
@@ -22,13 +22,19 @@ if ($currentUser) {
$error = null;
$infoMessage = null;
if (isset($_GET['expired'])) {
$infoMessage = "Uw dagsessie is om 23:59 beëindigd. Log opnieuw in om een nieuwe werkdag te starten.";
$infoMessage = "Uw dagsessie is beëindigd. 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.";
}
if (isset($_GET['ip_verified'])) {
$infoMessage = "Nieuwe inloglocatie succesvol geverifieerd! U kunt nu veilig inloggen.";
}
$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']);
unset($_SESSION['pending_2fa_user_id'], $_SESSION['pending_2fa_user_email'], $_SESSION['pending_2fa_user_name'], $_SESSION['pending_new_ip']);
header('Location: /auth/login.php');
exit;
}
@@ -50,11 +56,21 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password_hash'])) {
// Succesvol stap 1: zet tijdelijke pending sessie voor 2FA
$_SESSION['pending_2fa_user_id'] = $user['id'];
$_SESSION['pending_2fa_user_email'] = $user['email'];
$_SESSION['pending_2fa_user_name'] = $user['name'];
$step = 2;
// Controleer of de inloglocatie/IP bekend is
$clientIp = fociz_get_client_ip();
$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);
$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
$_SESSION['pending_2fa_user_id'] = $user['id'];
$_SESSION['pending_2fa_user_email'] = $user['email'];
$_SESSION['pending_2fa_user_name'] = $user['name'];
$step = 2;
}
} else {
$error = "Onjuist e-mailadres of wachtwoord.";
}
@@ -105,12 +121,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($isValid) {
// Volledige login succesvol (geldig tot 23:59:59 van vandaag)
$clientIp = fociz_get_client_ip();
fociz_register_user_ip((int)$user['id'], $clientIp, true);
$_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'] = date('Y-m-d');
$_SESSION['auth_fingerprint'] = fociz_get_client_fingerprint();
$_SESSION['session_expires_at'] = strtotime('today 23:59:59');
$_SESSION['last_activity'] = time();
+135
View File
@@ -0,0 +1,135 @@
<?php
/**
* Fociz Nieuwe Inloglocatie / IP Verificatie
*/
require_once __DIR__ . '/../includes/auth.php';
start_secure_session();
$token = trim($_GET['token'] ?? '');
$db = get_db_connection();
$error = null;
$success = false;
$userEmail = '';
$ipAddress = '';
if (empty($token) || !preg_match('/^[a-f0-9]{64}$/', $token)) {
$error = "Ongeldige of ontbrekende verificatielink.";
} else {
try {
$stmt = $db->prepare("
SELECT kip.*, u.email, u.name
FROM user_known_ips kip
JOIN users u ON kip.user_id = u.id
WHERE kip.verify_token = ?
");
$stmt->execute([$token]);
$record = $stmt->fetch();
if (!$record) {
$error = "Deze verificatielink is niet gevonden of al gebruikt.";
} elseif (!empty($record['verify_token_expires_at']) && strtotime($record['verify_token_expires_at']) < time()) {
$error = "Deze verificatielink is verlopen (geldigheidsduur was 30 minuten). Probeer opnieuw in te loggen om een nieuwe link te ontvangen.";
} else {
// Verifieer het IP-adres
$updateStmt = $db->prepare("
UPDATE user_known_ips
SET is_verified = 1, verify_token = NULL, verify_token_expires_at = NULL, last_seen_at = NOW()
WHERE id = ?
");
$updateStmt->execute([$record['id']]);
$success = true;
$userEmail = $record['email'];
$ipAddress = $record['ip_address'];
}
} catch (Throwable $e) {
error_log("verify-ip.php error: " . $e->getMessage());
$error = "Er is een databasefout opgetreden bij het verifiëren.";
}
}
$pageTitle = "Fociz — Locatie Verificatie";
?>
<!DOCTYPE html>
<html lang="nl" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= htmlspecialchars($pageTitle) ?></title>
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg">
<link rel="alternate icon" href="/favicon.ico">
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
indigo: { 500: '#6366f1', 600: '#4f46e5', 700: '#4338ca' }
}
}
}
}
</script>
<script src="https://unpkg.com/lucide@latest"></script>
</head>
<body class="bg-slate-100 dark:bg-[#0c0f17] text-slate-800 dark:text-slate-100 min-h-screen flex items-center justify-center p-4 transition-colors duration-200">
<div class="w-full max-w-md bg-white dark:bg-[#121622] rounded-2xl border border-slate-200/80 dark:border-slate-800/80 shadow-2xl p-8 space-y-6">
<!-- Logo & Header -->
<div class="text-center space-y-2">
<div class="inline-flex items-center justify-center w-12 h-12 rounded-xl bg-indigo-500/10 border border-indigo-500/20 text-indigo-500 mb-2">
<i data-lucide="shield-check" class="w-6 h-6"></i>
</div>
<h1 class="text-xl font-bold text-slate-900 dark:text-white">Locatie Verificatie</h1>
<p class="text-xs text-slate-500 dark:text-slate-400">Beveiligde inlogtoegang voor Fociz</p>
</div>
<?php if ($success): ?>
<div class="p-4 rounded-xl bg-emerald-50 dark:bg-emerald-500/10 border border-emerald-200 dark:border-emerald-500/20 text-emerald-800 dark:text-emerald-400 text-sm space-y-2 text-center">
<div class="flex items-center justify-center space-x-2 font-bold text-emerald-700 dark:text-emerald-300">
<i data-lucide="check-circle-2" class="w-5 h-5"></i>
<span>Locatie Succesvol Geverifieerd!</span>
</div>
<p class="text-xs">
Het IP-adres <strong class="font-mono"><?= htmlspecialchars($ipAddress) ?></strong> is nu geautoriseerd voor uw account (<?= htmlspecialchars($userEmail) ?>).
</p>
</div>
<div class="pt-2">
<a href="/auth/login.php?ip_verified=1" class="w-full bg-indigo-600 hover:bg-indigo-500 text-white font-bold py-2.5 px-4 rounded-xl transition shadow-xs flex items-center justify-center space-x-2 cursor-pointer">
<span>Direct Inloggen</span>
<i data-lucide="arrow-right" class="w-4 h-4"></i>
</a>
</div>
<?php else: ?>
<div class="p-4 rounded-xl bg-rose-50 dark:bg-rose-500/10 border border-rose-200 dark:border-rose-500/20 text-rose-800 dark:text-rose-400 text-sm space-y-2 text-center">
<div class="flex items-center justify-center space-x-2 font-bold text-rose-700 dark:text-rose-300">
<i data-lucide="alert-circle" class="w-5 h-5"></i>
<span>Verificatie Mislukt</span>
</div>
<p class="text-xs leading-relaxed">
<?= htmlspecialchars($error ?? 'Onbekende fout.') ?>
</p>
</div>
<div class="pt-2">
<a href="/auth/login.php" class="w-full bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 font-semibold py-2.5 px-4 rounded-xl transition flex items-center justify-center space-x-2 cursor-pointer text-sm">
<i data-lucide="arrow-left" class="w-4 h-4"></i>
<span>Terug naar Inloggen</span>
</a>
</div>
<?php endif; ?>
</div>
<script>
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
</script>
</body>
</html>