- docker-compose.coolify(.prebuilt).yml: Service craftvia-worker (Target worker, Chromium, shm_size 1gb, gleiche Härtung), Craftvia-Variablen für app und worker. - Dockerfile: worker-Stage mit HOME=/home/app (Chromium-Profil für non-root); lokaler docker build der Targets runner und worker erfolgreich, PDF-Erzeugung im Image geprüft. - .env.example/.env.prod.example/.env.coolify.example: alle Craftvia-Variablen inkl. RLS, KI-Provider, PDF_CHROMIUM_PATH, OFFLINE_MAX_DAYS, API_RATE_LIMIT_*, AI_GENERATION_RETENTION_DAYS, AI_MONTHLY_TOKEN_LIMIT. - CI (.github, .gitea): Job gate mit Postgres (pgvector) und Redis als Service: migrate deploy, seed, Passwort für craftvia_app, tsc, lint, build, npm run test. - docs/craftvia/DEPLOY.md (aus den Certvia-Deploy-Docs abgeleitet): Architektur, Domains, Secrets, Worker, Migrationen, RLS-Aktivierung, Backup/Restore, KI, Rate Limits, Aufbewahrung, Smoke, Update/Rollback. build-and-push-images.sh baut craftvia-worker. - Certvia-/ISMS-Dokumente aus docs/ nach docs/_certvia-archiv/ (mit README); Verweise in README.md und Skript-Kommentaren angepasst. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
101 lines
7.9 KiB
Markdown
101 lines
7.9 KiB
Markdown
# 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.
|