Shuffle/docs/auth-konfiguration.md
cschlaefke 59a47c5a5d feat(auth): configure Supabase Auth and add de/en mail templates
Adds AUTH-CFG settings, confirmation/recovery templates and
docs/auth-konfiguration.md (KTO-01 criteria 1, 3, 4, 7, 8, 11).

Refs #1

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-02 23:23:06 +01:00

18 KiB
Raw Blame History

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 <Site-URL>/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.<typ>.enabled = false / GOTRUE_MAILER_NOTIFICATIONS_<TYP>_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.

# 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://<vorlagen-host>/confirmation.html
GOTRUE_MAILER_TEMPLATES_RECOVERY=https://<vorlagen-host>/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":"<grund>"}. 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.