feat: Anmelden mit Passkey

Passkeys (WebAuthn/FIDO2) als zweiter Anmeldeweg - auf der Anmeldeseite
und bei der erneuten Bestaetigung vor sensiblen Aktionen. Verwaltet unter
"Mein Konto".

Der Passkey ERSETZT den zweiten Faktor, statt zu ihm hinzuzukommen. Das
ist tragbar, weil er zwei Faktoren in sich vereint: Besitz des Geraets
und dessen Entsperrung. Damit das keine Annahme bleibt, wird die
Nutzerverifikation angefordert (userVerification: 'required') und
serverseitig nachgeprueft - eine Antwort ohne sie wird abgewiesen.

TOTP muss vorher eingerichtet sein. Ein Passkey haengt an einem Geraet;
waere er der einzige Weg hinein, koennte ein verlorenes Handy die Akte
schliessen.

Aufbau:
- Die Pruefung laeuft vollstaendig im Credentials-Provider "passkey",
  nicht in der Server Action davor. Ein "war schon geprueft" ueber die
  Grenze zu reichen waere die Stelle, an der sich Anmeldungen faelschen
  lassen.
- Challenges liegen serverseitig und werden per DELETE genau einmal
  verbraucht - die Datenbank entscheidet, wer sie bekommt.
- Bereichskennung und Herkunft kommen aus AUTH_URL, nicht aus einer
  zweiten Konfigurationsquelle.
- Ohne https wird der Knopf gar nicht erst angeboten.
- Rate-Limiting, Kontosperre und Audit-Log gelten unveraendert; die
  Anmeldung nennt den verwendeten Passkey namentlich.

Zwei Anmerkungen zur Migration:
- "prisma migrate diff" wollte zusaetzlich die Trigramm-Indizes aus
  prisma/sql/suche.sql loeschen, die Prisma nicht kennt. Die Zeilen sind
  entfernt und im Migrationskopf vermerkt - ein DROP haette die unscharfe
  Suche stillgelegt, ohne dass etwas fehlgeschlagen waere.
- "prisma format" hat das Schema durchgaengig neu ausgerichtet. Ohne
  Whitespace betrachtet sind es 88 hinzugefuegte Zeilen und keine
  geloeschte.

Geprueft: 21 Einheitstests, drei End-to-End-Tests gegen Chromiums
virtuellen Authenticator (einrichten, anmelden, bestaetigen, entfernen,
Geraet ohne Verifikation abgewiesen), beide Geraeteprofile. 496 Tests,
null Typfehler, Produktionsbau steht.

Nebenbei: Die Zahlen im README waren veraltet (24 statt 33 Modelle, 46
statt 79 Seiten). Sie stehen jetzt mit dem Befehl daneben, mit dem man
sie nachzaehlt.
This commit is contained in:
Florian Wilde
2026-08-22 18:44:56 +02:00
parent 9603917a86
commit c38c988c3c
25 changed files with 2702 additions and 104 deletions
+76 -1
View File
@@ -26,6 +26,80 @@ Wiederherstellung*.
_Noch nichts._
## [1.1.0] – 2026-08-22
### Hinzugefügt
- **Anmelden mit Passkey.** Auf der Anmeldeseite steht jetzt unter dem
Formular ein zweiter Weg: *Mit Passkey anmelden*. Ein Fingerabdruck, ein
Blick in die Kamera oder die Geräte-PIN — keine E-Mail, kein Passwort,
kein abgetippter Code.
**Der Passkey ersetzt den zweiten Faktor, er kommt nicht zusätzlich.**
Das ist kein Nachlassen: Ein Passkey verlangt den Besitz des Geräts
*und* dessen Entsperrung, das sind dieselben zwei Faktoren wie vorher.
Die Anwendung fordert die Nutzerverifikation ausdrücklich an und weist
eine Antwort ohne sie zurück. Ein Gerät, das auf blosses Antippen hin
signiert, kommt nicht herein.
In einem Punkt ist er dem bisherigen Weg sogar überlegen: Ein Passkey
ist an die Adresse gebunden, unter der er eingerichtet wurde. Auf einer
nachgebauten Anmeldeseite funktioniert er nicht — und Phishing ist der
übliche Weg, an einen TOTP-Code zu kommen.
**Die Authenticator-App muss trotzdem eingerichtet sein**, bevor sich
ein Passkey anlegen lässt. Ein Passkey hängt an einem Gerät. Ginge das
verloren und wäre er der einzige Weg hinein, käme niemand mehr an die
Akte — und darin stehen Unterlagen, die eine betreute Person braucht.
Der Passkey ist die bequeme Tür, nicht die einzige.
Verwaltet werden sie unter **Mein Konto → Passkeys**: einrichten,
benennen, entfernen. Die Liste zeigt, wann ein Gerät zuletzt benutzt
wurde und ob der Schlüssel gesichert ist (iCloud, Google, Bitwarden)
oder nur auf diesem einen Gerät liegt. Einrichten und Entfernen stehen
im Protokoll, jede Anmeldung nennt den verwendeten Passkey namentlich.
- **Bestätigen mit Passkey.** Export, endgültiges Löschen und die
Benutzerverwaltung verlangen eine frische Anmeldung. Auch die geht jetzt
mit dem Passkey. Geprüft wird zusätzlich, dass er zum angemeldeten
Zugang gehört — sonst liesse sich mit dem eigenen Finger eine fremde,
offen stehende Sitzung freischalten. Genau der Fall, gegen den diese
Zwischenfrage gebaut ist.
### Zu beachten
- **Passkeys brauchen `https`.** Der Browser gibt das Verfahren nur in
einem sicheren Kontext frei. Läuft die Anwendung ohne TLS, erscheint der
Knopf gar nicht erst, statt beim Klick mit einer kryptischen Meldung
abzubrechen.
- **`AUTH_URL` muss stimmen — jetzt noch mehr als vorher.** Aus ihr wird
die Kennung abgeleitet, an die der Authenticator den Schlüssel bindet.
Steht dort eine andere Adresse als die im Browser, lässt sich zwar ein
Passkey einrichten, aber nie wieder verwenden.
**Ein Wechsel der Domain macht alle Passkeys unbrauchbar.** Das ist
keine Panne, sondern der Phishingschutz. Nach einem Umzug richtet man
sie neu ein; die Anmeldung mit Passwort und Code bleibt währenddessen
offen.
- **Eine Datenbank-Migration ist nötig** (`20260822160000_passkey_anmeldung`).
Sie läuft beim Start des Behälters automatisch mit. Bestehende Zugänge
sind nicht betroffen: Ohne eingerichteten Passkey ändert sich nichts.
### Geprüft
- 21 neue Prüfungen (`tests/passkey.test.ts`), vor allem für die Herleitung
von Kennung und Herkunft aus `AUTH_URL`: Port, Schrägstrich am Ende,
Unterdomain, `http` gegen `https`, `localhost`. Das ist die Stelle, an
der eine Installation still kaputtginge — dieselbe Fehlerklasse wie
seinerzeit bei `APP_BIND` und `ENC_KEY_V2`.
- Drei End-to-End-Tests gegen einen echten Browser (`e2e/passkey.spec.ts`),
mit Chromiums virtuellem Authenticator: einrichten, abmelden, mit Passkey
anmelden, Benutzung vermerkt, entfernen. Dazu die Bestätigung vor dem
Vollexport und der Fall, dass ein Gerät ohne Nutzerverifikation nicht
hereinkommt.
## [1.0.5] – 2026-08-22
### Hinzugefügt
@@ -392,7 +466,8 @@ Was davon wann angegangen wird, steht in [`docs/ROADMAP.md`](docs/ROADMAP.md).
---
[Unveröffentlicht]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.0.5...HEAD
[Unveröffentlicht]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.1.0...HEAD
[1.1.0]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.0.5...v1.1.0
[1.0.5]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.0.4...v1.0.5
[1.0.4]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.0.3...v1.0.4
[1.0.3]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.0.2...v1.0.3