Anmeldung über einen generischen OIDC-Anbieter (Ziel authentik) mit Discovery, PKCE, state und nonce, dazu ein lokaler Login mit argon2 als Notfallzugang. Die Rechte stehen in einer Tabelle, die Prüfung liegt ausschließlich serverseitig, und Rolle wie Aktiv-Status werden bei jeder Anfrage aus der Datenbank gelesen — eine entzogene Rolle greift damit sofort, ein abgeschaltetes Konto kommt nicht mit gültigem Cookie noch zwölf Stunden weiter. Kein Auth.js: v5 steht dauerhaft auf Beta, das Projekt ist im Wartungsmodus und sein Prisma-Adapter deckt Prisma 7 nicht ab. Stattdessen openid-client und jose, wie in ProxLedger. Die API von openid-client 6 wurde vorher gegen die Typdefinitionen im Paket geprüft, nicht aus dem Gedächtnis geschrieben. Ein unabhängiger Review hat einen Blocker und acht weitere Befunde gefunden, alle behoben. Der wichtigste war eine offene Weiterleitung: die Prüfung des Rücksprungziels verglich Präfixe, und "/<TAB>/fremd.example" besteht alle drei Bedingungen. Browser entfernen Tabulator, Zeilenumbruch und Wagenrücklauf beim Verarbeiten einer URL, übrig bleibt "//fremd.example" und damit ein fremder Host — genau das Phishing, das die Prüfung abwehren sollte. Sie löst das Ziel jetzt gegen eine Wegwerf-Basis auf und vergleicht die Herkunft. Weitere Befunde: jede OIDC-Anmeldung hätte ohne konfigurierte Gruppen die Rolle still auf LESER gesetzt, bei einem einzigen Administrator ohne Rückweg; die Verknüpfung über die E-Mail-Adresse geschah ohne email_verified und wäre der Weg gewesen, den lokalen Notfall-Administrator zu übernehmen; der letzte aktive Administrator konnte sich selbst herabstufen. Drei Fehler waren meine eigenen. Der Platzhalter-Hash gegen den Zeitkanal war erfunden und hätte sofort geworfen statt Zeit zu kosten, die Zusage wäre also falsch gewesen. nextUrl.origin lieferte im Test localhost statt 127.0.0.1 und wäre hinter einem Reverse Proxy der interne Host; alle Umleitungen gehen jetzt über APP_URL. Und overflow-x: hidden auf body hat die eigene Mobil-Prüfung blind gemacht — auf der Benutzerseite war bei 390 px ein Knopf abgeschnitten, ohne dass scrollWidth es zeigen konnte. 274 Tests, darunter die vollständige Rechtematrix, die Angriffe auf das Rücksprungziel, die Trennung von Sitzungs- und Kurzzeit-Token und ein struktureller Nachweis, dass jede Server Action mit einer Rechteprüfung beginnt. Closes #7 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
5.3 KiB
5.3 KiB
#007 M2 – Auth, Rollen, Benutzer
Status: erledigt Meilenstein: v0.1.0 Labels: phase-1, backend, frontend, security Geschätzt: 8 h Tatsächlich: 1 h 35 min
Ziel
Anmeldung per generischem OIDC (Ziel authentik) und als Notfallzugang lokal per E-Mail und Passwort, dazu die serverseitige Rechteprüfung, auf der alle weiteren Services aufsetzen.
Aufgaben
src/server/auth/oidc.ts:openid-client6.x mit Discovery, UmgebungsvariablenOIDC_ISSUER,OIDC_CLIENT_ID,OIDC_CLIENT_SECRET,OIDC_NAME; Rücksprung-URL ausAPP_URLabgeleitet, damit es hinter dem Reverse Proxy stimmtsrc/lib/sitzung.ts:jose-signiertes Sitzungscookie (HS256,APP_SECRETmit mindestens 32 Zeichen erzwungen, Hinweis aufopenssl rand -base64 48), Nutzlast{benutzerId, email, name, herkunft}, Laufzeit 12 h- Lokaler Login mit
@node-rs/argon2, nur aktiv beiAUTH_LOCAL_ENABLED=true - Rolle bei jedem OIDC-Login aus dem
groups-Claim überOIDC_GROUP_ADMIN,OIDC_GROUP_EDITOR,OIDC_GROUP_TECHNICIAN,OIDC_GROUP_VIEWERaktualisieren; JIT-Provisioning unbekannter Benutzer beim ersten Login src/server/auth/rollen.ts:RECHTEals eine Tabelle (satisfies Record<string, readonly Rolle[]>) plusdarf()undROLLEN_TEXTsrc/server/auth/sitzungspflicht.ts:angemeldet()(React-cache, ohne Weiterleitung) undrechtErforderlich(recht); Rolle und Aktiv-Status pro Request aus der Datenbank, nicht aus dem Token;sitzungenUngueltigAbfür Widerruf; reine, testbaresitzungBewerten()src/proxy.ts: Allowlist öffentlicher Pfade, Weiterleitung auf/anmelden?ziel=…- Login-Seite im Design, OIDC-Knopf und lokales Formular, Rücksprung auf die ursprünglich angeforderte URL (nötig für
/t/[tagCode]) - Admin → Benutzer: Liste mit Rolle und Aktiv-Status, lokale Benutzer anlegen wenn lokaler Login aktiv
- Screenshots Login und Benutzerliste in
docs/screenshots/m2/
Akzeptanzkriterien
- Anmeldung mit dem Seed-Admin gelingt, Abmelden beendet die Sitzung
- Bei
AUTH_LOCAL_ENABLED=falseist das lokale Formular nicht vorhanden und der Endpunkt lehnt ab - Tests fahren jede Rolle gegen jedes Recht:
LESERdarf nichts schreiben,TECHNIKERdarf keine Verwaltung,REDAKTEURdarf Vorlagen,ADMINalles - Eine Rollenänderung in der Datenbank greift ohne neue Anmeldung
- Ein deaktivierter Benutzer wird mit bestehendem Cookie abgewiesen
- Aufruf von
/t/LZ-7K3F9Qohne Anmeldung führt nach dem Login genau dorthin zurück - Passwörter stehen nur als argon2-Hash in der Datenbank, nie im Klartext und nie in einer API-Antwort
Beim Bauen aufgefallen
- Offene Weiterleitung gefunden und geschlossen. Die Prüfung des Rücksprungziels verglich
Präfixe (
/, nicht//, nicht/\)./<TAB>/fremd.examplebesteht alle drei — Browser entfernen Tabulator, Zeilenumbruch und Wagenrücklauf beim Verarbeiten einer URL, übrig bleibt//fremd.exampleund damit ein fremder Host. Genau das Phishing, das die Prüfung abwehren sollte. Sie löst das Ziel jetzt gegen eine Wegwerf-Basis auf und vergleicht die Herkunft; eine zweite Runde fängt zusätzlich/..//fremd.exampleab, das beim Auflösen zu//fremd.examplewird. Prozentkodierte Steuerzeichen (/%09/…) sind harmlos: sie werden nicht dekodiert und bleiben ein Pfad auf der eigenen Herkunft. nextUrl.origintaugt nicht für Umleitungen. Die OIDC-Umleitung landete auflocalhost:3100, obwohl die Anfrage an127.0.0.1:3100ging. Hinter einem Reverse Proxy wäre das der interne Host. Alle Umleitungen gehen jetzt übersrc/lib/adressen.tsund damit überAPP_URL.- Der Platzhalter-Hash war erfunden und hätte beim Prüfen sofort geworfen statt Zeit zu kosten — die Zusage, dass die Antwortzeit nichts über vorhandene Konten verrät, wäre falsch gewesen. Jetzt ein echter argon2-Hash, einmal lazy erzeugt. Nachgemessen: rund 1 ms Unterschied über je 60 Anfragen, über ein Netz nicht auswertbar.
overflow-x: hiddenaufbodyhat meinen eigenen Mobil-Test blind gemacht. Auf der Benutzerseite war bei 390 px der Knopf „Setzen" abgeschnitten, die PrüfungscrollWidth > clientWidthkonnte das aber nicht sehen. Dashiddenist raus, die Liste sind Karten statt einer Tabelle.- Jede OIDC-Anmeldung hätte die Rolle überschrieben, auch ohne konfigurierte
OIDC_GROUP_*-Variablen — eine stille Herabstufung des Administrators aufLESER, aus der es bei nur einem Administrator keinen Rückweg gegeben hätte. Ohne eingerichtete Zuordnung bleibt die Rolle jetzt unangetastet. - Verknüpfung über die E-Mail-Adresse ohne
email_verifiedwäre der Weg gewesen, den lokalen Notfall-Administrator zu übernehmen, wenn der Anbieter frei wählbare Adressen zulässt. - Der letzte aktive Administrator konnte sich selbst zum Leser machen. Jetzt abgelehnt, mit Zählabfrage vor der Änderung.
MELDUNGEN[schluessel]griff auf die Prototypenkette zu:?fehler=constructorergab eine 500er-Seite — auf der einen Seite, die immer gehen muss. Jetzt eineMap.- Beim Verschieben der Übersicht nach
(app)/blieb<Huelle>in der Seite stehen, die Hülle wurde also doppelt gerendert — zwei Kopfzeilen, das innere Menü ohne Benutzer. Im Screenshot sichtbar gewesen und trotzdem übersehen; der unabhängige Review hat es gefunden.