prepare("INSERT INTO staff (name, email, password_hash, role, is_active, created_at) VALUES (?,?,?,?,1,?)") ->execute(['Test Person', 'person@firma.example', password_hash('geheim-123', PASSWORD_DEFAULT), 'agent', db_now()]); $sid = (int) $pdo->lastInsertId(); $pdo->prepare("INSERT INTO staff (name, email, password_hash, role, is_active, created_at) VALUES (?,?,?,?,1,?)") ->execute(['Chef', 'chef@firma.example', password_hash('x', PASSWORD_DEFAULT), 'admin', db_now()]); $staff = fn() => $pdo->query('SELECT * FROM staff WHERE id = ' . $sid)->fetch(); $events = fn(string $ev = '') => (int) $pdo->query('SELECT COUNT(*) FROM login_events' . ($ev !== '' ? " WHERE event = '$ev'" : ''))->fetchColumn(); $chrome = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36'; $phone = 'Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Mobile Safari/537.36'; $as = function (string $ip, string $ua, ?string $cookie): void { $_SERVER['REMOTE_ADDR'] = $ip; $_SERVER['HTTP_USER_AGENT'] = $ua; unset($_COOKIE[LOGIN_GUARD_COOKIE]); if ($cookie !== null) $_COOKIE[LOGIN_GUARD_COOKIE] = $cookie; }; $dev1 = str_repeat('a', 32); $dev2 = str_repeat('b', 32); // Beschreibung und Ort $check(login_guard_describe_device($chrome) === 'Chrome auf Windows', 'Gerätename: Chrome auf Windows'); $check(login_guard_describe_device($phone) === 'Chrome auf Android', 'Gerätename: Chrome auf Android'); $check(login_guard_place('203.0.113.9')[0] === 'N:203.0' && login_guard_place('10.0.0.5')[0] === 'local', 'Ort: Netz der IP bzw. lokales Netz'); $check(str_starts_with(login_guard_place('2a02:810d:1::5')[0], 'N6:2a02:810d'), 'Ort: IPv6-Netz'); // 1) Erste Anmeldung: still gemerkt, keine Warnung $as('203.0.113.9', $chrome, $dev1); $GLOBALS['login_guard_last_link'] = null; $check(login_guard_success($staff(), 'web') === 'first', 'Erste Anmeldung wird still gemerkt'); $check($GLOBALS['login_guard_last_link'] === null && $events('success') === 1, 'Erste Anmeldung: keine Warnung, ein Protokolleintrag'); // 2) Bekanntes Gerät, gleicher Ort $check(login_guard_success($staff(), 'web') === 'known' && $GLOBALS['login_guard_last_link'] === null, 'Bekanntes Gerät: keine Warnung'); // 3) Neues Gerät: Warnung mit Link $as('203.0.113.9', $phone, $dev2); $check(login_guard_success($staff(), 'web') === 'new_device', 'Neues Gerät wird erkannt'); $link = (string) $GLOBALS['login_guard_last_link']; $check(str_contains($link, '/staff/login_alert.php?t='), 'Warn-Link wurde erzeugt'); $token1 = substr($link, strpos($link, 't=') + 2); $check($events('new_device') === 1, 'Protokoll: Neues Gerät'); $check(login_guard_success($staff(), 'web') === 'known', 'Das neue Gerät ist danach bekannt'); // 4) Bekanntes Gerät, neuer Ort $GLOBALS['login_guard_last_link'] = null; $as('198.51.100.7', $chrome, $dev1); $check(login_guard_success($staff(), 'web') === 'new_place' && $GLOBALS['login_guard_last_link'] !== null, 'Neuer Ort vom bekannten Gerät wird gemeldet'); $token2 = substr((string) $GLOBALS['login_guard_last_link'], -48); $GLOBALS['login_guard_last_link'] = null; $as('192.0.2.77', $chrome, $dev1); set_setting('login_guard_warn_place', '0'); $check(login_guard_success($staff(), 'web') === 'known' && $GLOBALS['login_guard_last_link'] === null, 'Ort-Warnung lässt sich ausschalten'); set_setting('login_guard_warn_place', '1'); // 5) App: Gerät = Gerätename $as('203.0.113.9', 'okhttp/4.12', null); $check(login_guard_success($staff(), 'app', 'Pixel 8') === 'new_device', 'App mit neuem Gerätenamen wird gemeldet'); $check(login_guard_success($staff(), 'app', 'Pixel 8') === 'known', 'Gleiches App-Gerät ist bekannt'); $check((int) $pdo->query("SELECT COUNT(*) FROM login_devices WHERE channel = 'app'")->fetchColumn() === 1, 'App-Gerät gemerkt'); // 6) Fehlversuche und Sperre login_guard_failed(null, 'unbekannt@x.example', 'web', 'Unbekannte E-Mail-Adresse'); login_guard_failed($staff(), 'person@firma.example', 'web', 'Falsches Passwort'); $check($events('failed') === 2, 'Fehlversuche stehen im Protokoll'); login_guard_locked('person@firma.example', 'web'); login_guard_locked('person@firma.example', 'web'); $check($events('locked') === 1, 'Sperre wird je Sperrzeit nur einmal protokolliert'); $pdo->exec("UPDATE login_events SET created_at = '2000-01-01 00:00:00' WHERE event = 'locked'"); login_guard_locked('person@firma.example', 'web'); $check($events('locked') === 2, 'Nach der Sperrzeit wird eine neue Sperre wieder protokolliert'); set_setting('login_guard_max_attempts', '999'); $check(login_guard_max_attempts() === 20, 'Anzahl Fehlversuche ist auf 20 begrenzt'); set_setting('login_guard_max_attempts', '5'); // 7) Link prüfen, einmal verwenden $check(login_guard_find_alert($pdo, 'zzz') === null && login_guard_find_alert($pdo, str_repeat('0', 48)) === null, 'Ungültiger Link wird abgelehnt'); $ev = login_guard_find_alert($pdo, $token1); $check($ev !== null && (int) $ev['staff_id'] === $sid, 'Gültiger Link findet den Eintrag'); $pdo->prepare('UPDATE login_events SET created_at = ? WHERE id = ?')->execute(['2000-01-01 00:00:00', (int) $ev['id']]); $check(login_guard_find_alert($pdo, $token1) === null, 'Link ist nach 7 Tagen abgelaufen'); // 8) Konto sichern $pdo->prepare('INSERT INTO mobile_api_tokens (staff_id, token_hash, device_name, expires_at, created_at, last_used_at) VALUES (?,?,?,?,?,?)') ->execute([$sid, hash('sha256', 't'), 'Pixel 8', date('Y-m-d H:i:s', time() + 3600), db_now(), db_now()]); $ev2 = login_guard_find_alert($pdo, $token2); $check($ev2 !== null, 'Zweiter Link ist gültig'); $_SESSION = ['staff_login_at' => time() - 100]; $check(login_guard_session_revoked($sid) === false, 'Vor dem Sichern ist die Sitzung gültig'); $check(login_guard_revoke($pdo, $ev2) === true, 'Konto sichern gelingt'); $check(!password_verify('geheim-123', (string) $staff()['password_hash']), 'Passwort ist ungültig'); $check((int) $pdo->query('SELECT COUNT(*) FROM mobile_api_tokens WHERE staff_id = ' . $sid)->fetchColumn() === 0, 'App-Anmeldungen sind beendet'); $check((int) $pdo->query('SELECT COUNT(*) FROM login_devices WHERE staff_id = ' . $sid)->fetchColumn() === 0, 'Gemerkte Geräte sind vergessen'); $check($events('revoked') === 1, 'Protokoll: Konto gesichert'); $check(login_guard_session_revoked($sid) === true, 'Ältere Sitzungen sind ungültig'); $_SESSION = ['staff_login_at' => time() + 5]; $check(login_guard_session_revoked($sid) === false, 'Eine spätere Anmeldung bleibt gültig'); $check(login_guard_find_alert($pdo, $token2) === null, 'Der Link ist nur einmal verwendbar'); // 9) Ausschalter set_setting('login_guard', '0'); $before = $events(); $as('203.0.113.9', $chrome, str_repeat('c', 32)); $check(login_guard_success($staff(), 'web') === 'off' && $events() === $before, 'Ausgeschaltet wird nichts gemerkt oder protokolliert'); login_guard_failed(null, 'x@y.example', 'web', 'x'); $check($events() === $before, 'Ausgeschaltet gibt es auch keine Fehlversuch-Einträge'); set_setting('login_guard', '1'); // 10) Aufbewahrung $pdo->exec("UPDATE login_events SET created_at = '2000-01-01 00:00:00' WHERE id IN (SELECT id FROM login_events ORDER BY id LIMIT 3)"); $n0 = $events(); $old = (int) $pdo->query("SELECT COUNT(*) FROM login_events WHERE created_at < '2001-01-01'")->fetchColumn(); login_guard_purge($pdo); $check($old >= 3 && $events() === $n0 - $old, 'Alte Einträge werden nach der Aufbewahrungsfrist gelöscht'); @unlink($db); if ($failures) { fwrite(STDERR, "Anmelde-Wächter-Prüfung fehlgeschlagen:\n- " . implode("\n- ", $failures) . "\n"); exit(1); } echo "Anmelde-Wächter-Prüfungen bestanden.\n";