OIDC mit Discovery, PKCE, state und nonce; lokaler Login mit argon2 als Notfallzugang; Rechte in einer Tabelle, Prüfung ausschließlich serverseitig, Rolle und Aktiv-Status pro Anfrage aus der Datenbank. Ein unabhängiger Review fand eine offene Weiterleitung: die Präfix-Prüfung des Rücksprungziels ließ /<TAB>/fremd.example durch, woraus der Browser //fremd.example macht. Das Ziel wird jetzt aufgelöst und die Herkunft verglichen. Dazu acht weitere Befunde, alle behoben. 274 Tests. 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.