app/totp.php implementiert RFC 6238 selbst statt per Bibliothek: der Algorithmus ist ein HMAC plus eine Truncation, und ein zweiter Faktor ist die letzte Stelle fuer ungepruefte Abhaengigkeiten. Der QR-Code entsteht aus dem ohnehin vorhandenen TCPDF, damit kein externer Dienst das Geheimnis sieht. Beim Login wird die Anmeldung bei aktivem zweitem Faktor nicht abgeschlossen; der Zwischenzustand gewaehrt keinerlei Zugriff und ist byte-identisch zu einem unangemeldeten Aufruf. users.totp_last_step verhindert die Wiederverwendung eines abgefangenen Codes innerhalb seines Gueltigkeitsfensters. Abschalten verlangt Passwort und Code. APP_REQUIRE_2FA_FOR_ADMINS macht den Faktor fuer Platform-Admins verbindlich, per Weiterleitung auf die Einrichtung statt als harte Sperre - sonst koennte der Schalter den einzigen Admin aussperren. Standard aus. check-konto-und-mandantenwechsel erwartete beim Login noch das entfernte Kundenkuerzel-Feld und damit einen direkten Sprung aufs Dashboard; der Check bildet jetzt den tatsaechlichen Weg ueber die Mandantenauswahl ab. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
147 lines
6.8 KiB
PHP
147 lines
6.8 KiB
PHP
<?php
|
|
|
|
require_once __DIR__ . '/functions.php';
|
|
require_once __DIR__ . '/app/rate-limit.php';
|
|
require_once __DIR__ . '/app/totp.php';
|
|
|
|
$user = saas_require_login();
|
|
$userId = (int)$user['user_id'];
|
|
$pdo = app_db_pdo();
|
|
|
|
$errors = [];
|
|
$notice = null;
|
|
// Die Klartext-Codes existieren nur fuer die Dauer dieses einen Requests.
|
|
// Sie werden bewusst nicht in der Session geparkt: ein Reload soll sie nicht
|
|
// erneut anzeigen, sonst waere die Einmaligkeit nur behauptet.
|
|
$freshRecoveryCodes = null;
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
app_require_csrf();
|
|
$action = (string)($_POST['aktion'] ?? '');
|
|
|
|
if ($action === 'starten') {
|
|
if (app_totp_begin_setup($pdo, $userId) === null) {
|
|
$errors[] = 'Der zweite Faktor ist bereits aktiv.';
|
|
}
|
|
} elseif ($action === 'bestaetigen') {
|
|
// Auch die Einrichtung ist ein Rateversuch gegen sechs Ziffern.
|
|
if (!app_rate_limit_check($pdo, '2fa_setup:' . $userId, 10, 900)) {
|
|
$errors[] = 'Zu viele Versuche. Bitte warte einige Minuten.';
|
|
} else {
|
|
$freshRecoveryCodes = app_totp_confirm_setup($pdo, $userId, (string)($_POST['code'] ?? ''));
|
|
if ($freshRecoveryCodes === null) {
|
|
$errors[] = 'Der Code stimmt nicht. Prüfe die Uhrzeit deines Geräts und versuche es erneut.';
|
|
}
|
|
}
|
|
} elseif ($action === 'abschalten') {
|
|
// Abschalten verlangt beides: Passwort und einen gueltigen Code.
|
|
// Wer nur kurz an einem offenen Bildschirm sitzt, soll den Schutz
|
|
// nicht mit einem Klick entfernen koennen.
|
|
$passwordOk = saas_password_matches($pdo, $userId, (string)($_POST['passwort'] ?? ''));
|
|
$codeOk = app_totp_verify_for_user($pdo, $userId, (string)($_POST['code'] ?? ''));
|
|
|
|
if ($passwordOk && $codeOk) {
|
|
app_totp_disable($pdo, $userId);
|
|
$notice = 'Der zweite Faktor ist abgeschaltet.';
|
|
} else {
|
|
$errors[] = 'Passwort oder Code stimmt nicht.';
|
|
}
|
|
}
|
|
}
|
|
|
|
$state = app_totp_state($pdo, $userId);
|
|
$isActive = $state['confirmed_at'] !== null;
|
|
$setupSecret = (!$isActive && $state['secret'] !== null) ? $state['secret'] : null;
|
|
$remainingRecoveryCodes = $isActive ? app_totp_unused_recovery_code_count($pdo, $userId) : 0;
|
|
|
|
include 'header.php';
|
|
include 'headerline.php';
|
|
include 'nav.php';
|
|
?>
|
|
|
|
<section id="banner">
|
|
<div class="content">
|
|
<h2>Zwei-Faktor-Anmeldung</h2>
|
|
|
|
<?php if ($notice !== null): ?>
|
|
<div class="hint-box success"><p><?php echo saas_html($notice); ?></p></div>
|
|
<?php endif; ?>
|
|
|
|
<?php foreach ($errors as $error): ?>
|
|
<div class="hint-box error"><p><?php echo saas_html($error); ?></p></div>
|
|
<?php endforeach; ?>
|
|
|
|
<?php if ($freshRecoveryCodes !== null): ?>
|
|
<div class="hint-box success">
|
|
<p><b>Der zweite Faktor ist jetzt aktiv.</b></p>
|
|
<p>Bewahre diese Wiederherstellungscodes an einem sicheren Ort auf. Jeder gilt
|
|
genau einmal und ersetzt den Code aus der App, falls du keinen Zugriff mehr auf
|
|
dein Gerät hast. <b>Sie werden nur dieses eine Mal angezeigt.</b></p>
|
|
<p style="font-family: monospace; font-size: 1.1em; line-height: 1.8;">
|
|
<?php foreach ($freshRecoveryCodes as $code): ?>
|
|
<?php echo saas_html($code); ?><br>
|
|
<?php endforeach; ?>
|
|
</p>
|
|
</div>
|
|
<?php elseif ($isActive): ?>
|
|
<div class="hint-box success">
|
|
<p>Der zweite Faktor ist <b>aktiv</b>, eingerichtet am
|
|
<?php echo saas_html($state['confirmed_at']); ?>.</p>
|
|
<p>Unbenutzte Wiederherstellungscodes: <b><?php echo $remainingRecoveryCodes; ?></b><?php
|
|
echo $remainingRecoveryCodes === 0
|
|
? ' – alle aufgebraucht. Schalte den zweiten Faktor einmal ab und wieder an, um neue zu erhalten.'
|
|
: ''; ?></p>
|
|
</div>
|
|
|
|
<h3>Abschalten</h3>
|
|
<p>Zum Abschalten brauchst du dein Passwort und einen gültigen Code.</p>
|
|
<form method="post" action="zwei-faktor-einrichten.php">
|
|
<?php echo app_csrf_field(); ?>
|
|
<input type="hidden" name="aktion" value="abschalten">
|
|
<label for="passwort">Passwort</label>
|
|
<input type="password" name="passwort" id="passwort" required>
|
|
<label for="code_ab">Code aus der App</label>
|
|
<input type="text" name="code" id="code_ab" inputmode="numeric" autocomplete="one-time-code" required>
|
|
<p><button type="submit">Zweiten Faktor abschalten</button></p>
|
|
</form>
|
|
|
|
<?php elseif ($setupSecret !== null): ?>
|
|
<p>Scanne den Code mit deiner Authenticator-App – zum Beispiel Aegis, 2FAS,
|
|
Google Authenticator oder dem Passwortmanager deiner Wahl. Gib danach den
|
|
angezeigten sechsstelligen Code ein, damit die Einrichtung abgeschlossen wird.</p>
|
|
|
|
<div style="margin: 1.5em 0; max-width: 220px;">
|
|
<?php echo app_totp_qr_svg(app_totp_provisioning_uri($setupSecret, (string)$user['email'])); ?>
|
|
</div>
|
|
|
|
<p>Falls das Scannen nicht klappt, trage das Geheimnis von Hand ein:<br>
|
|
<span style="font-family: monospace; font-size: 1.15em; letter-spacing: 0.1em;"><?php
|
|
echo saas_html(trim(chunk_split($setupSecret, 4, ' '))); ?></span></p>
|
|
|
|
<form method="post" action="zwei-faktor-einrichten.php">
|
|
<?php echo app_csrf_field(); ?>
|
|
<input type="hidden" name="aktion" value="bestaetigen">
|
|
<label for="code">Code aus der App</label>
|
|
<input type="text" name="code" id="code" inputmode="numeric" autocomplete="one-time-code" required>
|
|
<p><button type="submit" class="primary">Einrichtung abschließen</button></p>
|
|
</form>
|
|
|
|
<?php else: ?>
|
|
<p>Mit einem zweiten Faktor reicht dein Passwort allein nicht mehr aus, um sich
|
|
anzumelden. Zusätzlich wird ein sechsstelliger Code aus einer Authenticator-App
|
|
auf deinem Telefon verlangt.</p>
|
|
<p>Du brauchst dafür eine App wie Aegis, 2FAS oder Google Authenticator. Die Codes
|
|
entstehen auf deinem Gerät; die Kaffeeliste sendet dafür nichts an Dritte.</p>
|
|
<form method="post" action="zwei-faktor-einrichten.php">
|
|
<?php echo app_csrf_field(); ?>
|
|
<input type="hidden" name="aktion" value="starten">
|
|
<p><button type="submit" class="primary">Einrichtung starten</button></p>
|
|
</form>
|
|
<?php endif; ?>
|
|
|
|
<p><a href="konto.php">← Zurück zum Kundenkonto</a></p>
|
|
</div>
|
|
</section>
|
|
|
|
<?php include 'footer.php'; ?>
|