diff --git a/docs/auth-konfiguration.md b/docs/auth-konfiguration.md new file mode 100644 index 0000000..9681354 --- /dev/null +++ b/docs/auth-konfiguration.md @@ -0,0 +1,173 @@ +# Auth-Konfiguration (Supabase Auth / GoTrue) + +| | | +|---|---| +| Gilt für | KTO-01 (Issue #1) und alle folgenden Auth-Stories | +| Stand | 2026-11-02 | +| Versionen (verifiziert) | Supabase CLI 3.0.0, GoTrue `supabase/gotrue:v2.197.0`, Postgres 17, Mailpit v1.31.3 | +| Quellen | `docs/specs/1-auth-registrierung-anmeldung.md` (§ 4.1, 4.3), `docs/specs/1-auth-spike-ergebnisse.md` (Nachweis, bleibt unverändert liegen) | +| Pflege | Jede Änderung an `supabase/config.toml` (Abschnitt `[auth*]`) und am Env-Block unten gemeinsam ändern. Tests referenzieren die IDs `AUTH-CFG-nn`. | + +Zweck: Die Auth-Einstellungen sind explizit festgelegt, weil sich die Standardwerte je nach Quelle unterscheiden (GoTrue-Binary, CLI-Vorlage, Docker-Setup; Klärung 18). Lokal und in der CI gilt `supabase/config.toml`. Staging und Produktion (self-hosted, ADR-0002) setzen dieselben Werte per Umgebungsvariablen. + +## 1. Einstellungen (AUTH-CFG) + +Spalte „Wirkung“: **lokal** = wirkt in der CLI über `config.toml`; **Env** = wirkt nur im Self-Hosting über Umgebungsvariablen (in `config.toml` nur als Kommentar bzw. Zielwert); **Gateway** = nur am Proxy/Gateway lösbar. + +| ID | Einstellung | Wert | Wirkung | `config.toml` / Env | Begründung | Krit. | +|---|---|---|---|---|---|---| +| AUTH-CFG-01 | Site-URL | lokal `http://localhost:3000`; Prod `https://shuffle.cshomelabs.work` (Platzhalter); Staging legt devops fest | lokal + Env | `auth.site_url` / `GOTRUE_SITE_URL` | Basis der Mail-Links (`{{ .SiteURL }}`) | 1, 8 | +| AUTH-CFG-02 | Redirect-Allow-List | nur `/auth/confirm` | lokal + Env | `auth.additional_redirect_urls` / `GOTRUE_URI_ALLOW_LIST` | Nur eigene Web-URLs erlaubt | 1, 8 | +| AUTH-CFG-03 | Registrierung erlaubt | an | lokal + Env | `auth.enable_signup`, `auth.email.enabled` / `GOTRUE_DISABLE_SIGNUP=false`, `GOTRUE_EXTERNAL_EMAIL_ENABLED=true` | Selbstregistrierung ist der Kern von KTO-01 | 1 | +| AUTH-CFG-04 | E-Mail-Bestätigung Pflicht | an | lokal + Env | `auth.email.enable_confirmations = true` / `GOTRUE_MAILER_AUTOCONFIRM=false` | CLI-Standard ist `false`. Voraussetzung für den Enumerationsschutz, zusammen mit AUTH-CFG-24 | 1, 4, 6 | +| AUTH-CFG-05 | Passwort-Mindestlänge | 10 | lokal + Env | `auth.minimum_password_length` / `GOTRUE_PASSWORD_MIN_LENGTH` | Entscheidung O10; Standard wäre 6 | 3 | +| AUTH-CFG-06 | Zeichenklassen | keine | lokal + Env | `auth.password_requirements = ""` / `GOTRUE_PASSWORD_REQUIRED_CHARACTERS` leer | Nicht gefordert | 3 | +| AUTH-CFG-07 | Gültigkeit Bestätigungs- und Reset-Link | 3600 s (1 h) | lokal + Env | `auth.email.otp_expiry` / `GOTRUE_MAILER_OTP_EXP=3600` | Entscheidung F3. Das Self-hosted-Binary hätte 86400, deshalb im Env explizit setzen | 1, 8 | +| AUTH-CFG-08 | Mindestabstand zwischen Mails je Nutzer | lokal/CI 1 s; Staging/Prod 60 s | lokal + Env | `auth.email.max_frequency` / `GOTRUE_SMTP_MAX_FREQUENCY=60s` | Lokal bewusst 1 s, sonst scheitert der Resend-Test direkt nach dem Signup an 429. Die 60 s werden auf Staging geprüft (Checkliste) | 6, 8 | +| AUTH-CFG-09 | Mails pro Stunde, **instanzweit** | lokal/CI-Zielwert 100; Staging/Prod **30** | **nur Env** | `auth.rate_limit.email_sent` (lokal ohne Wirkung) / `GOTRUE_RATE_LIMIT_EMAIL_SENT=30` | Entscheidung F4. Die CLI setzt intern `360000` (Spike A3). Risiko R3 | 1, 6, 8 | +| AUTH-CFG-10 | Drosselung Passwort-Login und Token-Refresh | 150 je 5 min je IP, Burst 30 | **nur Env, nur mit AUTH-CFG-13** | `auth.rate_limit.token_refresh` / `GOTRUE_RATE_LIMIT_TOKEN_REFRESH=150` | Login Nr. 31 einer IP → 429 `over_request_rate_limit`. Keine Sperre je Konto, nur je IP. 429 ohne `Retry-After` | 7 | +| AUTH-CFG-11 | Drosselung Registrierung, Reset, erneut senden | 30 je 5 min je IP, Burst 30 | **nur Env, nur mit AUTH-CFG-13** | `auth.rate_limit.sign_in_sign_ups` / `GOTRUE_RATE_LIMIT_OTP=30` | Für Signup getestet; dass Recover und Resend denselben Limiter nutzen, ist nur aus dem Code abgeleitet | 1, 6, 8 | +| AUTH-CFG-12 | Drosselung Link-Prüfung | 30 je 5 min je IP | **nur Env, nur mit AUTH-CFG-13** | `auth.rate_limit.token_verifications` / `GOTRUE_RATE_LIMIT_VERIFY=30` | – | 8 | +| AUTH-CFG-13 | Client-IP hinter Proxy | `GOTRUE_RATE_LIMIT_HEADER=X-Forwarded-For` | **nur Env (Pflicht Staging/Prod)** | – / `GOTRUE_RATE_LIMIT_HEADER` | Ohne diese Einstellung gibt es **keine** IP-Drosselung; Requests ohne den Header werden auch mit ihr nicht gedrosselt. Der Proxy muss den Header überschreiben | 7 | +| AUTH-CFG-14 | JWT-Laufzeit | 3600 s | lokal + Env | `auth.jwt_expiry` / `GOTRUE_JWT_EXP=3600` | Entscheidung F1. Andere Geräte sind nach einem Passwort-Reset spätestens nach 60 min abgemeldet (Restrisiko R8) | 5, 9 | +| AUTH-CFG-15 | Refresh-Token-Rotation | an, Wiederverwendungsfenster 10 s | lokal + Env | `auth.enable_refresh_token_rotation`, `auth.refresh_token_reuse_interval` / `GOTRUE_SECURITY_REFRESH_TOKEN_ROTATION_ENABLED=true`, `GOTRUE_SECURITY_REFRESH_TOKEN_REUSE_INTERVAL=10` | Standard | 5, 9 | +| AUTH-CFG-16 | Sitzungsbegrenzung | **nicht gesetzt** | lokal + Env | `auth.sessions.*` (auskommentiert) / `GOTRUE_SESSIONS_TIMEBOX`, `GOTRUE_SESSIONS_INACTIVITY_TIMEOUT` ungesetzt | Angemeldet bis zum Abmelden | 5 | +| AUTH-CFG-17 | Mailvorlage Bestätigung | `supabase/templates/confirmation.html`, Betreff de/en | lokal + Env | `auth.email.template.confirmation` / `GOTRUE_MAILER_TEMPLATES_CONFIRMATION` (URL), `GOTRUE_MAILER_SUBJECTS_CONFIRMATION` | Sprache aus `user_metadata.locale`, sonst Englisch. Self-hosted lädt per URL (siehe 3) | 1, 11 | +| AUTH-CFG-18 | Mailvorlage Reset | `supabase/templates/recovery.html`, Betreff de/en | lokal + Env | `auth.email.template.recovery` / `GOTRUE_MAILER_TEMPLATES_RECOVERY`, `GOTRUE_MAILER_SUBJECTS_RECOVERY` | wie oben | 8, 11 | +| AUTH-CFG-19 | Benachrichtigungs-Mails | alle aus | lokal + Env | `auth.email.notification..enabled = false` / `GOTRUE_MAILER_NOTIFICATIONS__ENABLED=false` | Nur Bestätigung und Reset (Krit. 11). `email_changed` schaltet KTO-11 ein (N2) | 11 | +| AUTH-CFG-20 | Magic Link / OTP-Login | `POST /auth/v1/otp` und `/auth/v1/magiclink` am Gateway sperren | **Gateway** | – (kein Schalter in GoTrue) | Sonst Mails außerhalb von Krit. 11 und Konten ohne Passwort (Spike A7). Der Trigger ersetzt die Sperre nicht. Lokal nicht gesperrt | 11, 3 | +| AUTH-CFG-21 | Leaked-Password-Prüfung (HIBP) | aus | **nur Env** | – / `GOTRUE_PASSWORD_HIBP_ENABLED=false` | Entscheidung F2: keine Übermittlung an Dritte | 3 | +| AUTH-CFG-22 | SMTP / Mailfänger | lokal Mailpit; Staging/Prod EU-Anbieter mit AVV (**offen**, F7) | lokal + Env | `local_smtp` / `GOTRUE_SMTP_*` | Release-Blocker, kein Merge-Blocker | 1, 8, 11 | +| AUTH-CFG-23 | E-Mail-Wechsel auf beiden Adressen bestätigen | **in #1 nicht festgelegt** (CLI-Standard `true`) | – | `auth.email.double_confirm_changes` / `GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED` | Entscheidung fällt in KTO-11 (N2) | (KTO-11) | +| AUTH-CFG-24 | SMS-Autoconfirm aus | `SMS_AUTOCONFIRM=false` | lokal + Env | `auth.sms.enable_confirmations = true` / `GOTRUE_SMS_AUTOCONFIRM=false`; in der Self-Hosting-Vorlage `ENABLE_PHONE_AUTOCONFIRM=false` | SMS ist nicht im Einsatz, die Einstellung steuert aber den Enumerationsschutz beim E-Mail-Signup. Mit `true` (CLI-Standard und Vorlage `docker/.env.example`!) liefert Signup mit bestätigter Adresse 422 `user_already_exists` | 4 | + +Ohne eigene ID: `auth.email.secure_password_change = false` (Standard, ausdrücklich gesetzt). Recovery-Sitzungen sind davon ausgenommen; ob für die Passwortänderung das aktuelle Passwort verlangt wird, entscheidet KTO-11 (N13). + +## 2. Env-Block für Staging und Produktion (GoTrue, ohne Secrets) + +Gilt für das Image `supabase/gotrue:v2.197.0` (pinnen, nicht `latest`). Secrets (JWT-Secret, SMTP-Passwort, DB-URL) stehen nicht hier, sondern im Secret-Speicher des Hosting-Setups. + +```env +# AUTH-CFG-01, -02, -03 +GOTRUE_SITE_URL=https://shuffle.cshomelabs.work +GOTRUE_URI_ALLOW_LIST=https://shuffle.cshomelabs.work/auth/confirm +GOTRUE_DISABLE_SIGNUP=false +GOTRUE_EXTERNAL_EMAIL_ENABLED=true + +# AUTH-CFG-04, -24 (beide Autoconfirm-Schalter müssen false sein; Supabase-Vorlage: ENABLE_PHONE_AUTOCONFIRM=false) +GOTRUE_MAILER_AUTOCONFIRM=false +GOTRUE_SMS_AUTOCONFIRM=false + +# AUTH-CFG-05, -06, -21 +GOTRUE_PASSWORD_MIN_LENGTH=10 +GOTRUE_PASSWORD_REQUIRED_CHARACTERS= +GOTRUE_PASSWORD_HIBP_ENABLED=false + +# AUTH-CFG-07, -08 +GOTRUE_MAILER_OTP_EXP=3600 +GOTRUE_SMTP_MAX_FREQUENCY=60s + +# AUTH-CFG-09 bis -13 (GOTRUE_RATE_LIMIT_HEADER ist Pflicht, sonst keine IP-Drosselung) +GOTRUE_RATE_LIMIT_HEADER=X-Forwarded-For +GOTRUE_RATE_LIMIT_EMAIL_SENT=30 +GOTRUE_RATE_LIMIT_TOKEN_REFRESH=150 +GOTRUE_RATE_LIMIT_OTP=30 +GOTRUE_RATE_LIMIT_VERIFY=30 + +# AUTH-CFG-14, -15 (AUTH-CFG-16: Sitzungs-Variablen NICHT setzen) +GOTRUE_JWT_EXP=3600 +GOTRUE_SECURITY_REFRESH_TOKEN_ROTATION_ENABLED=true +GOTRUE_SECURITY_REFRESH_TOKEN_REUSE_INTERVAL=10 + +# AUTH-CFG-17, -18 (Betreff je Sprache verzweigt, Text identisch zu config.toml; URLs siehe Abschnitt 3) +GOTRUE_MAILER_TEMPLATES_CONFIRMATION=https:///confirmation.html +GOTRUE_MAILER_TEMPLATES_RECOVERY=https:///recovery.html +GOTRUE_MAILER_SUBJECTS_CONFIRMATION={{ if and .Data (eq (index .Data "locale") "de") }}Bestätige deine E-Mail-Adresse für Shuffle{{ else }}Confirm your email address for Shuffle{{ end }} +GOTRUE_MAILER_SUBJECTS_RECOVERY={{ if and .Data (eq (index .Data "locale") "de") }}Passwort für Shuffle zurücksetzen{{ else }}Reset your Shuffle password{{ end }} + +# AUTH-CFG-19 (Benachrichtigungen aus; KTO-11 schaltet email_changed ein) +GOTRUE_MAILER_NOTIFICATIONS_PASSWORD_CHANGED_ENABLED=false +GOTRUE_MAILER_NOTIFICATIONS_EMAIL_CHANGED_ENABLED=false +GOTRUE_MAILER_NOTIFICATIONS_PHONE_CHANGED_ENABLED=false +GOTRUE_MAILER_NOTIFICATIONS_IDENTITY_LINKED_ENABLED=false +GOTRUE_MAILER_NOTIFICATIONS_IDENTITY_UNLINKED_ENABLED=false +GOTRUE_MAILER_NOTIFICATIONS_MFA_FACTOR_ENROLLED_ENABLED=false +GOTRUE_MAILER_NOTIFICATIONS_MFA_FACTOR_UNENROLLED_ENABLED=false + +# AUTH-CFG-22 (Anbieter offen, F7): GOTRUE_SMTP_HOST, _PORT, _USER, _PASS, _ADMIN_EMAIL, _SENDER_NAME +``` + +Hinweis zu den Benachrichtigungs-Variablen: Namen aus dem GoTrue-Schema für `v2.197.0` abgeleitet; beim ersten Aufsetzen mit einer Testmail (Passwortänderung) prüfen, dass keine Mail ausgeht (Checkliste). + +## 3. Gateway- und Dashboard-Einstellungen + +- **Proxy vor GoTrue (AUTH-CFG-13):** GoTrue ist nur über den Proxy erreichbar. Der Proxy **überschreibt** `X-Forwarded-For` mit der echten Client-IP (nicht anhängen), sonst fälschen Clients ihre IP und umgehen die Drosselung. Ein direkter Zugriff auf GoTrue umgeht die Drosselung ebenfalls. +- **Magic-Link-Sperre (AUTH-CFG-20):** Das Gateway lehnt `POST /auth/v1/otp` und `POST /auth/v1/magiclink` ab (z. B. 404/403). Beide Endpunkte lassen sich in GoTrue nicht abschalten; `/otp` legt mit gültigen Metadaten ein Konto an. +- **Vorlagen-Hosting (AUTH-CFG-17, -18):** Self-hosted GoTrue lädt Mailvorlagen **per URL**. Die Dateien aus `supabase/templates/` werden unter einer intern erreichbaren URL bereitgestellt (z. B. statisch am Gateway). Ist die URL nicht erreichbar, fällt GoTrue **still** auf die englische Standardvorlage zurück (Risiko R5). Die URL gehört in den Health-Check. +- **SPA-Rewrite:** Die Web-App muss `/auth/confirm` ausliefern (alle Pfade → `index.html`), sonst laufen die Mail-Links ins Leere. +- **Dashboard:** Im Self-Hosting gibt es keine Cloud-Einstellungen; Studio ändert keine Auth-Werte. Alle Werte stehen im Env-Block oben. Änderungen im Studio sind zu unterlassen, damit Repo und Betrieb übereinstimmen. +- **Image-Pinning:** `supabase/gotrue:v2.197.0`. Die Postgres-Hauptversion 17 muss mit `db.major_version` übereinstimmen. + +## 4. Verifiziert (Spike B0, 2026-11-02) + +Getestet gegen CLI 3.0.0 und GoTrue `v2.197.0` über Kong (wie die App). Die Drosselungs- und Mail-Limit-Tests liefen zusätzlich gegen einen zweiten GoTrue-Container desselben Images mit den Env-Variablen der CLI plus `GOTRUE_RATE_LIMIT_HEADER=X-Forwarded-For` und `GOTRUE_RATE_LIMIT_EMAIL_SENT=5` (entspricht dem Self-Hosting-Pfad). Nicht aussagekräftig lokal: SMTP-Anbieter, Verhalten hinter dem Produktions-Gateway, Header-Behandlung des Proxys, echte Zustellung. Vollständige Nachweise: `docs/specs/1-auth-spike-ergebnisse.md`. + +| Punkt | Ergebnis | +|---|---| +| (a) Betreff und Vorlage werten `.Data.locale` aus | Bestätigt. `de` → deutsch; `en` oder fehlend → englisch. `index .Data "locale"` ist sicher, wenn der Schlüssel fehlt. Nach Profiländerung (Sync-Trigger) kommt die Mail in der neuen Sprache. | +| (b) `type` für `token_hash` | Bestätigung `email` (auch `signup`), Reset `recovery`. Zweites Einlösen → 403 `otp_expired`. | +| (c) Limiter Passwort-Login | `GOTRUE_RATE_LIMIT_TOKEN_REFRESH` (150 je 5 min, Burst 30); Signup/Recover/Resend `GOTRUE_RATE_LIMIT_OTP` (30 je 5 min). **Greifen nur mit `GOTRUE_RATE_LIMIT_HEADER` und Requests mit dem Header.** Die lokale CLI kann den Header nicht setzen, also lokal keine Drosselung. 429 ohne `Retry-After`. | +| (d) `email_sent` | Instanzweit. In der CLI wirkungslos (`360000`). Bei erschöpftem Limit antwortet `/recover` für bestehende Adressen 429, für unbekannte 200 (Seitenkanal, akzeptiert F12). | +| (e) Mailfänger | Mailpit v1.31.3, `http://127.0.0.1:54324/api/v1/messages`, `/api/v1/message/{id}`. | +| (f) Trigger-Fehler bei `signUp` | HTTP 500, Header `X-Sb-Error-Code: unexpected_failure`, Body `{"code":"P0001","message":""}`. Format nicht vertraglich zugesichert. | +| (g) `weak_password` | 422, `reasons = ["length"]`. | +| (h) Versionen | CLI 3.0.0 startet GoTrue `v2.197.0`, identisch zur `docker-compose.yml` des Supabase-Repos (Stand 2026-11-02). | +| (i) Linkablauf | Maßgeblich sind `auth.users.confirmation_sent_at` / `recovery_sent_at`, nicht `auth.one_time_tokens.created_at`. | +| (j) `amr` im Recovery-JWT | `[{"method":"otp"}]`, identisch zur Bestätigungssitzung; nicht unterscheidbar (R12). | + +Abweichungen von der ursprünglichen Spec-Annahme: + +| # | Abweichung | Konsequenz | +|---|---|---| +| A1 | Enumerationsschutz hängt an `GOTRUE_SMS_AUTOCONFIRM=false` (CLI-Standard und Self-Hosting-Vorlage setzen `true`). Sonst 422 `user_already_exists`. | AUTH-CFG-24 | +| A2 | Ohne `GOTRUE_RATE_LIMIT_HEADER` keine IP-Drosselung; Requests ohne Header werden auch mit Einstellung nicht gedrosselt. | AUTH-CFG-13 Pflicht; Drosselungstest gegen Extra-Container | +| A3 | `email_sent` wirkt in der CLI nicht. | AUTH-CFG-09 nur per Env | +| A4 | Trigger-Fehler kommen als HTTP 500 mit der Trigger-Meldung im Body. | Mapping über Status 500 | +| A5 | Recovery-JWT ist an `amr` nicht erkennbar. | Guard nur im laufenden Prozess | +| A6 | Linkablauf wird über `auth.users.*_sent_at` bestimmt. | E2E-Helfer setzt diese Spalten | +| A7 | `/otp` und `/magiclink` nicht abschaltbar, `/otp` kann Konten anlegen. | AUTH-CFG-20 am Gateway | + +### Prüfung der Konfiguration und Mailvorlagen (B2, lokal) + +Mit der committeten `config.toml` geprüft (Mailpit-API): Registrierung mit `locale=de` → Betreff „Bestätige deine E-Mail-Adresse für Shuffle“, `lang="de"`; `locale=en` und ohne `locale` → englischer Betreff und Text; Passwort-Reset analog („Passwort für Shuffle zurücksetzen“ / „Reset your Shuffle password“). Links: `http://localhost:3000/auth/confirm?token_hash=…&type=email` bzw. `…&type=recovery`; der Token aus der Mail lässt sich per `POST /verify` einlösen. Im GoTrue-Container sind `GOTRUE_MAILER_AUTOCONFIRM=false`, `GOTRUE_SMS_AUTOCONFIRM=false`, `GOTRUE_PASSWORD_MIN_LENGTH=10`, `GOTRUE_MAILER_OTP_EXP=3600` und `GOTRUE_URI_ALLOW_LIST=http://localhost:3000/auth/confirm` gesetzt. Signup mit 9 Zeichen → 422 `weak_password`; Signup mit bestätigter Adresse → 200 mit erfundenem Nutzer und leeren `identities`, keine Mail. + +## 5. Release-Checkliste (vor jedem Release auf Staging/Produktion) + +Automatisiert nicht abdeckbar (Spec § 7.1); vor dem Release abhaken: + +- [ ] GoTrue-Image ist `supabase/gotrue:v2.197.0` (oder bewusst aktualisiert, dann Spike-Punkte (c), (d), (f) wiederholen). +- [ ] Env-Block aus Abschnitt 2 ist vollständig gesetzt; `GOTRUE_PASSWORD_HIBP_ENABLED=false` (AUTH-CFG-21). +- [ ] `GOTRUE_SMS_AUTOCONFIRM=false` bzw. `ENABLE_PHONE_AUTOCONFIRM=false`: Signup mit bestätigter Adresse → 200, `identities` leer (AUTH-CFG-24). +- [ ] Drosselung am echten Gateway: 31 Fehllogins einer IP → 429; ein vom Client gefälschter `X-Forwarded-For` wird überschrieben; GoTrue ist von außen nicht direkt erreichbar (AUTH-CFG-13, -10). +- [ ] `POST /auth/v1/otp` und `/auth/v1/magiclink` werden vom Gateway abgelehnt (AUTH-CFG-20). +- [ ] Mailvorlagen-URLs erreichbar; Testmail de und en (Bestätigung und Reset) auf Staging, Betreff und Text in der richtigen Sprache (AUTH-CFG-17, -18, Risiko R5). +- [ ] Mindestabstand 60 s zwischen Mails je Nutzer (AUTH-CFG-08) und Link-Gültigkeit 1 h (AUTH-CFG-07) auf Staging geprüft. +- [ ] `GOTRUE_RATE_LIMIT_EMAIL_SENT=30` gesetzt und Monitoring auf 429 `over_email_send_rate_limit` aktiv (AUTH-CFG-09, R3). +- [ ] Nach Passwortänderung kommt keine Benachrichtigungs-Mail (AUTH-CFG-19). +- [ ] SMTP-Anbieter mit AVV und Absender-Domain entschieden und eingerichtet (AUTH-CFG-22, F7). +- [ ] SPA-Rewrite: `/auth/confirm` wird von der Web-App ausgeliefert. +- [ ] Rechtstexte-URLs und `TERMS_VERSION` final (R11); Bundle-/Application-ID final (R15). + +## 6. Bekannte Grenzen + +- **Keine Drosselung lokal und in der CI-CLI** (A2). Der Drosselungstest läuft gegen einen Extra-Container (Spec D2/T3). +- **Keine Wartezeit-Angabe:** Die 429-Antwort trägt kein `Retry-After`; die App nennt keine Dauer. +- **Drosselung nur je IP**, nicht je Konto. +- **Mail-Limit instanzweit** (30/h): Angreifer können Mailausfälle auslösen. Während der Sperre ist die Existenz eines Kontos über 429 vs. 200 erkennbar (R3, akzeptiert F12). +- **Enumeration auf API-Ebene:** Signup mit unbestätigter Adresse gibt den echten Nutzer zurück, mit bestätigter einen erfundenen (R6). Die App zeigt in beiden Fällen denselben Bildschirm. +- **Abgemeldete Geräte nach Reset:** Access-Tokens gelten bis zu 60 min weiter (R8, F1). +- **Abgebrochene Recovery** bleibt nach einem Reload als normale Anmeldung bestehen (R12). +- **Häufige Passwörter** prüft nur der Client; der Server erzwingt nur die Länge 10 (R14). +- **Magic Link / OTP** sind nicht abschaltbar, nur am Gateway sperrbar (R4). +- **Vorlagen-Rückfall:** Unerreichbare Vorlagen-URL → stille englische Standardvorlage (R5). +- **Plain-Text-Teil der Mails:** GoTrue sendet nur HTML. Die Vorlagen zeigen den Link deshalb zusätzlich als Klartext; Mailpit erzeugt lokal einen Text-Teil. diff --git a/supabase/README.md b/supabase/README.md index 38c893b..09513d8 100644 --- a/supabase/README.md +++ b/supabase/README.md @@ -5,6 +5,7 @@ |---|---| | `migrations/` | Schema, Trigger und RLS-Policies für R1 (17 Tabellen, 2 Views). Nur additiv ändern, nie bestehende Dateien editieren. | | `tests/database/` | pgTAP-Tests: Struktur-Regeln und Zugriffsrechte je Bereich. | +| `templates/` | Auth-Mailvorlagen (HTML, Go-Template) für Bestätigung und Passwort-Reset. | | `seed.sql` | Beispieldaten nur für die lokale Entwicklung. | ## Lokal starten und testen @@ -18,6 +19,8 @@ supabase test db # alle pgTAP-Tests ausführen - Der Trigger `handle_new_user` erzwingt Altersbestätigung, Zustimmung, `terms_version` und Anzeigename (1–30 Zeichen). Fehlertexte: siehe `docs/datenmodell-r1.md`. Clients senden diese Werte als `user_metadata` bei `signUp`. - pgTAP-Fixtures, die `auth.users` direkt füllen, brauchen daher `age_confirmed`, `terms_accepted`, `terms_version` und `display_name` in `raw_user_meta_data`. - Ergebnisse der GoTrue-Prüfung: `docs/specs/1-auth-spike-ergebnisse.md`. +- Auth-Einstellungen (`AUTH-CFG-nn` in `config.toml`), Env-Block für Staging/Produktion, Release-Checkliste und bekannte Grenzen: [`docs/auth-konfiguration.md`](../docs/auth-konfiguration.md). +- Mailvorlagen (Bestätigung, Passwort-Reset; de/en nach `user_metadata.locale`): `templates/`. Lokal landen Mails in Mailpit (`http://127.0.0.1:54324`). ## Regeln - Jede Tabelle hat `id` (uuid, PK), `created_at`, `updated_at` und Row Level Security. diff --git a/supabase/config.toml b/supabase/config.toml index 0f4c730..772e81e 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -103,6 +103,9 @@ openai_api_key = "env(OPENAI_API_KEY)" # Email testing server. Emails sent with the local dev setup are not actually sent - rather, they # are monitored, and you can view the emails that would have been sent from the web interface. +# AUTH-CFG-22 (wirkt lokal): Mailfänger ist Mailpit (Container heißt historisch supabase_inbucket_*). +# Web-Oberfläche und API: http://127.0.0.1:54324 (API: /api/v1/messages, /api/v1/message/{id}). +# Staging/Prod: SMTP über GOTRUE_SMTP_* (Anbieter offen, siehe docs/auth-konfiguration.md). [local_smtp] enabled = true # Port to use for the email testing server web interface. @@ -155,36 +158,44 @@ max_indexes = 5 [auth] enabled = true -# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used -# in emails. +# AUTH-CFG-01 (wirkt lokal): Site-URL, Basis der Mail-Links ({{ .SiteURL }}). +# Staging/Prod: GOTRUE_SITE_URL (Prod-Platzhalter https://shuffle.cshomelabs.work). site_url = "http://localhost:3000" # The public URL that Auth serves on. Defaults to the API external URL with `/auth/v1` appended. # external_url = "" -# A list of *exact* URLs that auth providers are permitted to redirect to post authentication. -additional_redirect_urls = ["http://localhost:3000"] -# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). +# AUTH-CFG-02 (wirkt lokal): Redirect-Allow-List, nur die eigene Web-App-Seite für Mail-Links. +# Staging/Prod: GOTRUE_URI_ALLOW_LIST. +additional_redirect_urls = ["http://localhost:3000/auth/confirm"] +# AUTH-CFG-14 (wirkt lokal): JWT-Laufzeit 3600 s. Bestimmt die Verzögerung bei KTO-01 Kriterium 9 +# (andere Geräte spätestens nach 60 min abgemeldet, Restrisiko R8). Staging/Prod: GOTRUE_JWT_EXP. jwt_expiry = 3600 # JWT issuer URL. If not set, defaults to auth.external_url. # jwt_issuer = "" # Path to JWT signing key. DO NOT commit your signing keys file to git. # signing_keys_path = "./signing_keys.json" -# If disabled, the refresh token will never expire. +# AUTH-CFG-15 (wirkt lokal): Refresh-Token-Rotation an, Wiederverwendungsfenster 10 s. +# Staging/Prod: GOTRUE_SECURITY_REFRESH_TOKEN_ROTATION_ENABLED, GOTRUE_SECURITY_REFRESH_TOKEN_REUSE_INTERVAL. enable_refresh_token_rotation = true -# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds. -# Requires enable_refresh_token_rotation = true. refresh_token_reuse_interval = 10 -# Allow/disallow new user signups to your project. Existing users can still sign in. +# AUTH-CFG-03 (wirkt lokal): Registrierung erlaubt. Staging/Prod: GOTRUE_DISABLE_SIGNUP=false. enable_signup = true # Allow/disallow anonymous sign-ins to your project. enable_anonymous_sign_ins = false # Allow/disallow testing manual linking of accounts enable_manual_linking = false -# Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more. -minimum_password_length = 6 -# Passwords that do not meet the following requirements will be rejected as weak. Supported values -# are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols` +# AUTH-CFG-05 (wirkt lokal): Passwort-Mindestlänge 10 (O10). Staging/Prod: GOTRUE_PASSWORD_MIN_LENGTH. +minimum_password_length = 10 +# AUTH-CFG-06 (wirkt lokal): keine Zeichenklassen gefordert. Staging/Prod: GOTRUE_PASSWORD_REQUIRED_CHARACTERS (leer). password_requirements = "" +# AUTH-CFG-21 (nur Env / nur Self-Hosting): Leaked-Password-Prüfung (HIBP) ist aus (Entscheidung F2). +# Die CLI kennt dafür keinen Schlüssel und führt keine Prüfung aus. Staging/Prod muss +# GOTRUE_PASSWORD_HIBP_ENABLED=false explizit setzen. + +# AUTH-CFG-20 (nur Self-Hosting / Gateway): Magic Link und OTP-Login (/auth/v1/otp, /auth/v1/magiclink) +# lassen sich in GoTrue nicht abschalten (Spike A7). Staging/Prod: am Gateway sperren. +# Lokal bewusst nicht gesperrt. + # Configure passkey sign-ins. # [auth.passkey] # enabled = false @@ -195,18 +206,27 @@ password_requirements = "" # rp_id = "localhost" # rp_origins = ["http://127.0.0.1:3000"] +# Rate Limits. ACHTUNG: Die lokale CLI kann GOTRUE_RATE_LIMIT_HEADER nicht setzen (AUTH-CFG-13). +# Ohne diesen Header gibt es keine IP-Drosselung; lokal und in der CI-CLI wird nichts gedrosselt. +# Die Werte unten sind daher Dokumentation der Zielwerte. Wirksam sind sie nur im Self-Hosting mit +# GOTRUE_RATE_LIMIT_HEADER und Requests, die den Header tragen (Spike B0, A2). [auth.rate_limit] -# Number of emails that can be sent per hour. Requires auth.email.smtp to be enabled. -email_sent = 2 +# AUTH-CFG-09 (nur Env, lokal OHNE Wirkung): Mails pro Stunde, instanzweit. Die CLI setzt intern +# GOTRUE_RATE_LIMIT_EMAIL_SENT=360000 (Spike A3). Lokal/CI-Zielwert 100, Staging/Prod: 30 über +# GOTRUE_RATE_LIMIT_EMAIL_SENT=30. +email_sent = 100 # Number of SMS messages that can be sent per hour. Requires auth.sms to be enabled. sms_sent = 30 # Number of anonymous sign-ins that can be made per hour per IP address. Requires enable_anonymous_sign_ins = true. anonymous_users = 30 -# Number of sessions that can be refreshed in a 5 minute interval per IP address. +# AUTH-CFG-10 (nur mit AUTH-CFG-13 wirksam, lokal ohne Wirkung): Passwort-Login und Token-Refresh, +# 150 je 5 min je IP. Env: GOTRUE_RATE_LIMIT_TOKEN_REFRESH. token_refresh = 150 -# Number of sign up and sign-in requests that can be made in a 5 minute interval per IP address (excludes anonymous users). +# AUTH-CFG-11 (nur mit AUTH-CFG-13 wirksam, lokal ohne Wirkung): Registrierung, Reset, erneut senden, +# 30 je 5 min je IP. Env: GOTRUE_RATE_LIMIT_OTP. sign_in_sign_ups = 30 -# Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address. +# AUTH-CFG-12 (nur mit AUTH-CFG-13 wirksam, lokal ohne Wirkung): Link-Prüfung, 30 je 5 min je IP. +# Env: GOTRUE_RATE_LIMIT_VERIFY. token_verifications = 30 # Number of Web3 logins that can be made in a 5 minute interval per IP address. web3 = 30 @@ -217,22 +237,28 @@ web3 = 30 # provider = "hcaptcha" # secret = "" +# AUTH-CFG-13 (nur Env / nur Self-Hosting): Client-IP hinter Proxy, GOTRUE_RATE_LIMIT_HEADER +# (z. B. X-Forwarded-For). Pflicht für Staging/Prod, in der CLI nicht setzbar. + [auth.email] -# Allow/disallow the email provider. When false, users can't sign up or sign in with email. -# To block new signups only, set enable_signup = false under [auth] instead. +# AUTH-CFG-03 (wirkt lokal): E-Mail-Anmeldung erlaubt. Staging/Prod: GOTRUE_EXTERNAL_EMAIL_ENABLED=true. enabled = true -# If enabled, a user will be required to confirm any email change on both the old, and new email -# addresses. If disabled, only the new email is required to confirm. +# AUTH-CFG-23 (wirkt lokal, in #1 NICHT festgelegt): CLI-Standard bleibt unverändert; Entscheidung in +# KTO-11 (Info-Mail an die alte Adresse, N2). Staging/Prod: GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED. double_confirm_changes = true -# If enabled, users need to confirm their email address before signing in. -enable_confirmations = false -# If enabled, users will need to reauthenticate or have logged in recently to change their password. +# AUTH-CFG-04 (wirkt lokal): E-Mail-Bestätigung Pflicht (CLI-Standard wäre false). Voraussetzung für den +# Enumerationsschutz zusammen mit AUTH-CFG-24. Staging/Prod: GOTRUE_MAILER_AUTOCONFIRM=false. +enable_confirmations = true +# Ohne AUTH-CFG-ID (Spec 9.4): Passwortänderung ohne erneute Anmeldung bleibt wie im Standard. +# Recovery-Sitzungen sind ohnehin ausgenommen; die Entscheidung "aktuelles Passwort verlangen" fällt in KTO-11 (N13). secure_password_change = false -# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email. +# AUTH-CFG-08 (wirkt lokal): Mindestabstand zwischen Mails je Nutzer. Lokal/CI 1 s (sonst scheitert der +# Resend-Test direkt nach dem Signup mit 429). Staging/Prod: 60 s über GOTRUE_SMTP_MAX_FREQUENCY=60s. max_frequency = "1s" -# Number of characters used in the email OTP. +# Länge des E-Mail-OTP-Codes (nur der Vollständigkeit halber gesetzt; Shuffle nutzt token_hash-Links). otp_length = 6 -# Number of seconds before the email OTP expires (defaults to 1 hour). +# AUTH-CFG-07 (wirkt lokal): Gültigkeit von Bestätigungs- und Reset-Link, 3600 s (1 h, Entscheidung F3). +# Staging/Prod: GOTRUE_MAILER_OTP_EXP=3600 explizit setzen (das Binary hätte 86400). otp_expiry = 3600 # Use a production-ready SMTP server @@ -245,23 +271,45 @@ otp_expiry = 3600 # admin_email = "admin@email.com" # sender_name = "Admin" -# Uncomment to customize email template -# [auth.email.template.invite] -# subject = "You have been invited" -# content_path = "./supabase/templates/invite.html" +# AUTH-CFG-17 (wirkt lokal): Mailvorlage Bestätigung, de/en per user_metadata.locale (Else-Zweig Englisch). +# Betreff und Text verzweigen gleich (Spike B0 (a)). Staging/Prod: Vorlagen per URL +# (GOTRUE_MAILER_TEMPLATES_CONFIRMATION, GOTRUE_MAILER_SUBJECTS_CONFIRMATION). +[auth.email.template.confirmation] +subject = '{{ if and .Data (eq (index .Data "locale") "de") }}Bestätige deine E-Mail-Adresse für Shuffle{{ else }}Confirm your email address for Shuffle{{ end }}' +content_path = "./supabase/templates/confirmation.html" -# Uncomment to customize notification email template -# [auth.email.notification.password_changed] -# enabled = true -# subject = "Your password has been changed" -# content_path = "./supabase/templates/password_changed_notification.html" +# AUTH-CFG-18 (wirkt lokal): Mailvorlage Passwort zurücksetzen, wie oben. +# Staging/Prod: GOTRUE_MAILER_TEMPLATES_RECOVERY, GOTRUE_MAILER_SUBJECTS_RECOVERY. +[auth.email.template.recovery] +subject = '{{ if and .Data (eq (index .Data "locale") "de") }}Passwort für Shuffle zurücksetzen{{ else }}Reset your Shuffle password{{ end }}' +content_path = "./supabase/templates/recovery.html" + +# AUTH-CFG-19 (wirkt lokal): Benachrichtigungs-Mails explizit aus (KTO-01 Kriterium 11, nur Bestätigung +# und Reset). email_changed schaltet KTO-11 ein (N2). Staging/Prod: GOTRUE_MAILER_NOTIFICATIONS__ENABLED=false. +[auth.email.notification.password_changed] +enabled = false +[auth.email.notification.email_changed] +enabled = false +[auth.email.notification.phone_changed] +enabled = false +[auth.email.notification.identity_linked] +enabled = false +[auth.email.notification.identity_unlinked] +enabled = false +[auth.email.notification.mfa_factor_enrolled] +enabled = false +[auth.email.notification.mfa_factor_unenrolled] +enabled = false [auth.sms] # Allow/disallow the phone provider. When false, users can't sign up or sign in with SMS. # To block new signups only, set enable_signup = false under [auth] instead. enabled = false -# If enabled, users need to confirm their phone number before signing in. -enable_confirmations = false +# AUTH-CFG-24 (wirkt lokal): SMS ist nicht im Einsatz, diese Einstellung steuert aber den +# Enumerationsschutz beim E-Mail-Signup. Mit false (= GOTRUE_SMS_AUTOCONFIRM=true, CLI-Standard) liefert +# Signup mit bestätigter Adresse 422 user_already_exists (Spike A1). MUSS true bleiben. +# Staging/Prod: GOTRUE_SMS_AUTOCONFIRM=false bzw. ENABLE_PHONE_AUTOCONFIRM=false (Vorlage setzt true!). +enable_confirmations = true # Template for sending OTP to users template = "Your code is {{ .Code }}" # Controls the minimum amount of time that must pass before sending another sms otp. @@ -272,7 +320,8 @@ max_frequency = "5s" # [auth.sms.test_otp] # 14152127777 = "123456" -# Configure logged in session timeouts. +# AUTH-CFG-16 (wirkt lokal): Sitzungsbegrenzung bewusst NICHT gesetzt (KTO-01 Kriterium 5: angemeldet +# bis zum Abmelden). Nicht auskommentieren. # [auth.sessions] # Force log out after the specified duration. # timebox = "24h" diff --git a/supabase/templates/confirmation.html b/supabase/templates/confirmation.html new file mode 100644 index 0000000..539fc14 --- /dev/null +++ b/supabase/templates/confirmation.html @@ -0,0 +1,50 @@ + +{{- /* + AUTH-CFG-17: Bestätigungs-Mail (KTO-01 Kriterium 1, 11). + Sprache aus user_metadata.locale ("de"), sonst Englisch. Keine externen Ressourcen, keine Bilder. + Der Link führt auf die App-Seite /auth/confirm; erst der Klick dort löst die Prüfung aus (Spec E3). + Das Token (TokenHash) wird bewusst nicht als Klartext angezeigt, nur der Link. +*/ -}} +{{ if and .Data (eq (index .Data "locale") "de") }} + + + + + Bestätige deine E-Mail-Adresse für Shuffle + + +
+

Bestätige deine E-Mail-Adresse

+

Willkommen bei Shuffle! Bitte bestätige deine E-Mail-Adresse, damit dein Konto genutzt werden kann.

+

+ E-Mail-Adresse bestätigen +

+

Der Link ist 1 Stunde gültig und kann nur einmal verwendet werden.

+

Falls der Knopf nicht funktioniert, kopiere diese Adresse in die Adresszeile deines Browsers:

+

{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email

+

Wenn du das nicht warst, ignoriere diese E-Mail. Es wird dann nichts geändert.

+
+ + +{{ else }} + + + + + Confirm your email address for Shuffle + + +
+

Confirm your email address

+

Welcome to Shuffle! Please confirm your email address so you can use your account.

+

+ Confirm email address +

+

The link is valid for 1 hour and can only be used once.

+

If the button does not work, copy this address into your browser's address bar:

+

{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email

+

If this wasn't you, you can ignore this email. Nothing will be changed.

+
+ + +{{ end }} diff --git a/supabase/templates/recovery.html b/supabase/templates/recovery.html new file mode 100644 index 0000000..a081f59 --- /dev/null +++ b/supabase/templates/recovery.html @@ -0,0 +1,49 @@ + +{{- /* + AUTH-CFG-18: Passwort-zurücksetzen-Mail (KTO-01 Kriterium 8, 11). + Sprache aus user_metadata.locale ("de"), sonst Englisch. Keine externen Ressourcen, keine Bilder. + Der Link führt auf die App-Seite /auth/confirm; erst der Klick dort löst die Prüfung aus (Spec E3). +*/ -}} +{{ if and .Data (eq (index .Data "locale") "de") }} + + + + + Passwort für Shuffle zurücksetzen + + +
+

Passwort zurücksetzen

+

Für dein Shuffle-Konto wurde ein neues Passwort angefordert. Mit dem folgenden Knopf legst du ein neues Passwort fest.

+

+ Neues Passwort festlegen +

+

Der Link ist 1 Stunde gültig und kann nur einmal verwendet werden.

+

Falls der Knopf nicht funktioniert, kopiere diese Adresse in die Adresszeile deines Browsers:

+

{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=recovery

+

Wenn du das nicht warst, ignoriere diese E-Mail. Dein Passwort bleibt dann unverändert.

+
+ + +{{ else }} + + + + + Reset your Shuffle password + + +
+

Reset your password

+

A new password was requested for your Shuffle account. Use the button below to choose a new password.

+

+ Set a new password +

+

The link is valid for 1 hour and can only be used once.

+

If the button does not work, copy this address into your browser's address bar:

+

{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=recovery

+

If this wasn't you, you can ignore this email. Your password will stay the same.

+
+ + +{{ end }}