Anmeldung per Passkey (WebAuthn) #6

Closed
opened 2026-09-01 21:25:25 +00:00 by florian.wilde · 4 comments
Owner

Worum es geht

Anmelden per Passkey — Windows Hello, Face ID, Touch ID oder Sicherheitsschlüssel — statt Passwort. Auf dem Handy ist das der Unterschied zwischen einem Blick in die Kamera und dem Abtippen eines langen Passworts.

Was dafür spricht, dass es sich gut einfügt

Die Sitzungsschicht ist schon unabhängig davon, wie jemand seine Identität nachgewiesen hat. createSession(userId) (src/lib/auth.ts:49) legt Token, Cookie und Sitzungszeile an und interessiert sich nicht für den Weg dorthin — OIDC hängt bereits genau da ein. Ein Passkey wäre der dritte Weg in denselben Trichter, kein Umbau der Anmeldung.

Zwei weitere Dinge sind bereits vorbereitet:

  • User.passwordHash ist nullable (prisma/schema.prisma) — ein Konto ganz ohne Passwort ist im Datenmodell schon darstellbar, das kam mit OIDC.
  • OidcAccount gibt das Muster für eine verknüpfte Zugangstabelle vor.

Datenmodell

Ein neues Modell, an User gehängt, mit onDelete: Cascade wie die anderen:

Feld Zweck
credentialId Kennung des Authenticators, @unique
publicKey öffentlicher Schlüssel (Bytes)
counter Signaturzähler gegen Wiedereinspielen
transports usb, nfc, ble, internal, hybrid
deviceType / backedUp Einzelgerät oder synchronisiert — bestimmt, wie dringend ein zweiter Weg nötig ist
name vom Nutzer vergeben („iPhone", „Yubikey am Schlüsselbund")
createdAt / lastUsedAt für die Übersicht in den Einstellungen

Die Challenge braucht eine kurzlebige Ablage zwischen den zwei Schritten. Vorschlag: signiertes, httpOnly-Cookie mit kurzer Laufzeit statt einer Tabelle — spart Schema und einen Aufräumjob.

Bibliothek

@simplewebauthn/server und @simplewebauthn/browser. Reines JavaScript, keine nativen Build-Abhängigkeiten — passt zu der Entscheidung, die auch schon zu bcryptjs statt argon2 geführt hat. Selbst implementieren wäre bei CBOR-Zerlegung und Attestationsprüfung der falsche Ehrgeiz.

Der Fallstrick beim Selbsthosten — bitte vorher lesen

Ein Passkey ist fest an den Hostnamen gebunden (die RP ID). Daraus folgt dreierlei, und das ist bei einer selbst gehosteten App wichtiger als bei einem SaaS:

  1. Ohne HTTPS geht es nicht, außer auf localhost. Wer Flimmer im LAN über nacktes HTTP betreibt, kann Passkeys nicht nutzen. Das muss die Oberfläche sagen, statt einen kryptischen Browserfehler zu zeigen.
  2. Die RP ID muss aus APP_URL abgeleitet werden (src/lib/env.ts:40), nicht aus dem Host-Header — sonst hängt sie hinter einem Reverse Proxy am falschen Namen.
  3. Ein Domainwechsel entwertet alle Passkeys. Wer von flimmer.fritz.box auf flimmer.example.com umzieht, muss sie neu anlegen. Gehört in die README, nicht in eine Fehlermeldung.

Nicht aussperren

Das Passwort bleibt. Ein Passkey kommt zusätzlich, und wer alle Wege bis auf einen entfernt, bekommt eine deutliche Warnung — besonders bei einem Passkey, der nicht synchronisiert ist (backedUp: false): geht das Gerät verloren, ist das Konto weg. Für den Notfall sollte ein Administrator ein Passwort zurücksetzen können.

Umzusetzen

  • Modell Passkey samt Migration
  • src/lib/passkeys.ts: Optionen erzeugen und Antworten prüfen, für Registrierung und Anmeldung; am Ende createSession(userId)
  • Vier Endpunkte unter /api/auth/passkey/…: je ein Paar aus „Optionen holen" und „Antwort prüfen" für Registrierung und Anmeldung
  • Einstellungen → Passkeys: anlegen, benennen, entfernen, zuletzt benutzt
  • /anmelden: Knopf „Mit Passkey anmelden" über auffindbare Zugangsdaten (ohne vorherige E-Mail-Eingabe); optional später die Browser-Autovervollständigung
  • README: HTTPS-Voraussetzung und die Sache mit dem Domainwechsel
  • Abschaltbar per ENV, analog zu OIDC

Abnahme

  • Ein angemeldeter Nutzer kann in den Einstellungen einen Passkey anlegen und benennen
  • Abmelden, dann Anmeldung allein per Passkey — ohne E-Mail-Eingabe
  • Die entstandene Sitzung ist von einer Passwort-Sitzung nicht zu unterscheiden (dieselbe Tabelle, dasselbe Cookie, Abmelden funktioniert)
  • Passwortanmeldung funktioniert unverändert weiter
  • Mehrere Passkeys je Konto; einzeln entfernbar; lastUsedAt wird gepflegt
  • Ein Passkey eines anderen Kontos wird abgewiesen (Test)
  • Ein zurückgesetzter Signaturzähler führt zur Ablehnung (Test)
  • Über nacktes HTTP erscheint ein verständlicher Hinweis statt eines Browserfehlers
  • Der letzte verbleibende Anmeldeweg lässt sich nicht versehentlich entfernen

Aufwand

Mittel. Das Datenmodell ist klein und die Sitzungsschicht muss nicht angefasst werden — der Aufwand steckt in den vier Endpunkten, der Oberfläche und den Tests. Ein Nachmittag, kein Wochenende.


Angelegt auf ausdrücklichen Wunsch direkt in Gitea, ohne Verknüpfung zu einem Nebula-Task.

## Worum es geht Anmelden per Passkey — Windows Hello, Face ID, Touch ID oder Sicherheitsschlüssel — statt Passwort. Auf dem Handy ist das der Unterschied zwischen einem Blick in die Kamera und dem Abtippen eines langen Passworts. ## Was dafür spricht, dass es sich gut einfügt Die Sitzungsschicht ist schon unabhängig davon, **wie** jemand seine Identität nachgewiesen hat. `createSession(userId)` (`src/lib/auth.ts:49`) legt Token, Cookie und Sitzungszeile an und interessiert sich nicht für den Weg dorthin — OIDC hängt bereits genau da ein. Ein Passkey wäre der dritte Weg in denselben Trichter, kein Umbau der Anmeldung. Zwei weitere Dinge sind bereits vorbereitet: - `User.passwordHash` ist **nullable** (`prisma/schema.prisma`) — ein Konto ganz ohne Passwort ist im Datenmodell schon darstellbar, das kam mit OIDC. - `OidcAccount` gibt das Muster für eine verknüpfte Zugangstabelle vor. ## Datenmodell Ein neues Modell, an `User` gehängt, mit `onDelete: Cascade` wie die anderen: | Feld | Zweck | |---|---| | `credentialId` | Kennung des Authenticators, `@unique` | | `publicKey` | öffentlicher Schlüssel (`Bytes`) | | `counter` | Signaturzähler gegen Wiedereinspielen | | `transports` | `usb`, `nfc`, `ble`, `internal`, `hybrid` | | `deviceType` / `backedUp` | Einzelgerät oder synchronisiert — bestimmt, wie dringend ein zweiter Weg nötig ist | | `name` | vom Nutzer vergeben („iPhone", „Yubikey am Schlüsselbund") | | `createdAt` / `lastUsedAt` | für die Übersicht in den Einstellungen | Die **Challenge** braucht eine kurzlebige Ablage zwischen den zwei Schritten. Vorschlag: signiertes, httpOnly-Cookie mit kurzer Laufzeit statt einer Tabelle — spart Schema und einen Aufräumjob. ## Bibliothek `@simplewebauthn/server` und `@simplewebauthn/browser`. Reines JavaScript, keine nativen Build-Abhängigkeiten — passt zu der Entscheidung, die auch schon zu `bcryptjs` statt `argon2` geführt hat. Selbst implementieren wäre bei CBOR-Zerlegung und Attestationsprüfung der falsche Ehrgeiz. ## Der Fallstrick beim Selbsthosten — bitte vorher lesen Ein Passkey ist **fest an den Hostnamen gebunden** (die RP ID). Daraus folgt dreierlei, und das ist bei einer selbst gehosteten App wichtiger als bei einem SaaS: 1. **Ohne HTTPS geht es nicht**, außer auf `localhost`. Wer Flimmer im LAN über nacktes HTTP betreibt, kann Passkeys nicht nutzen. Das muss die Oberfläche sagen, statt einen kryptischen Browserfehler zu zeigen. 2. **Die RP ID muss aus `APP_URL` abgeleitet werden** (`src/lib/env.ts:40`), nicht aus dem `Host`-Header — sonst hängt sie hinter einem Reverse Proxy am falschen Namen. 3. **Ein Domainwechsel entwertet alle Passkeys.** Wer von `flimmer.fritz.box` auf `flimmer.example.com` umzieht, muss sie neu anlegen. Gehört in die README, nicht in eine Fehlermeldung. ## Nicht aussperren Das Passwort bleibt. Ein Passkey kommt **zusätzlich**, und wer alle Wege bis auf einen entfernt, bekommt eine deutliche Warnung — besonders bei einem Passkey, der nicht synchronisiert ist (`backedUp: false`): geht das Gerät verloren, ist das Konto weg. Für den Notfall sollte ein Administrator ein Passwort zurücksetzen können. ## Umzusetzen - Modell `Passkey` samt Migration - `src/lib/passkeys.ts`: Optionen erzeugen und Antworten prüfen, für Registrierung und Anmeldung; am Ende `createSession(userId)` - Vier Endpunkte unter `/api/auth/passkey/…`: je ein Paar aus „Optionen holen" und „Antwort prüfen" für Registrierung und Anmeldung - Einstellungen → **Passkeys**: anlegen, benennen, entfernen, zuletzt benutzt - `/anmelden`: Knopf „Mit Passkey anmelden" über auffindbare Zugangsdaten (ohne vorherige E-Mail-Eingabe); optional später die Browser-Autovervollständigung - README: HTTPS-Voraussetzung und die Sache mit dem Domainwechsel - Abschaltbar per ENV, analog zu OIDC ## Abnahme - [ ] Ein angemeldeter Nutzer kann in den Einstellungen einen Passkey anlegen und benennen - [ ] Abmelden, dann Anmeldung allein per Passkey — ohne E-Mail-Eingabe - [ ] Die entstandene Sitzung ist von einer Passwort-Sitzung nicht zu unterscheiden (dieselbe Tabelle, dasselbe Cookie, Abmelden funktioniert) - [ ] Passwortanmeldung funktioniert unverändert weiter - [ ] Mehrere Passkeys je Konto; einzeln entfernbar; `lastUsedAt` wird gepflegt - [ ] Ein Passkey eines anderen Kontos wird abgewiesen (Test) - [ ] Ein zurückgesetzter Signaturzähler führt zur Ablehnung (Test) - [ ] Über nacktes HTTP erscheint ein verständlicher Hinweis statt eines Browserfehlers - [ ] Der letzte verbleibende Anmeldeweg lässt sich nicht versehentlich entfernen ## Aufwand Mittel. Das Datenmodell ist klein und die Sitzungsschicht muss nicht angefasst werden — der Aufwand steckt in den vier Endpunkten, der Oberfläche und den Tests. Ein Nachmittag, kein Wochenende. --- *Angelegt auf ausdrücklichen Wunsch direkt in Gitea, ohne Verknüpfung zu einem Nebula-Task.*
florian.wilde added the in-arbeit label 2026-09-02 04:44:47 +00:00
florian.wilde started working 2026-09-02 04:44:52 +00:00
Author
Owner

🤖 Claude Code hat mit der Umsetzung begonnen.

  • Branch: feat/6-passkey-anmeldung
  • Verstanden als: Ein Passkey wird als dritter Weg in denselben Sitzungstrichter gebaut — anlegen und verwalten in den Einstellungen, anmelden ohne E-Mail-Eingabe über auffindbare Zugangsdaten, und niemand darf sich dabei aussperren.
  • Geplantes Vorgehen:
    • Modell Passkey samt Migration; Challenge in einem kurzlebigen, signierten httpOnly-Cookie statt in einer Tabelle
    • src/lib/passkeys.ts mit @simplewebauthn/server, RP ID aus APP_URL abgeleitet; vier Endpunkte unter /api/auth/passkey/…, die am Ende createSession() aufrufen
    • Einstellungen und Anmeldeseite, abschaltbar per ENV wie OIDC; über nacktes HTTP ein verständlicher Hinweis statt eines Browserfehlers
    • Sperre gegen das Entfernen des letzten Anmeldewegs
    • Tests für Zählerprüfung und fremde Passkeys, dazu ein echter Durchlauf mit virtuellem Authenticator über Playwright — Anlegen, Abmelden, Anmelden allein per Passkey

Ein Hinweis vorweg: Passkeys verlangen einen echten Hostnamen. Auf 127.0.0.1 lehnt der Browser sie ab, weil eine IP-Adresse keine gültige RP ID ist — geprüft wird deshalb gegen http://localhost.

Zeiterfassung läuft.

🤖 **Claude Code hat mit der Umsetzung begonnen.** - Branch: `feat/6-passkey-anmeldung` - Verstanden als: Ein Passkey wird als **dritter Weg** in denselben Sitzungstrichter gebaut — anlegen und verwalten in den Einstellungen, anmelden ohne E-Mail-Eingabe über auffindbare Zugangsdaten, und niemand darf sich dabei aussperren. - Geplantes Vorgehen: - Modell `Passkey` samt Migration; Challenge in einem kurzlebigen, signierten httpOnly-Cookie statt in einer Tabelle - `src/lib/passkeys.ts` mit `@simplewebauthn/server`, RP ID aus `APP_URL` abgeleitet; vier Endpunkte unter `/api/auth/passkey/…`, die am Ende `createSession()` aufrufen - Einstellungen und Anmeldeseite, abschaltbar per ENV wie OIDC; über nacktes HTTP ein verständlicher Hinweis statt eines Browserfehlers - Sperre gegen das Entfernen des letzten Anmeldewegs - Tests für Zählerprüfung und fremde Passkeys, dazu ein **echter Durchlauf mit virtuellem Authenticator** über Playwright — Anlegen, Abmelden, Anmelden allein per Passkey Ein Hinweis vorweg: Passkeys verlangen einen echten Hostnamen. Auf `127.0.0.1` lehnt der Browser sie ab, weil eine IP-Adresse keine gültige RP ID ist — geprüft wird deshalb gegen `http://localhost`. Zeiterfassung läuft.
florian.wilde worked for 19 minutes 2026-09-02 05:04:51 +00:00
florian.wilde added zu-testen and removed in-arbeit labels 2026-09-02 05:05:40 +00:00
Author
Owner

✅ Umsetzung abgeschlossen — bereit zum Testen.

Was geändert wurde

  • Modell Passkey samt Migration; src/lib/passkeys.ts mit @simplewebauthn/server
  • Vier Endpunkte unter /api/auth/passkey/…, dazu /api/passkeys/[id] zum Umbenennen und Entfernen
  • Einstellungen → Passkeys: anlegen, umbenennen, entfernen, „zuletzt benutzt"; /anmelden mit „Mit Passkey anmelden" ohne E-Mail-Eingabe
  • Challenge in einem signierten, fünf Minuten gültigen httpOnly-Cookie statt in einer Tabelle
  • RP ID aus APP_URL; über nacktes HTTP verschwindet die Verwaltung mit einem Hinweis
  • Sperre gegen das Entfernen des letzten Anmeldewegs; ENABLE_PASSKEYS schaltet alles ab
  • README und .env.example nachgezogen

Geänderte Dateien

  • prisma/schema.prisma, prisma/migrations/20260902044634_passkeys/
  • src/lib/passkeys.ts, src/lib/env.ts
  • src/app/api/auth/passkey/** (vier Endpunkte), src/app/api/passkeys/[id]/route.ts
  • src/components/settings/Passkeys.tsx, src/app/einstellungen/page.tsx, src/app/(auth)/anmelden/page.tsx
  • tests/passkeys.test.ts, README.md, .env.example

Abweichung von der Vorgabe: Die Beschreibung sagte, finishLogin() solle am Ende createSession() aufrufen. Das steht jetzt im Endpunkt statt in der Domänenschicht — passkeys.ts enthält nur WebAuthn, Cookie und Sitzungszeile gehören zur HTTP-Schicht. Nebeneffekt: das Modul bleibt in Tests importierbar, was es über lib/auth (wegen next/navigation) nicht wäre.

Eine Feststellung zum Signaturzähler: Meine eigene Prüfung lief ins Leere — @simplewebauthn/server 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. Ist jetzt übersetzt; die eigene Prüfung bleibt als zusätzliche Sicherung für den Fall, den die Bibliothek durchlässt.

Geprüft — mit virtuellem Authenticator (Playwright/CDP) gegen einen Produktionsbau:

Schritt Ergebnis
Passkey anlegen 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 auf 9999 gesetzt abgelehnt, Zähler bleibt stehen
Passwortanmeldung danach unverändert
unsichere APP_URL Knopf weg, Hinweis auf HTTPS sichtbar

66 Tests grün (sieben neue), Typecheck sauber. Die Test-Passkeys sind aus der Entwicklungsdatenbank wieder entfernt.

So testest du es

  1. git switch feat/6-passkey-anmeldung
  2. npm run db:migrate (die Migration muss eingespielt werden)
  3. npm run dev — wichtig: über http://localhost:3011 aufrufen, nicht über 127.0.0.1. Eine IP-Adresse ist als RP ID unzulässig, der Browser lehnt Passkeys dort ab.
  4. Einstellungen → Passkeys → Namen eintragen → „Passkey anlegen". Windows Hello meldet sich.
  5. Abmelden, dann auf /anmelden „Mit Passkey anmelden" — ohne E-Mail-Eingabe.
  6. Erwartet: du landest angemeldet auf der Übersicht. Der Passkey trägt in den Einstellungen jetzt ein Datum unter „zuletzt benutzt".
  7. Gegenprobe: Passwortanmeldung geht unverändert.

Aufgewendete Zeit: 00:20
Pull Request: #12

Nach Freigabe: /issue-freigabe 6

✅ **Umsetzung abgeschlossen — bereit zum Testen.** <!-- claude-status branch: feat/6-passkey-anmeldung pr: 12 zeit_sekunden: 1199 --> **Was geändert wurde** - Modell `Passkey` samt Migration; `src/lib/passkeys.ts` mit `@simplewebauthn/server` - Vier Endpunkte unter `/api/auth/passkey/…`, dazu `/api/passkeys/[id]` zum Umbenennen und Entfernen - Einstellungen → Passkeys: anlegen, umbenennen, entfernen, „zuletzt benutzt"; `/anmelden` mit „Mit Passkey anmelden" ohne E-Mail-Eingabe - Challenge in einem signierten, fünf Minuten gültigen httpOnly-Cookie statt in einer Tabelle - RP ID aus `APP_URL`; über nacktes HTTP verschwindet die Verwaltung mit einem Hinweis - Sperre gegen das Entfernen des letzten Anmeldewegs; `ENABLE_PASSKEYS` schaltet alles ab - README und `.env.example` nachgezogen **Geänderte Dateien** - `prisma/schema.prisma`, `prisma/migrations/20260902044634_passkeys/` - `src/lib/passkeys.ts`, `src/lib/env.ts` - `src/app/api/auth/passkey/**` (vier Endpunkte), `src/app/api/passkeys/[id]/route.ts` - `src/components/settings/Passkeys.tsx`, `src/app/einstellungen/page.tsx`, `src/app/(auth)/anmelden/page.tsx` - `tests/passkeys.test.ts`, `README.md`, `.env.example` **Abweichung von der Vorgabe:** Die Beschreibung sagte, `finishLogin()` solle am Ende `createSession()` aufrufen. Das steht jetzt im Endpunkt statt in der Domänenschicht — `passkeys.ts` enthält nur WebAuthn, Cookie und Sitzungszeile gehören zur HTTP-Schicht. Nebeneffekt: das Modul bleibt in Tests importierbar, was es über `lib/auth` (wegen `next/navigation`) nicht wäre. **Eine Feststellung zum Signaturzähler:** Meine eigene Prüfung lief ins Leere — `@simplewebauthn/server` 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. Ist jetzt übersetzt; die eigene Prüfung bleibt als zusätzliche Sicherung für den Fall, den die Bibliothek durchlässt. **Geprüft** — mit virtuellem Authenticator (Playwright/CDP) gegen einen Produktionsbau: | Schritt | Ergebnis | |---|---| | Passkey anlegen | 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 auf 9999 gesetzt | **abgelehnt**, Zähler bleibt stehen | | Passwortanmeldung danach | unverändert | | unsichere `APP_URL` | Knopf weg, Hinweis auf HTTPS sichtbar | 66 Tests grün (sieben neue), Typecheck sauber. Die Test-Passkeys sind aus der Entwicklungsdatenbank wieder entfernt. **So testest du es** 1. `git switch feat/6-passkey-anmeldung` 2. `npm run db:migrate` (die Migration muss eingespielt werden) 3. `npm run dev` — **wichtig:** über `http://localhost:3011` aufrufen, nicht über `127.0.0.1`. Eine IP-Adresse ist als RP ID unzulässig, der Browser lehnt Passkeys dort ab. 4. Einstellungen → Passkeys → Namen eintragen → „Passkey anlegen". Windows Hello meldet sich. 5. Abmelden, dann auf `/anmelden` „Mit Passkey anmelden" — ohne E-Mail-Eingabe. 6. Erwartet: du landest angemeldet auf der Übersicht. Der Passkey trägt in den Einstellungen jetzt ein Datum unter „zuletzt benutzt". 7. Gegenprobe: Passwortanmeldung geht unverändert. **Aufgewendete Zeit:** 00:20 **Pull Request:** #12 Nach Freigabe: `/issue-freigabe 6`
Author
Owner

📝 Nebenbei aufgefallen — hat nichts mit Passkeys zu tun, fiel aber beim Prüfen auf:

Über nacktes HTTP ist die Instanz gar nicht benutzbar — auch mit Passwort nicht.

Beim Test des HTTPS-Hinweises habe ich eine Instanz mit APP_URL=http://10.1.5.238:3013 gestartet und versucht, mich normal mit E-Mail und Passwort anzumelden. Die Anmeldung meldet Erfolg, aber der nächste Seitenaufruf landet wieder auf /anmelden.

Ursache: createSession() setzt das Sitzungscookie mit secure: process.env.NODE_ENV === "production" (src/lib/auth.ts:71). Im Produktionsbau ist das true, und ein Secure-Cookie sendet der Browser über nacktes HTTP nicht. Auf localhost fällt es nicht auf, weil Browser localhost als sicheren Kontext behandeln — auf einer LAN-IP schon.

Das heißt: wer Flimmer im Heimnetz ohne HTTPS betreibt, kann sich nicht anmelden. Es gibt dabei keine Fehlermeldung, man landet nur immer wieder auf der Anmeldeseite. Das ist kein neuer Fehler und auch keine falsche Entscheidung — Secure in der Produktion ist richtig —, aber es ist nirgends dokumentiert und die Sackgasse ist stumm.

Drei denkbare Wege, alle nicht Teil dieses PR:

  1. In der README festhalten, dass HTTPS Voraussetzung ist. Billigste Lösung.
  2. Beim Start warnen, wenn APP_URL mit http:// beginnt und der Host nicht localhost ist.
  3. Eine ENV wie ALLOW_INSECURE_COOKIES für bewusste LAN-Installationen — mit deutlicher Warnung, weil das Sitzungstoken dann im Klartext über das Netz geht.

Ich würde 1 und 2 nehmen; 3 nur, wenn du es wirklich brauchst.

📝 **Nebenbei aufgefallen** — hat nichts mit Passkeys zu tun, fiel aber beim Prüfen auf: **Über nacktes HTTP ist die Instanz gar nicht benutzbar — auch mit Passwort nicht.** Beim Test des HTTPS-Hinweises habe ich eine Instanz mit `APP_URL=http://10.1.5.238:3013` gestartet und versucht, mich normal mit E-Mail und Passwort anzumelden. Die Anmeldung meldet Erfolg, aber der nächste Seitenaufruf landet wieder auf `/anmelden`. Ursache: `createSession()` setzt das Sitzungscookie mit `secure: process.env.NODE_ENV === "production"` (`src/lib/auth.ts:71`). Im Produktionsbau ist das `true`, und ein `Secure`-Cookie sendet der Browser über nacktes HTTP nicht. Auf `localhost` fällt es nicht auf, weil Browser localhost als sicheren Kontext behandeln — auf einer LAN-IP schon. Das heißt: **wer Flimmer im Heimnetz ohne HTTPS betreibt, kann sich nicht anmelden.** Es gibt dabei keine Fehlermeldung, man landet nur immer wieder auf der Anmeldeseite. Das ist kein neuer Fehler und auch keine falsche Entscheidung — `Secure` in der Produktion ist richtig —, aber es ist nirgends dokumentiert und die Sackgasse ist stumm. Drei denkbare Wege, alle nicht Teil dieses PR: 1. **In der README festhalten**, dass HTTPS Voraussetzung ist. Billigste Lösung. 2. **Beim Start warnen**, wenn `APP_URL` mit `http://` beginnt und der Host nicht `localhost` ist. 3. Eine ENV wie `ALLOW_INSECURE_COOKIES` für bewusste LAN-Installationen — mit deutlicher Warnung, weil das Sitzungstoken dann im Klartext über das Netz geht. Ich würde 1 und 2 nehmen; 3 nur, wenn du es wirklich brauchst.
Author
Owner

🚀 Freigegeben und nach main übernommen.

  • Merge-Commit: 824cc77
  • Pull Request: #12 (gemergt)
  • Gesamtaufwand: 00:20

Vor dem Merge noch einmal geprüft: Typecheck sauber, 66 Tests grün.

⚠️ Beim Aktualisieren der Instanz kommt eine neue Migration mit (20260902044634_passkeys). Der Container spielt sie beim Start selbst ein; ./update.sh legt vorher eine Sicherung an.

Issue wird geschlossen.

🚀 **Freigegeben und nach `main` übernommen.** <!-- claude-status merge: 824cc7764cda3c908ae1f232d54bfdfff130aeea pr: 12 zeit_sekunden: 1199 --> - Merge-Commit: `824cc77` - Pull Request: #12 (gemergt) - Gesamtaufwand: 00:20 Vor dem Merge noch einmal geprüft: Typecheck sauber, 66 Tests grün. ⚠️ **Beim Aktualisieren der Instanz kommt eine neue Migration mit** (`20260902044634_passkeys`). Der Container spielt sie beim Start selbst ein; `./update.sh` legt vorher eine Sicherung an. Issue wird geschlossen.
florian.wilde removed the zu-testen label 2026-09-02 06:59:46 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Total Time Spent: 19 minutes
florian.wilde
19 minutes
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: florian.wilde/flimmer#6