# SEC2 — Passwort-Reset, Passwort ändern, E-Mail-Änderung & Sessions (Detail-Prompt) > Claude-Code-Prompt für **einen** Entwickler. Basis `dev` → Branch **`dev/sec2-auth-selfservice`** (PR-Ziel `dev`). **Abhängigkeit: SEC1** (Mail-Templates `password_reset`, `password_changed`, `email_change_verify`). > Ziel: sicherer **Self-Service** — Passwort vergessen/zurücksetzen, Passwort selbst ändern, E-Mail-Adresse ändern (verifiziert) — plus ein wiederverwendbarer **Session-Invalidierungs-Baustein**. Gilt für **Mandanten-Nutzer** (`/login`) **und Plattform-Admins** (`/platform/login`). ## Vorhandenes wiederverwenden - `src/lib/password-policy.ts` (Validierung, client-safe) + `src/server/password.ts` (**Argon2id** + Generator). **Nicht** neu implementieren. - Zwei NextAuth-Instanzen: `src/server/auth.ts` (Mandant) · `src/server/platform-auth.ts` (Plattform). **Force-Change-Gate** im `(app)`-Layout als Muster. - Konto-**Lockout** (Tenant) + `User.mustChangePassword` vorhanden. Audit-Log (`scope=platform` für Plattform-Ereignisse). SEC1-Mailservice (`enqueueMail`). --- ## 1. Token-Modell (single-use, gehasht) **`AuthToken`** (eigenes Modell, nicht im JWT): `id`, `principalType` (`tenant_user` | `platform_admin`), `principalId`, `tenantId?` (nur bei tenant_user, für RLS), `type` (`password_reset` | `email_change`), `tokenHash` (SHA-256 des Rohtokens), `newEmail?` (nur bei email_change), `expiresAt`, `usedAt?`, `createdAt`, `requestIp?`. - **Roh-Token** = 32 Byte CSPRNG, base64url; nur im **Link** (nie in DB/Log). In DB nur der **Hash**. Lookup per Hash, **konstante-Zeit**-Vergleich. - **Gültigkeit** kurz (Reset 30–60 Min, E-Mail-Verify 60 Min); **single-use** (`usedAt` setzen); alte offene Tokens desselben Typs beim Neuanfordern invalidieren. - tenant_user-Zeilen unter RLS (`tenantId`); platform_admin-Zeilen `tenantId=null` (`scope=platform`). Migration + RLS-DO-Block. --- ## 2. Passwort-Reset (Self-Service) **2a Anfrage** — „Passwort vergessen?" auf `/login` **und** `/platform/login`: - Eingabe E-Mail → **immer gleiche Antwort** („Falls ein Konto existiert, wurde eine E-Mail gesendet."). **Kein** Rückschluss auf Existenz (Enumeration-Schutz). - **Rate-Limit**: je IP **und** je Konto (z. B. 5/Stunde); zusätzlich globaler Missbrauchsschutz. - Existiert das Konto **und** ist aktiv/nicht gesperrt: `AuthToken(password_reset)` erzeugen, Link `"{APP_BASE_URL}/reset?token=…"` per SEC1 (`password_reset`) senden. **Deaktivierte/gesperrte** Konten: keine Mail, keine Fehlermeldung. **2b Einlösung** — `/reset?token=…`: - Token per Hash validieren (existiert, nicht abgelaufen, nicht benutzt, Konto aktiv). Ungültig → generische Meldung + Angebot „neu anfordern". - Neues Passwort setzen: **Policy-Prüfung** (`password-policy.ts`) + Argon2id (`password.ts`). Bei Wiederverwendung: **Breach-Check-Hook** vorsehen (Implementierung in SEC5). - Danach: `usedAt` setzen, **alle Sessions invalidieren** (Abschnitt 5), `mustChangePassword=false`, **Bestätigungs-Mail** (`password_changed`), **Audit**-Eintrag (ohne Token). - Redirect zum passenden Login (`/login` bzw. `/platform/login`). --- ## 3. Passwort selbst ändern Profilseite (Mandanten-App **und** Plattform-Profil): - Felder: aktuelles Passwort, neues, Wiederholung. **Alt-Passwort verifizieren** (Argon2id), Policy-Prüfung. - Erfolg: Passwort setzen, **andere Sessions abmelden** (aktuelle behalten — Abschnitt 5), Bestätigungs-Mail (`password_changed`), Audit. - Fehler generisch; Rate-Limit auf Alt-Passwort-Versuche. --- ## 4. E-Mail-Adresse ändern (verifiziert, Double-Opt-in) - Nutzer gibt neue Adresse ein → **Alt-Passwort bestätigen** (Step-up folgt in SEC3). Prüfen, dass die neue Adresse **frei** ist (mandantenweit bzw. plattformweit), ohne Enumeration nach außen. - `AuthToken(email_change, newEmail)` erzeugen; **Verifizierungslink an die NEUE Adresse** (`email_change_verify`). - Klick auf Link: Token validieren → E-Mail aktualisieren, `usedAt` setzen; **Benachrichtigung an die ALTE Adresse** („Ihre E-Mail wurde geändert"); Audit. Login-Identität konsistent halten (E-Mail ist Login). - Kollisionsfall (Adresse zwischenzeitlich vergeben): sauber ablehnen. --- ## 5. Session-Invalidierung (wiederverwendbarer Baustein) NextAuth v5 nutzt **JWT (stateless)** → globale Invalidierung über eine **Versionsmarke**: - Feld **`sessionsValidAfter`** (Timestamp) bzw. `tokenVersion` an `User` **und** `PlatformAdmin`. - JWT trägt `iat`/Version; im **Auth-Callback bzw. Layout-Check** (die DB wird ohnehin schon für Kontostatus/`mustChangePassword` geprüft) zusätzlich `sessionsValidAfter` vergleichen → ältere Tokens sind ungültig → Abmeldung. - **Bump-Auslöser:** Passwort-Reset (2b), Passwort ändern (3, andere Sessions), Konto-Deaktivierung, MFA-Änderung (SEC3), E-Mail-Änderung. - **„Aktuelle Session behalten"** (bei Self-Change): nach Bump das **aktuelle** JWT mit neuer Version neu ausstellen. - **AK:** ein Bump macht bestehende Sessions serverseitig ungültig; Self-Change meldet **nur** die anderen ab. - *Optionaler Folgeschritt (nicht SEC2-Pflicht):* „aktive Sitzungen anzeigen + einzeln abmelden" braucht Session-Records — als P1 vermerken. --- ## 6. Sicherheit (verbindlich) - Tokens: CSPRNG, **nur gehasht** gespeichert, single-use, kurzlebig, konstante-Zeit-Vergleich; **nie** in Logs/MailLog. - **Enumeration-Schutz** überall (Reset, E-Mail-Änderung): generische, einheitliche Antworten und Timing. - **Rate-Limiting** an Reset-Anfrage, Reset-Einlösung, Alt-Passwort-Prüfung (IP + Konto) — bindet an den globalen Limiter aus SEC5, hier aber schon inline scharf. - Reset/Änderung an **gesperrten/deaktivierten** Konten unmöglich. - Alle Ereignisse ins **Audit** (wer/wann/was, ohne Secrets); kompatibel zur SEC5-Audit-Erweiterung. - Kein Auto-Login direkt nach Reset (Nutzer meldet sich neu an) — reduziert Token-Missbrauch. --- ## 7. Akzeptanzkriterien - „Passwort vergessen" auf `/login` **und** `/platform/login` erzeugt (nur bei aktivem Konto) eine Reset-Mail; Antwort ist **immer** enumeration-neutral; Rate-Limit greift. - Reset-Link ist **single-use**, abgelaufen/benutzt → generische Ablehnung; nach Reset sind **alle Sessions ungültig**, Bestätigungs-Mail + Audit vorhanden. - Passwort-Selbständerung mit Alt-Passwort-Prüfung + Policy; **andere** Sessions werden abgemeldet, aktuelle bleibt. - E-Mail-Änderung erst nach **Verifizierung der neuen Adresse** wirksam; **alte Adresse** wird informiert. - Token-Hashes in DB (keine Klartext-Tokens), keine Secrets in Logs. - Funktioniert für Mandanten-Nutzer **und** Plattform-Admins. ## 8. Definition of Done `tsc` → `lint` → `build` (Guard-Check) grün · neue Actions in `check-module-guards.ts` · `AuthToken` + `sessionsValidAfter`-Felder in Migration (+RLS-DO-Block, `AuthToken` in `TENANT_MODELS` für tenant-Zeilen) · Browser-Test aller Flows (Tenant + Plattform) gegen Mailpit · Demo-Umgebung lauffähig. ## 9. Testplan (Kurz) 1. **Reset happy path** (Tenant + Plattform): Anfrage → Mail (Mailpit) → Link → neues Passwort → alte Sessions abgemeldet → Login neu. 2. **Enumeration:** unbekannte E-Mail → identische Antwort/Timing, keine Mail. 3. **Token-Missbrauch:** abgelaufen / bereits benutzt / manipuliert → generische Ablehnung. 4. **Rate-Limit:** viele Anfragen → gedrosselt. 5. **Self-Change:** falsches Alt-Passwort → Ablehnung; korrekt → andere Session (zweiter Browser) wird ungültig, aktuelle bleibt. 6. **E-Mail-Änderung:** Verify-Link an neue Adresse nötig; alte Adresse erhält Hinweis; unbestätigt → keine Änderung. 7. **Deaktiviertes Konto:** kein Reset möglich. ## 10. Bibliotheken / Bausteine `crypto` (CSPRNG/SHA-256, konstante-Zeit) · vorhandene `password-policy.ts`/`password.ts` · SEC1-`enqueueMail` · Rate-Limit-Util (mit SEC5 teilen). Folgepaket **SEC3** nutzt den Session-Invalidierungs-Baustein (MFA-Änderung) und ergänzt **Step-up-Re-Auth** für die E-Mail-Änderung/kritische Aktionen.