diff --git a/.changeset/site-url-reverse-proxy.md b/.changeset/site-url-reverse-proxy.md new file mode 100644 index 000000000..1abf1c12a --- /dev/null +++ b/.changeset/site-url-reverse-proxy.md @@ -0,0 +1,11 @@ +--- +"emdash": minor +--- + +Adds `siteUrl` config option to fix reverse-proxy origin mismatch. Replaces `passkeyPublicOrigin` with a single setting that covers all origin-dependent features: passkeys, CSRF, OAuth, auth redirects, MCP discovery, snapshots, sitemap, robots.txt, and JSON-LD. + +Supports `EMDASH_SITE_URL` / `SITE_URL` environment variables for container deployments where the domain is only known at runtime. + +Disables Astro's `security.checkOrigin` (EmDash's own CSRF layer handles origin validation with dual-origin support and runtime siteUrl resolution). When `siteUrl` is set in config, also sets `security.allowedDomains` so `Astro.url` reflects the public origin in templates. + +**Breaking:** `passkeyPublicOrigin` is removed. Rename to `siteUrl` in your `astro.config.mjs`. diff --git a/demos/simple/astro.config.mjs b/demos/simple/astro.config.mjs index d77553265..fe222d948 100644 --- a/demos/simple/astro.config.mjs +++ b/demos/simple/astro.config.mjs @@ -30,8 +30,8 @@ export default defineConfig({ baseUrl: "/_emdash/api/media/file", }), plugins: [auditLogPlugin()], - // HTTPS reverse proxy: uncomment so passkey verify matches browser origin - // passkeyPublicOrigin: "https://emdash.local:8443", + // HTTPS reverse proxy: uncomment so all origin-dependent features match browser + // siteUrl: "https://emdash.local:8443", }), ], devToolbar: { enabled: false }, diff --git a/docs/src/content/docs/reference/configuration.mdx b/docs/src/content/docs/reference/configuration.mdx index e1d537604..c2f911ef5 100644 --- a/docs/src/content/docs/reference/configuration.mdx +++ b/docs/src/content/docs/reference/configuration.mdx @@ -200,13 +200,13 @@ Use Cloudflare Access as the authentication provider instead of passkeys. magic links, and self-signup are disabled. -### `passkeyPublicOrigin` +### `siteUrl` -**Optional.** Pass a full **browser-facing origin** (scheme + host + optional port, **no path**) so WebAuthn **`rpId`** and **`origin`** match what the user’s browser sends in `clientData.origin`. +**Optional.** The public browser-facing origin for the site (scheme + host + optional port, **no path**). -By default, passkeys follow **`Astro.url`** / **`request.url`**. Behind a **TLS-terminating reverse proxy**, the app often still sees **`http://`** on the internal hop while the tab is **`https://`**, or the reconstructed host does not match the public name — which breaks passkey verification. Set `passkeyPublicOrigin` to the origin users type in the address bar (for example `https://cms.example.com` or `https://cms.example.com:8443`). +Behind a **TLS-terminating reverse proxy**, `Astro.url` returns the internal address (`http://localhost:4321`) instead of the public one (`https://cms.example.com`). This breaks passkeys, CSRF origin matching, OAuth redirects, login redirects, MCP discovery, snapshot exports, sitemap, robots.txt, and JSON-LD structured data. Set `siteUrl` to fix all of these at once. -The integration **validates** this value at load time: it must be a valid URL with **`http:`** or **`https:`** protocol and is normalized to **`origin`**. +The integration **validates** this value at load time: it must be a valid URL with **`http:`** or **`https:`** protocol and is normalized to **origin** (path is stripped). ```js emdash({ @@ -215,17 +215,23 @@ emdash({ directory: "./uploads", baseUrl: "/_emdash/api/media/file", }), - passkeyPublicOrigin: "https://cms.example.com", + siteUrl: "https://cms.example.com", }); ``` -#### Reverse proxy and passkeys +When `siteUrl` is not set in config, EmDash checks environment variables in order: `EMDASH_SITE_URL`, then `SITE_URL`. This is useful for container deployments where the public URL is set at runtime. + + + +#### Reverse proxy setup Astro only reflects **`X-Forwarded-*`** when the public host is allowed. Configure [**`security.allowedDomains`**](https://docs.astro.build/en/reference/configuration-reference/#securityalloweddomains) for the hostname (and schemes) your users hit. In **`astro dev`**, add matching **`vite.server.allowedHosts`** so Vite accepts the proxy **`Host`** header. -Prefer fixing **`allowedDomains`** (and forwarded headers) first; use **`passkeyPublicOrigin`** when the reconstructed URL **still** diverges from the browser origin (typical when TLS is terminated in front and the upstream request stays **`http://`**). +Prefer fixing **`allowedDomains`** (and forwarded headers) first; use **`siteUrl`** when the reconstructed URL **still** diverges from the browser origin (typical when TLS is terminated in front and the upstream request stays **`http://`**). -With TLS in front, binding the dev server to loopback (**`astro dev --host 127.0.0.1`**) is often enough: the proxy connects locally while **`passkeyPublicOrigin`** matches the public HTTPS origin. +With TLS in front, binding the dev server to loopback (**`astro dev --host 127.0.0.1`**) is often enough: the proxy connects locally while **`siteUrl`** matches the public HTTPS origin.