Files
kaffeekasse-saas/zwei-faktor-einrichten.php
T
clemensandClaude Opus 5 a2dcc4df65 Zwei-Faktor-Anmeldung per TOTP
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>
2026-08-23 23:51:45 +02:00

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
? ' &ndash; 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 &ndash; 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">&larr; Zurück zum Kundenkonto</a></p>
</div>
</section>
<?php include 'footer.php'; ?>