# 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 | | AUTH-CFG-25 | Passwortänderung ohne erneute Anmeldung | `false` (Standard, ausdrücklich gesetzt) | lokal + Env | `auth.email.secure_password_change` / `GOTRUE_SECURITY_UPDATE_PASSWORD_REQUIRE_REAUTHENTICATION` (Env-Name vor Release gegen das Image prüfen) | Recovery-Sitzungen sind ausgenommen; ob das aktuelle Passwort verlangt wird, entscheidet KTO-11 (N13) | (KTO-11) | ## 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-23 (Stagingwert; in #1 nicht festgelegt, CLI-Standard true, Entscheidung in KTO-11) GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED=true # AUTH-CFG-25 (Env-Name vor Release gegen das Image prüfen) GOTRUE_SECURITY_UPDATE_PASSWORD_REQUIRE_REAUTHENTICATION=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 (printf "%v" (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 (printf "%v" (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. `printf "%v" (index .Data "locale")` ist sicher, wenn der Schlüssel fehlt oder kein String ist (sonst HTTP 500 bei `eq`). 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). - [ ] **Monitoring/Alarm auf SMTP-Fehler** (Release-Blocker, D1, Issue #37/F13): GoTrue-Logs mit `unexpected_failure`/HTTP 500 bei `/recover`, `/resend` und `/signup` alarmieren. Die App zeigt bei einem SMTP-Ausfall bewusst die neutrale „Prüfe dein Postfach“-Seite (Konten-Schutz); Nutzer bekommen keinen Hinweis und keine Mail, nur der Betrieb sieht den Ausfall. Die App loggt zusätzlich Operation, Fehlerart, Status und Code (ohne E-Mail). - [ ] 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). - **SMTP-Ausfall:** Auf API-Ebene unterscheiden sich bekannte und unbekannte Adressen (500 vs. 200, Spike #37). Die App zeigt neutral (F13, R17); Restrisiko rohe API akzeptiert. - **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. ## 7. E2E-Tests und SMTP-Ausfall (Issue #38, Spike #37) Die Playwright-Tests unter `e2e/` (Projekte `api`, `web`, `throttle`, `spike`; Anleitung `e2e/README.md`) sichern diese Konfiguration ab. Konstanten stehen in `e2e/support/auth-config.ts`; `tests/api/config.spec.ts` prüft sie gegen `supabase/config.toml` und diese Datei (alle IDs AUTH-CFG-01 bis -25 müssen in Abschnitt 1 vorkommen). | ID | Absichernder Test | |---|---| | AUTH-CFG-04, -24 | `tests/api/krit04-enumeration.spec.ts` (Signup mit bestätigter Adresse → 200, `identities` leer, keine Mail, altes Passwort gilt) | | AUTH-CFG-05 | `krit02-03-validierung.spec.ts` (9 Zeichen → 422 `weak_password`) | | AUTH-CFG-07 | `krit08-reset-link.spec.ts` und `web/krit08-reset.spec.ts` (Link nach 1 h + 1 s abgelaufen, 60 s vor Ablauf gültig) | | AUTH-CFG-10, -13 | `tests/throttle/krit07-drosselung.spec.ts` (Login #31 → 429, zweite IP unbeeinflusst, kein `Retry-After`, ohne Header keine Drosselung) | | AUTH-CFG-14, -15, -16 | `krit05-sitzung.spec.ts`, `config.spec.ts` | | AUTH-CFG-17, -18, -19 | `krit11-sprache-benachrichtigungen.spec.ts` | ### CI-Job `e2e` (Issue #38, Spec #1 D2) Der Job `e2e` in `.forgejo/workflows/ci.yml` läuft auf dem Runner `docker-host` (Docker-CLI + dind per TCP, wie `supabase-tests`) und führt die Projekte `api`, `web` und `throttle` aus. Ablauf: 1. `supabase start -x studio,imgproxy,edge-runtime,logflare,vector` (Mailpit und Postgres bleiben) und `supabase db reset`. 2. Anon- und Service-Role-Key kommen zur Laufzeit aus `supabase status -o env`; daraus entsteht eine temporäre Env-Datei (`jq` auf `app/env/ci.example.json`, im `RUNNER_TEMP`, nicht im Repo) für `flutter build web --dart-define-from-file`. Die Datei `ci.example.json` bleibt unverändert. 3. Zweiter GoTrue-Container für Krit. 7: `BIND_ADDR=0.0.0.0 HEALTH_HOST= e2e/support/start-throttle-gotrue.sh start throttle` (Port 9998). 4. `.forgejo/scripts/e2e-run.sh` startet den TCP-Relay `e2e/support/tcp-forward.mjs` (127.0.0.1:54321/54322/54324/9998 -> dind-Gateway) und ruft `npx playwright test --project=api --project=web --project=throttle` auf. Der Relay ist nötig, weil die Ports im dind liegen, die Tests, die Web-App (CORS/Origin) und `e2e/support/db.ts` (erlaubt nur lokale Hosts) aber 127.0.0.1 erwarten. Die statische Auslieferung der Web-App auf Port 3000 übernimmt das `webServer` der Playwright-Konfiguration (`serve -s`). 5. Bei Fehlern werden `e2e/playwright-report` und `e2e/test-results` (Traces) als Artefakt `playwright-` hochgeladen. Der Schritt „Aufräumen“ (`if: always()`) entfernt den Extra-Container und stoppt den Supabase-Stack. Das Projekt `spike` (SMTP-Ausfall, `SPIKE_GOTRUE_URL`) läuft in CI nicht. Der Job hat `needs: supabase-tests` (gleiche Container-Namen und Ports im dind) und läuft nicht für Fork-PRs: Wer PR-Code auf `docker-host` ausführt, hat Zugriff auf den dind-Daemon des Runners. **Achtung Artefakte:** Playwright-Reports, Traces und Screenshots (CI-Artefakte, `test-results/`, `playwright-report/`) enthalten Tokens, Passwörter und Mailinhalte der angelegten Testkonten. Die Konten existieren nur in der Wegwerf-Instanz (lokale CLI/CI), dennoch Artefakte nicht in ein öffentliches Repo oder eine öffentliche Release-Seite legen und nur kurz aufbewahren. Die Tests lehnen nicht-lokale Hosts ab (`SUPABASE_URL`, `MAILPIT_URL`, `E2E_DB_URL`; CI-Hosts nur explizit über `E2E_ALLOWED_HOSTS`). ### SMTP-Ausfall (Spike #37, Stand 2026-11-03, GoTrue `v2.197.0`) Messung gegen einen zusätzlichen GoTrue-Container, dessen SMTP-Port 1 die Verbindung ablehnt (`e2e/support/start-throttle-gotrue.sh start smtp-down`, Test `e2e/tests/spike/smtp-down.spec.ts`). Mailpit bleibt für die anderen Tests unberührt. Die Tests prüfen nichts, sie protokollieren. | Aufruf | bekannte Adresse | unbekannte Adresse | |---|---|---| | `POST /recover` | **500** `unexpected_failure` „Error sending recovery email“ | **200** | | `POST /resend` (signup) | unbestätigt: **500** „Error sending confirmation email“ | **200** | | `POST /signup` | unbestätigt: **500**; bestätigt: **200** (erfundener Nutzer, kein Versand) | neu: **500** „Error sending confirmation email“, Nutzer wird **nicht** angelegt (Rollback) | Antwortzeiten liegen alle im einstelligen bis niedrigen zweistelligen Millisekundenbereich (Verbindung wird sofort abgelehnt), also kein Zeit-Seitenkanal. **Befund:** Bei nicht erreichbarem SMTP unterscheiden sich die Antworten von `/recover`, `/resend` und `/signup` je nach Existenz des Kontos (500 vs. 200), und zwar nicht nur bei erschöpftem Mail-Limit (R3, F12). Die rohe API verrät dann die Existenz; laut Mapping 4.4 (nicht getestet) würde die App bei Reset 500 als `unexpected`, bei Registrierung als `registrationRejected` anzeigen, also einen Fehler statt des neutralen Bildschirms. Ob die App bei HTTP 500 aus Sendefehlern neutral bleiben soll (wie bei `over_email_send_rate_limit`), ist offen (Issue #37). Nicht untersucht: ein SMTP-Server, der die Verbindung annimmt, aber nicht antwortet (Timeout), und GoTrue hinter dem Produktions-Gateway. Dafür den Container mit einer nicht routbaren SMTP-Adresse (z. B. `GOTRUE_SMTP_HOST=10.255.255.1`) starten und `GOTRUE_SMTP_PORT` bis zum Timeout beobachten.