feat(auth): Zwei-Faktor-Pflicht (TOTP) für Admin-Rollen (#59) - #74
Open
pseidler89-sudo wants to merge 3 commits into
Open
feat(auth): Zwei-Faktor-Pflicht (TOTP) für Admin-Rollen (#59)#74pseidler89-sudo wants to merge 3 commits into
pseidler89-sudo wants to merge 3 commits into
Conversation
Admin-Konten hingen bisher allein am Magic-Link. Vor dem Betrieb mit mehreren Kommunen bekommen kommune_admin und super_admin einen zweiten Faktor. TOTP ohne Bibliothek, dafuer gegen die offiziellen Testvektoren aus RFC 4226 und RFC 6238 verifiziert (44 Tests) — HMAC-SHA-1 plus dynamische Trunkierung ist Anwendung vorhandener Primitive, keine eigene Kryptografie, und die RFC-Vektoren machen sie beweisbar statt bloss plausibel. Erzwingung sanft mit Frist (Owner-Entscheid 2026-08-05): Beim ersten Admin-Zugriff beginnt eine Kulanzfrist von 14 Tagen, danach ist die Einrichtung zwingend. Es gibt genau einen Admin; harte Erzwingung ab Deploy haette ihn bei einem Fehler aus seiner eigenen Plattform ausgesperrt. Step-up vor folgenreichen Aktionen: Rollenvergabe/-entzug, Umfrage veroeffentlichen/schliessen, Pruefung abschliessen, Digest freigeben/ veroeffentlichen. Waehrend der Kulanzfrist bleiben diese Aktionen offen — sonst waere die "sanfte" Frist fuer genau die wichtigsten Faelle vom ersten Tag an eine harte Sperre gewesen. Sicherheit: - Secret AES-256-GCM-verschluesselt (TOTP_ENC_KEY, fail-closed in prod wie IP_HASH_SALT) — ein DB-Dump allein hebelt den zweiten Faktor nicht aus. - Wiederverwendungssperre ueber users.totp_last_step: derselbe Code gilt kein zweites Mal (sonst waere er bis zu 90 s lang gueltig). - Rate-Limit 5 Versuche/15 min je Konto, 20 je IP, eigener Scope. - 10 Wiederherstellungscodes, sha256-gehasht, per CAS entwertet. - Notfallweg app/scripts/totp-reset.ts (Serverzugriff), beendet dabei alle Sessions des Kontos. - Der zweite Faktor haengt an der SESSION, nicht am Nutzer: ein zweiter Browser bekommt keinen Freifahrtschein, Logout entwertet ihn mit. Durchsetzung zentral in admin/layout.tsx statt in zwoelf Admin-Seiten einzeln — die eine vergessene Seite waere sonst das ganze Feature. Server Actions sind unabhaengig davon ueber requireAdminCtx abgesichert. Migration 0039 rein additiv. Art.-15-Export gibt totp_confirmed_at aus, das Secret bewusst nicht. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018YNj1kkesDywuEGvNweBHM
Drei adversariale Lenses haben zwei BLOCKER gefunden. Beide waren echt. BLOCKER 1 — rund 20 mutierende Admin-Actions hatten gar kein Zwei-Faktor-Gate. Vier Dateien brachten eine eigene Kopie des Session-Lookups mit und liefen an requireAdminCtx vorbei, darunter `einladen` — das ueber eine zweite Tuer genau die Rolle vergibt, fuer die assignRole extra Step-up bekommen hatte. Der Satz im Admin-Layout, die Actions seien "unabhaengig davon abgesichert", stimmte fuer die Haelfte nicht. Alle lokalen Resolver sind entfernt; requireAdminCtx liefert jetzt roleTypes mit, damit niemand mehr einen Grund hat, sich einen eigenen zu bauen. Ein Waechter-Test bricht, sobald eine "use server"-Datei wieder einen eigenen Session-Lookup enthaelt. BLOCKER 2 — die Kulanzfrist war als Konto-Eigenschaft gebaut, nicht als Migrationszustand. Jedes neu ernannte Admin-Konto haette dauerhaft 14 Tage ohne zweiten Faktor bekommen, beliebig oft verlaengerbar ueber ein weiteres Konto; und wer nur Server Actions aufruft, kam nie an der setzenden Stelle vorbei und waere dauerhaft befreit gewesen. Die Frist traegt jetzt Migration 0040 einmalig fuer die zum Rollout vorhandenen Admins ein; NULL heisst ab sofort keine Kulanz. Weiter behoben: - CAS statt Read-then-Write auf totp_last_step (Replay bei parallelen Requests). - Fehlende Tenant-Filter in fuenf neuen Queries. - Rate-Limit und Audit auf die Secret-Erzeugung; CAS-Guards gegen TOCTOU. - Konto-Loeschung liess TOTP-Secret und Wiederherstellungscodes stehen (Art. 17). Der vorhandene Vollstaendigkeitstest hat das nicht bemerkt, weil er nur die Felder kannte, die er kannte — er geht jetzt vom Schema aus. - Gerätewechsel (zweitFaktorNeuEinrichten) samt UI; die Fehlermeldung versprach bisher einen Weg, den es nicht gab. - Info-Mail an die Kontoadresse bei Aktivierung, Code-Einloesung und Neu- Einrichtung — ohne Codes, ohne Secret, ohne ausloesenden Link. - Verifizierer-Fläche: 2FA greift dort, WENN der Aufrufer Admin ist. Sonst haette ein ausgesperrter Admin weiter Wohnsitz-Verifizierungen vergeben koennen. - requireRedaktionCtx statt eines dritten lokalen Musters. - Notfall-Skript: 24 Stunden statt 14 Tagen Restfrist, korrekter Aufrufweg. - TOTP_ENC_KEY in SELBST_HOSTING.md. DEMO-MANDANT AUSGENOMMEN: Die Demo vergibt jedem Besucher auf Knopfdruck ein ephemeres kommune_admin-Konto. Unter die Pflicht gestellt, muesste er eine Authenticator-App einrichten, um sich eine Demo anzusehen — der Verwaltungs- Rundgang waere tot. Die Ausnahme steht sichtbar in der Richtlinie, mit Warnung, dass DEMO_TENANT_SLUG nie auf einen echten Mandanten zeigen darf. Volle Suite: 1268 gruen, 5 rot — dieselben fuenf, die auch auf sauberem main rot sind (Umgebungsartefakte, per Stash gegengeprueft). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018YNj1kkesDywuEGvNweBHM
Letzter offener Punkt aus Gate-B Runde 2. Die Gates gaben bei fehlender frischer Bestaetigung ein Feld `zweiFaktor` zurueck, und es gab eine fertige, getestete Hinweis-Komponente — nur warf jede Action das Feld weg, und die Komponente war nirgends eingebunden. `grep "auth.zweiFaktor"` fand null Treffer. Im Alltag hiess das: Wer 20 Minuten am Digest arbeitet und dann "Freigeben" klickt, bekommt "Diese Aktion verlangt eine frische Bestaetigung mit Ihrem Einmalcode." — ohne Link, ohne Knopf, und muss den Pfad raten. Das trifft jeden Arbeitsblock ueber 15 Minuten. Jetzt fuehren alle Step-up-Actions das Signal durch: Rollenvergabe/-entzug, Einladungen, Konto-Sicherheit, Vier-Augen-Ernennungen, Umfrage-Lebenszyklus, Digest-Freigabe und -Veroeffentlichung. Ergebnistypen nur OPTIONAL erweitert, deshalb keine Testanpassung noetig. Zwei modale Dialoge blieben bei Fehlern offen und haetten den Hinweis samt Link hinter einer Fokusfalle unerreichbar gemacht. Sie schliessen jetzt GENAU DANN, wenn `zweiFaktor` gesetzt ist; bei gewoehnlichen Fehlern bleiben sie stehen, damit man direkt erneut bestaetigen kann. An zwei weiteren Stellen, wo das Schliessen schon passierte, steht jetzt ein Kommentar, dass es eine Bedingung des Hinweises ist und kein blosses Aufraeumen — sonst wird es wegoptimiert. Bewusst NICHT verdrahtet: `einladungAnnehmen` und die Buerger-Actions in polls/actions.ts. Dort entsteht gar kein Signal, weil kein Admin-Gate laeuft — eine Attrappe waere schlimmer als die Luecke. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018YNj1kkesDywuEGvNweBHM
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Schließt #59.
Admin-Konten hingen bisher allein am Magic-Link. Vor dem Betrieb mit mehreren Kommunen bekommen
kommune_adminundsuper_admineinen zweiten Faktor.Entscheidungen (Owner, 2026-08-05)
Zur Erzwingung: Es gibt genau einen Admin. Harte Erzwingung ab Deploy hätte ihn bei einem Fehler in der Einrichtung aus seiner eigenen Plattform ausgesperrt.
TOTP ohne Bibliothek — mit Beweis
RFC 6238 ist HMAC-SHA-1 plus dynamische Trunkierung: Anwendung vorhandener Primitive aus
node:crypto, keine eigene Kryptografie. Statt einer Abhängigkeit im sicherheitskritischen Pfad ist die Implementierung gegen die offiziellen Testvektoren aus RFC 4226 (HOTP, Appendix D) und RFC 6238 (TOTP, Appendix B) verifiziert. Die Tests sind die Spezifikation — wertotp.tsändert, muss sie grün halten.SHA-1 ist hier korrekt und kein Versäumnis: Authenticator-Apps implementieren praktisch ausnahmslos den SHA-1-Default; die Sicherheit hängt an der Geheimhaltung des Secrets, nicht an Kollisionsresistenz.
Sicherheitseigenschaften
TOTP_ENC_KEY, fail-closed in Produktion wieIP_HASH_SALT). Anders als Session-Tokens kann es nicht gehasht werden — deshalb verschlüsselt: Ein DB-Dump allein hebelt den zweiten Faktor nicht aus.users.totp_last_step. Ohne sie bliebe ein abgefangener Code bis zu 90 Sekunden gültig (Toleranzfenster ±1 Schritt).UPDATE … WHERE used_at IS NULL RETURNING), damit zwei parallele Einlösungen nicht beide gewinnen.app/scripts/totp-reset.tsbeendet alle Sessions des Kontos mit — wäre die Rücksetzung nötig, weil ein Konto übernommen wurde, bliebe die fremde Anmeldung sonst bestehen.Durchsetzung zentral
Unter
/adminliegen zwölf Seiten mit je eigenem Guard. Die Pflicht dort ein dreizehntes Mal zu wiederholen, hieße, sie beim nächsten Unterverzeichnis genau einmal zu vergessen — und diese eine Lücke wäre das ganze Feature. Das Gate sitzt deshalb inadmin/layout.tsxund greift für jede Route darunter, auch für künftige. Server Actions sind unabhängig davon überrequireAdminCtxabgesichert; das Layout ist die äußere von zwei Schichten, nicht die einzige.beobachter(reine Lesesicht) fallen nicht unter die Pflicht.Eine Korrektur während der Arbeit
Step-up war zuerst so gebaut, dass es ohne eingerichtetes TOTP immer sperrt. Der Testlauf hat gezeigt, was das bedeutet hätte: Veröffentlichen, Freigeben und Rollenvergabe wären vom ersten Tag an hart gesperrt gewesen — genau das, was die Kulanzfrist verhindern soll. Jetzt gilt: TOTP aktiv → frischer Code nötig; kein TOTP → offen, solange die Frist läuft.
Geprüft
npm run lint(--max-warnings 0, a11y-Gate) undnpm run typechecksaubermainidentisch rot (gegengeprüft per Stash) — Umgebungsartefakte, keine Regressioncheck.sh(deterministische Vorprüfung): PASS, 0 BefundeVor dem Deploy
TOTP_ENC_KEYin der Prod- und Staging-Umgebung setzen (openssl rand -base64 32). Ohne den Schlüssel wirfttotp.tsin Produktion — fail-closed, absichtlich. Der Schlüssel darf danach nicht mehr getauscht werden, sonst sind alle eingerichteten Authenticator unbrauchbar.🤖 Generated with Claude Code
https://claude.ai/code/session_018YNj1kkesDywuEGvNweBHM