Files
deskhub/DeskHub-Webseite/tests/login_guard.php
T
2026-10-30 21:50:16 +00:00

137 lines
8.3 KiB
PHP

<?php
declare(strict_types=1);
/**
* Prüft den Anmelde-Wächter: erste Anmeldung still gemerkt, bekanntes Gerät ohne Warnung, neues Gerät und neuer Ort mit Warnung
* (Link „Das war ich nicht“), App-Anmeldungen, Fehlversuche und Sperre im Protokoll (einmal je Sperrzeit), „Konto sichern“
* (Passwort ungültig, Tokens und Geräte weg, Sitzungen ungültig, Link nur einmal), Ablauf des Links, Ausschalter und Aufbewahrung.
*/
$db = sys_get_temp_dir() . '/deskhub-lg-' . bin2hex(random_bytes(4)) . '.sqlite';
putenv('DESKHUB_TESTING=1');
putenv('DESKHUB_SQLITE_PATH=' . $db);
require_once dirname(__DIR__) . '/includes/bootstrap.php';
$failures = [];
$check = function (bool $ok, string $what) use (&$failures): void { if (!$ok) $failures[] = $what; };
$pdo = get_db();
$pdo->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";