0) { $error = sprintf( 'Zu viele fehlgeschlagene Versuche von diesem Netzwerk. Bitte in %d Minute(n) erneut versuchen.', (int)ceil($ipSperre / 60) ); } elseif (Auth::unlock($pdo, (string)($_POST['password'] ?? ''))) { $ziel = (string)($_POST['return_to'] ?? ''); header('Location: ' . (preg_match('#^/[A-Za-z0-9_./#?=-]*$#', $ziel) ? $ziel : '/index.php')); exit; } else { Auth::recordFailedAttempt($pdo, Http::clientIp(), Auth::lockedUsername()); $error = 'Das Passwort stimmt nicht.'; } } catch (\Throwable $e) { Logger::error('Entsperren fehlgeschlagen', ['error' => $e->getMessage()]); $error = 'Entsperren derzeit nicht moeglich. Bitte spaeter erneut versuchen.'; } } $gesperrt = Auth::isLocked(); } if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['step'] ?? '') === 'abandon') { // Bewusst abmelden statt entsperren. Auth::logout(); header('Location: /login.php'); exit; } // ----------------------------------------------------------------------- // Zweite Stufe: Notfalltoken und Abbruch // // Beides laeuft als gewoehnliches Formular und nicht ueber die JSON-API. // Der Hardwarekey braucht JavaScript, weil navigator.credentials sich // anders nicht bedienen laesst - der Ersatzweg soll diese Voraussetzung // gerade nicht teilen. // ----------------------------------------------------------------------- $step = (string)($_POST['step'] ?? ''); if ($_SERVER['REQUEST_METHOD'] === 'POST' && $step === 'cancel') { Auth::clearPending(); header('Location: /login.php'); exit; } if ($_SERVER['REQUEST_METHOD'] === 'POST' && $step === 'recovery') { if (!Csrf::isValid(is_string($_POST['csrf_token'] ?? null) ? $_POST['csrf_token'] : null)) { $error = 'Sitzung abgelaufen. Bitte erneut versuchen.'; } elseif (Auth::pendingUserId() <= 0) { $error = 'Die Anmeldung ist abgelaufen. Bitte von vorn beginnen.'; } else { try { $pdo = Db::init(); $repo = new CredentialRepo($pdo); if ($repo->consumeRecoveryToken(Auth::pendingUserId(), (string)($_POST['recovery_token'] ?? ''))) { Auth::completeSecondFactor($pdo, 'recovery_token'); // Der Token ist jetzt verbraucht. Das muss der Betreiber // erfahren, und zwar dort, wo er nach dem Anmelden landet - // also ueber die allgemeine Meldezeile und nicht in einem // Unterreiter, den er vielleicht wochenlang nicht aufschlaegt. $_SESSION['dc_flash'] = [ 'type' => 'danger', 'message' => 'Sie haben sich mit dem Notfalltoken angemeldet. ' . 'Er ist damit verbraucht. Unter System → Benutzer einen neuen erzeugen, ' . 'sonst bleibt nur noch der Hardwarekey.', ]; header('Location: /index.php'); exit; } Auth::recordSecondFactorFailure($pdo, 'recovery_token_invalid'); $error = 'Der Notfalltoken stimmt nicht.'; } catch (\Throwable $e) { Logger::error('Notfalltoken-Pruefung fehlgeschlagen', ['error' => $e->getMessage()]); $error = 'Anmeldung derzeit nicht moeglich. Bitte spaeter erneut versuchen.'; } } } if ($_SERVER['REQUEST_METHOD'] === 'POST' && $step === '') { $username = trim((string)($_POST['username'] ?? '')); $password = (string)($_POST['password'] ?? ''); // CSRF-Schutz auch am Login: verhindert erzwungene Fremdanmeldungen. if (!Csrf::isValid(is_string($_POST['csrf_token'] ?? null) ? $_POST['csrf_token'] : null)) { $error = 'Sitzung abgelaufen. Bitte erneut versuchen.'; } elseif ($username === '' || $password === '') { $error = 'Bitte fuellen Sie alle Felder aus.'; } else { try { $pdo = Db::init(); // Drosselung vor allem anderen. Die Kontosperre greift erst ab // dem fuenften, die IP-Sperre ab dem zehnten Fehlversuch - bis // dahin durfte bisher beliebig schnell geraten werden. Der // RateLimiter ist im Haus und auf acht anderen Endpunkten im // Einsatz; ausgerechnet die Anmeldung war ohne. $drossel = new RateLimiter($pdo, 10, 60, 'webui_login'); $ipLockout = Auth::lockoutSeconds($pdo, Http::clientIp()); $accLockout = Auth::accountLockoutSeconds($pdo, $username); if (!$drossel->check(Http::clientIp())) { $error = 'Zu viele Anmeldeversuche. Bitte eine Minute warten.'; } elseif ($ipLockout > 0) { // Die IP-Sperre darf beim Namen genannt werden: Sie sagt // etwas ueber das Netzwerk aus, nicht darueber, ob es ein // bestimmtes Konto gibt. $error = sprintf( 'Zu viele fehlgeschlagene Anmeldeversuche von diesem Netzwerk. Bitte in %d Minute(n) erneut versuchen.', (int)ceil($ipLockout / 60) ); } elseif ($accLockout > 0) { // Die KONTOsperre dagegen nicht. Gesperrt wird nur, was // existiert - eine eigene Meldung dafuer verriete also die // Existenz des Namens und haette die Laufzeitangleichung // ueber dummyHash() in verifyCredentials() wieder aufgehoben. // Die Wartezeit darf stehen, der Grund nicht. $error = sprintf( 'Anmeldung nicht moeglich. Bitte in %d Minute(n) erneut versuchen.', (int)ceil($accLockout / 60) ); } else { $result = Auth::startLogin($pdo, $username, $password); if ($result === Auth::LOGIN_COMPLETE) { header('Location: /index.php'); exit; } if ($result === Auth::LOGIN_SECOND_FACTOR) { // Nicht weiterleiten: Die zweite Stufe wird unten // gerendert. Ein Redirect wuerde nur eine Runde kosten // und der Adresszeile ansehen lassen, wie weit jemand // gekommen ist. $error = null; } } if ($error === null && Auth::pendingUserId() <= 0 && !Auth::isLoggedIn()) { // Pruefen, ob das Konto oder die IP durch diesen Fehlversuch soeben gesperrt wurde $newAccLockout = Auth::accountLockoutSeconds($pdo, $username); $newIpLockout = Auth::lockoutSeconds($pdo, Http::clientIp()); if ($newAccLockout > 0) { // Wieder ohne den Grund: "dieses Benutzerkonto wurde // gesperrt" ist eine Bestaetigung, dass es das Konto // gibt. $error = sprintf( 'Anmeldung nicht moeglich. Bitte in %d Minute(n) erneut versuchen.', (int)ceil($newAccLockout / 60) ); } elseif ($newIpLockout > 0) { $error = sprintf( 'Zu viele Fehlversuche. Ihre IP-Adresse wurde für %d Minute(n) gesperrt.', (int)ceil($newIpLockout / 60) ); } else { $error = 'Anmeldung nicht moeglich.'; } } } catch (\Throwable $e) { // Die urspruengliche Fassung gab hier die rohe PDO-Meldung samt // Hostname und Benutzernamen an jeden anonymen Besucher aus. Logger::error('Anmeldung fehlgeschlagen (technischer Fehler)', ['error' => $e->getMessage()]); $error = 'Anmeldung derzeit nicht moeglich. Bitte spaeter erneut versuchen.'; } } } // Erst hier abfragen, nicht weiter oben: Der Zwischenzustand entsteht unter // Umstaenden erst durch die Auswertung darueber. $pendingUsername = Auth::pendingUsername(); ?> Anmeldung - Deploymentcenter

Deploymentcenter

Sitzung gesperrt

Die Sitzung von wurde nach 30 Minuten ohne Aktivität gesperrt. Zum Weiterarbeiten genügt das Passwort — der Hardwarekey wird erst bei einer neuen Anmeldung wieder verlangt.
Passwort erkannt für . Bitte jetzt den Hardwarekey anstecken und berühren.
Warte auf den Hardwarekey …
oder
> Hardwarekey nicht zur Hand?

Wird dabei verbraucht. Nach der Anmeldung unter System → Benutzer einen neuen erzeugen.