74 lines
4.0 KiB
PHP
74 lines
4.0 KiB
PHP
<?php
|
|
/**
|
|
* Schnittstelle für Passkeys im Browser (JSON). Aktionen:
|
|
* login_options / login_verify Anmeldung ohne Passwort (ohne vorherige Anmeldung)
|
|
* register_options / register_verify Registrierung im Profil (angemeldet, Passwort wird bestätigt)
|
|
*/
|
|
require_once __DIR__ . '/../includes/bootstrap.php';
|
|
require_once __DIR__ . '/../includes/webauthn.php';
|
|
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
header('Cache-Control: no-store');
|
|
|
|
function passkey_out(array $data, int $code = 200): void
|
|
{
|
|
http_response_code($code);
|
|
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
exit;
|
|
}
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') passkey_out(['ok' => false, 'error' => 'Nur POST erlaubt.'], 405);
|
|
if (!csrf_valid()) passkey_out(['ok' => false, 'error' => 'Die Seite war zu lange geöffnet. Bitte neu laden und noch einmal versuchen.'], 403);
|
|
|
|
$pdo = get_db();
|
|
$action = (string) ($_POST['action'] ?? '');
|
|
$rp = webauthn_rp();
|
|
if (!webauthn_enabled()) {
|
|
passkey_out(['ok' => false, 'error' => 'Passkeys sind in dieser Installation ausgeschaltet.'], 403);
|
|
}
|
|
if (!$rp['secure']) {
|
|
passkey_out(['ok' => false, 'error' => 'Passkeys brauchen eine HTTPS-Adresse. Die Website-URL in den Einstellungen beginnt nicht mit https://.'], 400);
|
|
}
|
|
$cred = json_decode((string) ($_POST['credential'] ?? ''), true);
|
|
|
|
if ($action === 'login_options') {
|
|
if (!rate_limit_check('passkey_login_options', client_ip(), 60, 15)) passkey_out(['ok' => false, 'error' => 'Zu viele Versuche. Bitte warten Sie einige Minuten.'], 429);
|
|
passkey_out(['ok' => true, 'options' => webauthn_request_options()]);
|
|
}
|
|
|
|
if ($action === 'login_verify') {
|
|
if (!rate_limit_check('passkey_login_attempt', client_ip(), 15, 15)) passkey_out(['ok' => false, 'error' => 'Zu viele Versuche. Bitte warten Sie einige Minuten.'], 429);
|
|
if (!is_array($cred)) passkey_out(['ok' => false, 'error' => 'Antwort des Geräts ist ungültig.'], 400);
|
|
$r = webauthn_login_finish($pdo, $cred);
|
|
if (empty($r['ok'])) {
|
|
login_guard_failed(null, '', 'web', 'Passkey abgelehnt: ' . (string) ($r['error'] ?? ''));
|
|
passkey_out(['ok' => false, 'error' => (string) ($r['error'] ?? 'Anmeldung fehlgeschlagen.')], 401);
|
|
}
|
|
rate_limit_reset('passkey_login_attempt', client_ip());
|
|
unset($_SESSION['totp_pending_staff_id']);
|
|
staff_login($r['staff']);
|
|
passkey_out(['ok' => true, 'redirect' => '/staff/reports.php']);
|
|
}
|
|
|
|
// Ab hier: nur angemeldete Mitarbeiter
|
|
$staff = current_staff();
|
|
if (!$staff) passkey_out(['ok' => false, 'error' => 'Bitte melden Sie sich an.'], 401);
|
|
|
|
if ($action === 'register_options' || $action === 'register_verify') {
|
|
// Ein neuer Passkey bleibt dauerhaft gültig: darum zuerst das Passwort bestätigen (schützt vor einer fremden offenen Sitzung)
|
|
if ($action === 'register_options') {
|
|
if (!rate_limit_check('passkey_register', (string) $staff['id'], 10, 15)) passkey_out(['ok' => false, 'error' => 'Zu viele Versuche. Bitte warten Sie einige Minuten.'], 429);
|
|
if (!password_verify((string) ($_POST['password'] ?? ''), (string) $staff['password_hash'])) passkey_out(['ok' => false, 'error' => 'Das Passwort ist falsch.'], 403);
|
|
passkey_out(['ok' => true, 'options' => webauthn_creation_options($pdo, $staff)]);
|
|
}
|
|
if (!is_array($cred)) passkey_out(['ok' => false, 'error' => 'Antwort des Geräts ist ungültig.'], 400);
|
|
$r = webauthn_register_finish($pdo, $staff, $cred, (string) ($_POST['name'] ?? ''));
|
|
if (empty($r['ok'])) passkey_out(['ok' => false, 'error' => (string) ($r['error'] ?? 'Registrierung fehlgeschlagen.')], 400);
|
|
flash('success', 'Passkey hinzugefügt. Beim nächsten Anmelden können Sie „Mit Passkey anmelden“ wählen.');
|
|
audit_log((int) $staff['id'], 'Passkey hinzugefügt');
|
|
login_guard_record($pdo, 'passkey_added', (int) $staff['id'], (string) $staff['email'], 'web', 'Passkey hinzugefügt');
|
|
passkey_out(['ok' => true]);
|
|
}
|
|
|
|
passkey_out(['ok' => false, 'error' => 'Unbekannte Aktion.'], 400);
|