From bfd32d1aa76fc4e55c8ecf51fb8478a1a66383b2 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Fri, 17 Jul 2026 23:53:13 +0200 Subject: [PATCH] =?UTF-8?q?Konfigurierbare=20=C3=B6ffentliche=20URL=20(APP?= =?UTF-8?q?=5FURL)=20inkl.=20Passkey-Anbindung?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Neue Umgebungsvariable APP_URL (docker-compose reicht sie durch, Default http://localhost:3000): gilt beim CSRF-Origin-Check als eigene Origin, falls ein Reverse-Proxy den Host-Header umschreibt - scripts/configure-passkeys.mjs setzt die Passkey-Domain (WebAuthn rp_id/origins) des Supabase-Projekts auf APP_URL – ein Befehl statt manueller Dashboard-Konfiguration beim Domain-Wechsel --- .env.example | 7 +++++ README.md | 22 ++++++++++++---- docker-compose.yml | 5 ++++ lib/origin.ts | 7 ++++- scripts/configure-passkeys.mjs | 47 ++++++++++++++++++++++++++++++++++ 5 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 scripts/configure-passkeys.mjs diff --git a/.env.example b/.env.example index 17a65ba..14c6e49 100644 --- a/.env.example +++ b/.env.example @@ -9,3 +9,10 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY= # Service-Role-Key (GEHEIM! Nur serverseitig, für die Benutzerverwaltung) SUPABASE_SERVICE_ROLE_KEY= + +# Öffentliche URL, unter der die App erreichbar ist (Schema + Domain). +# Wird für den CSRF-Origin-Check hinter Reverse-Proxies genutzt und ist +# zugleich die Passkey-Domain: nach einer Änderung einmalig +# SUPABASE_ACCESS_TOKEN= node scripts/configure-passkeys.mjs +# ausführen, damit Passkeys auf der neuen Domain funktionieren. +APP_URL=http://localhost:3000 diff --git a/README.md b/README.md index 754a829..a1c5072 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ Beim Webserver/Hoster müssen genau diese drei Variablen hinterlegt werden | `NEXT_PUBLIC_SUPABASE_URL` | URL des Supabase-Projekts, z. B. `https://hvzikwjoyhoakedlywhj.supabase.co` | nein | | `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Öffentlicher anon-Key (RLS schützt die Daten) | nein | | `SUPABASE_SERVICE_ROLE_KEY` | Service-Role-Key für die Benutzerverwaltung | **ja – niemals im Browser/Repo!** | +| `APP_URL` | Öffentliche URL der App, z. B. `https://menu.example.de` (Origin-Check hinter Reverse-Proxy; zugleich Passkey-Domain) | nein | Build & Start auf dem Server – **mit Docker** (empfohlen, Node.js und PostgreSQL müssen nicht installiert sein): @@ -87,12 +88,23 @@ npm run start # Port 3000, bindet an 0.0.0.0 (hinter Reverse-Proxy betr ### Passkeys für die Produktions-Domain freischalten Passkeys sind an die Domain gebunden. Aktuell ist `localhost` konfiguriert. -Vor dem Livegang in der Supabase-Konfiguration (Management API oder -Dashboard → Authentication → Settings) anpassen: +Vor dem Livegang: `APP_URL` auf die öffentliche URL setzen und einmalig -- `webauthn_rp_id` = `menu.example.de` (die echte Domain, ohne Protokoll) -- `webauthn_rp_origins` = `https://menu.example.de` -- `passkey_enabled` = `true` (bereits aktiv) +```bash +SUPABASE_ACCESS_TOKEN= node scripts/configure-passkeys.mjs +``` + +ausführen (liest `APP_URL` aus der Umgebung; die URL kann alternativ als +Argument übergeben werden). Ohne lokales Node.js geht es auch über Docker: + +```bash +docker run --rm -v .:/app -w /app \ + -e SUPABASE_ACCESS_TOKEN= -e APP_URL=https://menu.example.de \ + node:24-alpine node scripts/configure-passkeys.mjs +``` + +Achtung: Bereits registrierte Passkeys der alten Domain verlieren dabei +ihre Gültigkeit und müssen neu registriert werden. ## Datenbank diff --git a/docker-compose.yml b/docker-compose.yml index 923930f..a81f3cd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,6 +30,10 @@ services: NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL} NEXT_PUBLIC_SUPABASE_ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY} SUPABASE_SERVICE_ROLE_KEY: ${SUPABASE_SERVICE_ROLE_KEY} + # Öffentliche URL der App (Domain hinter dem Reverse-Proxy). + # Wird für den CSRF-Origin-Check genutzt; dieselbe URL ist die + # Passkey-Domain (siehe scripts/configure-passkeys.mjs). + APP_URL: ${APP_URL:-http://localhost:3000} ports: - "0.0.0.0:3000:3000" restart: unless-stopped @@ -63,6 +67,7 @@ services: NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL} NEXT_PUBLIC_SUPABASE_ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY} SUPABASE_SERVICE_ROLE_KEY: ${SUPABASE_SERVICE_ROLE_KEY} + APP_URL: ${APP_URL:-http://localhost:3000} ports: - "0.0.0.0:3000:3000" volumes: diff --git a/lib/origin.ts b/lib/origin.ts index 518150e..510eed8 100644 --- a/lib/origin.ts +++ b/lib/origin.ts @@ -10,7 +10,12 @@ export function isSameOrigin(request: Request): boolean { if (!origin) return true; const host = request.headers.get("host"); try { - return new URL(origin).host === host; + const originUrl = new URL(origin); + if (originUrl.host === host) return true; + // Hinter einem Reverse-Proxy, der den Host-Header umschreibt, gilt + // zusätzlich die öffentlich konfigurierte App-URL als eigene Origin. + const appUrl = process.env.APP_URL; + return !!appUrl && originUrl.origin === new URL(appUrl).origin; } catch { return false; } diff --git a/scripts/configure-passkeys.mjs b/scripts/configure-passkeys.mjs new file mode 100644 index 0000000..8e2670b --- /dev/null +++ b/scripts/configure-passkeys.mjs @@ -0,0 +1,47 @@ +// Konfiguriert die Passkey-Domain (WebAuthn Relying Party) des +// Supabase-Projekts auf die öffentliche App-URL. Passkeys sind an die +// Domain gebunden – nach einem Domain-Wechsel einmal ausführen. +// +// Aufruf: +// SUPABASE_ACCESS_TOKEN=... APP_URL=https://menu.example.de node scripts/configure-passkeys.mjs +// oder mit der URL als Argument: +// SUPABASE_ACCESS_TOKEN=... node scripts/configure-passkeys.mjs https://menu.example.de + +const PROJECT_REF = "hvzikwjoyhoakedlywhj"; +const token = process.env.SUPABASE_ACCESS_TOKEN; +const rawUrl = process.argv[2] ?? process.env.APP_URL; + +if (!token || !rawUrl) { + console.error( + "Nutzung: SUPABASE_ACCESS_TOKEN=... [APP_URL=https://…] node scripts/configure-passkeys.mjs [url]" + ); + process.exit(1); +} + +const url = new URL(rawUrl); +const res = await fetch( + `https://api.supabase.com/v1/projects/${PROJECT_REF}/config/auth`, + { + method: "PATCH", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + passkey_enabled: true, + webauthn_rp_id: url.hostname, + webauthn_rp_display_name: "Canteen Menu", + webauthn_rp_origins: url.origin, + }), + } +); + +if (!res.ok) { + console.error(`Fehler ${res.status}: ${await res.text()}`); + process.exit(1); +} + +console.log(`Passkey-Domain gesetzt: rp_id=${url.hostname}, origin=${url.origin}`); +console.log( + "Hinweis: Bereits auf einer anderen Domain registrierte Passkeys verlieren ihre Gültigkeit." +);