Anmelden per Windows Hello, Face ID, Touch ID oder Sicherheitsschlüssel — als dritter Weg in denselben Trichter. finishLogin() prüft die Antwort und gibt die Kennung zurück, der Endpunkt legt damit über createSession() dieselbe Sitzung an wie bei Passwort und OIDC. Die Sitzungsschicht ist unangetastet geblieben.
vier Endpunkte: Optionen und Abschluss, je für Registrierung und Anmeldung
/api/passkeys/[id]
umbenennen und entfernen
Einstellungen → Passkeys
anlegen, umbenennen, entfernen, „zuletzt benutzt"
/anmelden
„Mit Passkey anmelden" ohne vorherige E-Mail-Eingabe
ENABLE_PASSKEYS
abschaltbar wie OIDC
Drei Entscheidungen, die Erklärung verdienen
Prüfung und Sitzung sind getrennt. In passkeys.ts steht nur WebAuthn; Cookie und Sitzungszeile setzt der Endpunkt. Das hält die Domänenschicht frei von HTTP — und nebenbei importierbar in Tests, was sie über lib/auth nicht wäre.
Die Challenge liegt in einem signierten Cookie, fünf Minuten gültig, Einmalgebrauch. Eine Tabelle bräuchte einen Aufräumjob für alles, was liegen bleibt, wenn jemand den Dialog abbricht.
Die RP ID kommt aus APP_URL, nicht aus dem Host-Header — hinter einem Reverse Proxy hinge sie sonst am falschen Namen, und ein Passkey ist fest an diesen Namen gebunden.
Niemand sperrt sich aus
Das Passwort bleibt. Wer weder Passwort noch OIDC hat, kann seinen letzten Passkey nicht entfernen — mit klarer Meldung statt einer stillen Sackgasse. Ein Passkey, der nur auf einem Gerät liegt, wird in der Übersicht als solcher ausgewiesen.
Eine Feststellung zum Signaturzähler
Meine eigene Zählerprüfung lief ins Leere: die Bibliothek prüft den Zähler bereits selbst und wirft dabei einen gewöhnlichen Error — der landete als „Das hat nicht geklappt." beim Nutzer und als unerwarteter Serverfehler im Log. Das ist jetzt übersetzt. Die eigene Prüfung bleibt als zusätzliche Sicherung für den Fall, den die Bibliothek durchlässt (Rückfall von einem gezählten auf einen ungezählten Stand).
Geprüft — mit virtuellem Authenticator gegen einen Produktionsbau
Playwright über CDP (WebAuthn.addVirtualAuthenticator), also ein echter WebAuthn-Durchlauf ohne Hardware:
Schritt
Ergebnis
Passkey in den Einstellungen anlegen
erscheint in der Liste, 1 Credential im Authenticator
abgelehnt mit „der Zähler passt nicht", Zähler bleibt stehen
Passwortanmeldung danach
unverändert
Instanz mit unsicherer APP_URL
Knopf verschwindet, Verwaltung zeigt den Hinweis auf HTTPS
Dazu sieben Tests für Verwaltung, Fremdzugriff, die Aussperrsperre und die RP-ID-Herleitung. 66 Tests grün, Typecheck sauber.
Nebenbefund, der nichts mit Passkeys zu tun hat
Beim Prüfen über http://10.1.5.238:3013 scheiterte schon die Passwortanmeldung: das Sitzungscookie wird in der Produktion mit Secure gesetzt und über nacktes HTTP nicht gesendet. Eine Instanz, die im LAN ohne HTTPS läuft, ist damit gar nicht benutzbar — unabhängig von diesem PR. Details im Issue-Kommentar.
Anmelden per Windows Hello, Face ID, Touch ID oder Sicherheitsschlüssel — als **dritter Weg in denselben Trichter**. `finishLogin()` prüft die Antwort und gibt die Kennung zurück, der Endpunkt legt damit über `createSession()` dieselbe Sitzung an wie bei Passwort und OIDC. Die Sitzungsschicht ist unangetastet geblieben.
## Was dazugekommen ist
| | |
|---|---|
| `prisma/schema.prisma` | Modell `Passkey` samt Migration |
| `src/lib/passkeys.ts` | WebAuthn-Prüfung, Challenge-Handhabung, Verwaltung |
| `/api/auth/passkey/…` | vier Endpunkte: Optionen und Abschluss, je für Registrierung und Anmeldung |
| `/api/passkeys/[id]` | umbenennen und entfernen |
| Einstellungen → Passkeys | anlegen, umbenennen, entfernen, „zuletzt benutzt" |
| `/anmelden` | „Mit Passkey anmelden" ohne vorherige E-Mail-Eingabe |
| `ENABLE_PASSKEYS` | abschaltbar wie OIDC |
## Drei Entscheidungen, die Erklärung verdienen
**Prüfung und Sitzung sind getrennt.** In `passkeys.ts` steht nur WebAuthn; Cookie und Sitzungszeile setzt der Endpunkt. Das hält die Domänenschicht frei von HTTP — und nebenbei importierbar in Tests, was sie über `lib/auth` nicht wäre.
**Die Challenge liegt in einem signierten Cookie**, fünf Minuten gültig, Einmalgebrauch. Eine Tabelle bräuchte einen Aufräumjob für alles, was liegen bleibt, wenn jemand den Dialog abbricht.
**Die RP ID kommt aus `APP_URL`**, nicht aus dem `Host`-Header — hinter einem Reverse Proxy hinge sie sonst am falschen Namen, und ein Passkey ist fest an diesen Namen gebunden.
## Niemand sperrt sich aus
Das Passwort bleibt. Wer weder Passwort noch OIDC hat, kann seinen **letzten Passkey nicht entfernen** — mit klarer Meldung statt einer stillen Sackgasse. Ein Passkey, der nur auf einem Gerät liegt, wird in der Übersicht als solcher ausgewiesen.
## Eine Feststellung zum Signaturzähler
Meine eigene Zählerprüfung lief ins Leere: **die Bibliothek prüft den Zähler bereits selbst** und wirft dabei einen gewöhnlichen `Error` — der landete als „Das hat nicht geklappt." beim Nutzer und als unerwarteter Serverfehler im Log. Das ist jetzt übersetzt. Die eigene Prüfung bleibt als zusätzliche Sicherung für den Fall, den die Bibliothek durchlässt (Rückfall von einem gezählten auf einen ungezählten Stand).
## Geprüft — mit virtuellem Authenticator gegen einen Produktionsbau
Playwright über CDP (`WebAuthn.addVirtualAuthenticator`), also ein echter WebAuthn-Durchlauf ohne Hardware:
| Schritt | Ergebnis |
|---|---|
| Passkey in den Einstellungen anlegen | erscheint in der Liste, 1 Credential im Authenticator |
| Abmelden | landet auf `/anmelden` |
| „Mit Passkey anmelden" | landet auf `/`, Bibliothek erreichbar |
| Sitzung | `flimmer_session` gesetzt, Challenge-Cookie verbraucht |
| Zähler künstlich auf 9999 gesetzt | **abgelehnt** mit „der Zähler passt nicht", Zähler bleibt stehen |
| Passwortanmeldung danach | unverändert |
| Instanz mit unsicherer `APP_URL` | Knopf verschwindet, Verwaltung zeigt den Hinweis auf HTTPS |
Dazu sieben Tests für Verwaltung, Fremdzugriff, die Aussperrsperre und die RP-ID-Herleitung. **66 Tests grün**, Typecheck sauber.
## Nebenbefund, der nichts mit Passkeys zu tun hat
Beim Prüfen über `http://10.1.5.238:3013` scheiterte schon die **Passwortanmeldung**: das Sitzungscookie wird in der Produktion mit `Secure` gesetzt und über nacktes HTTP nicht gesendet. Eine Instanz, die im LAN ohne HTTPS läuft, ist damit gar nicht benutzbar — unabhängig von diesem PR. Details im Issue-Kommentar.
Closes #6
Ein Passkey ist der dritte Weg in denselben Trichter: finishLogin() prueft die
Antwort und gibt die Kennung zurueck, der Endpunkt legt damit ueber
createSession() dieselbe Sitzung an wie bei Passwort und OIDC. Die
Sitzungsschicht bleibt unangetastet.
Die Trennung zwischen Pruefung und Sitzung ist Absicht: in passkeys.ts steht
nur WebAuthn, das Setzen von Cookie und Sitzungszeile gehoert zur HTTP-Schicht.
Die Challenge liegt in einem signierten, fuenf Minuten gueltigen httpOnly-
Cookie statt in einer Tabelle — das spart Schema und Aufraeumjob fuer alles,
was liegen bleibt, wenn jemand den Dialog abbricht.
Die RP ID kommt aus APP_URL, nicht aus dem Host-Header. Hinter einem Reverse
Proxy hinge sie sonst am falschen Namen, und ein Passkey ist fest an diesen
Namen gebunden. Ueber nacktes HTTP verschwindet die Verwaltung mit einem
Hinweis statt einen kryptischen Browserfehler zu zeigen.
Den letzten verbleibenden Anmeldeweg laesst sich nicht entfernen: wer kein
Passwort und keine OIDC-Verknuepfung hat, behaelt seinen letzten Passkey.
Die Bibliothek prueft den Signaturzaehler selbst und wirft dabei gewoehnliche
Error. Die werden jetzt uebersetzt, damit eine erwartete Ablehnung nicht als
unerwarteter Serverfehler im Log landet. Die eigene Zaehlerpruefung bleibt als
zusaetzliche Sicherung fuer den Fall, den die Bibliothek durchlaesst.
Geprueft mit virtuellem Authenticator gegen einen Produktionsbau: anlegen,
abmelden, Anmeldung allein per Passkey, Sitzungscookie gesetzt,
Challenge-Cookie verbraucht, Passwortweg unveraendert. Mit kuenstlich
hochgesetztem Zaehler wird die Anmeldung abgelehnt und der Zaehler bleibt
stehen. Dazu sieben Tests fuer Verwaltung, Rechte und die Aussperrsperre.
Refs #6
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Anmelden per Windows Hello, Face ID, Touch ID oder Sicherheitsschlüssel — als dritter Weg in denselben Trichter.
finishLogin()prüft die Antwort und gibt die Kennung zurück, der Endpunkt legt damit übercreateSession()dieselbe Sitzung an wie bei Passwort und OIDC. Die Sitzungsschicht ist unangetastet geblieben.Was dazugekommen ist
prisma/schema.prismaPasskeysamt Migrationsrc/lib/passkeys.ts/api/auth/passkey/…/api/passkeys/[id]/anmeldenENABLE_PASSKEYSDrei Entscheidungen, die Erklärung verdienen
Prüfung und Sitzung sind getrennt. In
passkeys.tssteht nur WebAuthn; Cookie und Sitzungszeile setzt der Endpunkt. Das hält die Domänenschicht frei von HTTP — und nebenbei importierbar in Tests, was sie überlib/authnicht wäre.Die Challenge liegt in einem signierten Cookie, fünf Minuten gültig, Einmalgebrauch. Eine Tabelle bräuchte einen Aufräumjob für alles, was liegen bleibt, wenn jemand den Dialog abbricht.
Die RP ID kommt aus
APP_URL, nicht aus demHost-Header — hinter einem Reverse Proxy hinge sie sonst am falschen Namen, und ein Passkey ist fest an diesen Namen gebunden.Niemand sperrt sich aus
Das Passwort bleibt. Wer weder Passwort noch OIDC hat, kann seinen letzten Passkey nicht entfernen — mit klarer Meldung statt einer stillen Sackgasse. Ein Passkey, der nur auf einem Gerät liegt, wird in der Übersicht als solcher ausgewiesen.
Eine Feststellung zum Signaturzähler
Meine eigene Zählerprüfung lief ins Leere: die Bibliothek prüft den Zähler bereits selbst und wirft dabei einen gewöhnlichen
Error— der landete als „Das hat nicht geklappt." beim Nutzer und als unerwarteter Serverfehler im Log. Das ist jetzt übersetzt. Die eigene Prüfung bleibt als zusätzliche Sicherung für den Fall, den die Bibliothek durchlässt (Rückfall von einem gezählten auf einen ungezählten Stand).Geprüft — mit virtuellem Authenticator gegen einen Produktionsbau
Playwright über CDP (
WebAuthn.addVirtualAuthenticator), also ein echter WebAuthn-Durchlauf ohne Hardware:/anmelden/, Bibliothek erreichbarflimmer_sessiongesetzt, Challenge-Cookie verbrauchtAPP_URLDazu sieben Tests für Verwaltung, Fremdzugriff, die Aussperrsperre und die RP-ID-Herleitung. 66 Tests grün, Typecheck sauber.
Nebenbefund, der nichts mit Passkeys zu tun hat
Beim Prüfen über
http://10.1.5.238:3013scheiterte schon die Passwortanmeldung: das Sitzungscookie wird in der Produktion mitSecuregesetzt und über nacktes HTTP nicht gesendet. Eine Instanz, die im LAN ohne HTTPS läuft, ist damit gar nicht benutzbar — unabhängig von diesem PR. Details im Issue-Kommentar.Closes #6