fix: passkeys behind TLS reverse proxy#225
Conversation
Add passkeyPublicOrigin and wire it through passkey routes so origin/rpId match the browser when dev runs behind nginx. Expose dev-only /_emdash/api/dev/passkey-url, add admin messaging for insecure WebAuthn contexts, nginx repro under demos/simple, and direct kysely dependency for the simple demo Node adapter bundle. Made-with: Cursor
🦋 Changeset detectedLatest commit: a00cbab The changes in this PR will be included in the next version bump. This PR includes changesets to release 10 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
All contributors have signed the CLA ✍️ ✅ |
|
I have read the CLA Document and I hereby sign the CLA |
|
This is absolutely necessary for self hosted users. |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/blocks
@emdash-cms/cloudflare
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
|
This is great, thanks! Could you update the docs here: https://github.com/emdash-cms/emdash/blob/main/docs/src/content/docs/reference/configuration.mdx |
There was a problem hiding this comment.
Pull request overview
Fixes WebAuthn passkey registration/login when EmDash (Astro dev or Node SSR) is running behind a TLS-terminating reverse proxy by allowing the server-side WebAuthn origin/rpId to be aligned with the browser-facing public origin.
Changes:
- Add
passkeyPublicOriginto theemdash()Astro integration config, validate/normalize it to.origin, and propagate it through passkey/setup API routes viagetPasskeyConfig(). - Improve Admin UI passkey messaging by detecting insecure contexts (non-secure origins where WebAuthn is unavailable) using new
webauthn-environmenthelpers. - Add/expand unit and e2e coverage for passkey config behavior and end-to-end passkey setup using a Playwright virtual authenticator.
Reviewed changes
Copilot reviewed 21 out of 22 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| skills/building-emdash-site/references/configuration.md | Documents reverse-proxy/TLS guidance for passkeys and host allowlists. |
| packages/core/tests/unit/auth/passkey-config.test.ts | Adds unit coverage for proxy URL shapes and invalid passkeyPublicOrigin. |
| packages/core/src/auth/passkey-config.ts | Adds optional passkeyPublicOrigin override for WebAuthn origin/rpId. |
| packages/core/src/astro/routes/api/setup/admin.ts | Threads passkeyPublicOrigin into setup registration options. |
| packages/core/src/astro/routes/api/setup/admin-verify.ts | Threads passkeyPublicOrigin into setup registration verification. |
| packages/core/src/astro/routes/api/auth/signup/complete.ts | Threads passkeyPublicOrigin into signup passkey verification. |
| packages/core/src/astro/routes/api/auth/passkey/options.ts | Threads passkeyPublicOrigin into auth options generation. |
| packages/core/src/astro/routes/api/auth/passkey/verify.ts | Threads passkeyPublicOrigin into auth assertion verification. |
| packages/core/src/astro/routes/api/auth/passkey/register/options.ts | Threads passkeyPublicOrigin into registration options generation. |
| packages/core/src/astro/routes/api/auth/passkey/register/verify.ts | Threads passkeyPublicOrigin into registration response verification. |
| packages/core/src/astro/routes/api/auth/invite/complete.ts | Threads passkeyPublicOrigin into invite passkey verification. |
| packages/core/src/astro/integration/runtime.ts | Adds EmDashConfig.passkeyPublicOrigin with inline docs. |
| packages/core/src/astro/integration/index.ts | Validates passkeyPublicOrigin (http/https) and normalizes to .origin. |
| packages/admin/src/lib/webauthn-environment.ts | Adds helpers to detect secure-context + WebAuthn constructor availability. |
| packages/admin/tests/lib/webauthn-environment.test.ts | Tests new WebAuthn environment helpers. |
| packages/admin/src/components/auth/PasskeyRegistration.tsx | Improves UX copy by distinguishing insecure context vs missing WebAuthn API. |
| packages/admin/src/components/auth/PasskeyLogin.tsx | Improves UX copy and uses environment helpers for conditional mediation gating. |
| e2e/tests/passkey-full-setup-virtual-auth.spec.ts | Adds an e2e passkey setup test using a virtual authenticator. |
| e2e/fixtures/virtual-authenticator.ts | Adds CDP helper to register/remove a virtual WebAuthn authenticator. |
| e2e/fixture/emdash-env.d.ts | Updates generated types to include bylines (ContentBylineCredit). |
| demos/simple/astro.config.mjs | Adds commented examples for reverse proxy host allowlists and passkeyPublicOrigin. |
| .changeset/reverse-proxy-passkey.md | Ships the change as patch bumps for emdash and @emdash-cms/admin. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
I ran into the same passkey issue behind a reverse proxy. One thing: |
Adds the new passkeyPublicOrigin option and reverse proxy guidance to the public-facing configuration docs as requested in PR review. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
yeah i was debating something like that but i figured since it's so easily set at build time and each site would likely have it's own build step so decided against and seemed safer that way too i guess are you trying to set the site origin dynamically on start rather than having the process.env available and require a rebuild? |
|
I have read the CLA Document and I hereby sign the CLA |
Exactly, the image is prebuilt once and deployed as-is on multiple containers. Think pulling a prebuilt image on Coolify or Dokploy and just setting |
@ascorbic we updated the docs and fixed some of the failing tests lmk if you need anything else. also for this req would you rather it be added to this pr or a separate one? mostly unsure if this behavior at runtime is what you guys want for the project which is why i didnt just add it |
Overlapping PRsThis PR modifies files that are also changed by other open PRs:
This may cause merge conflicts or duplicated work. A maintainer will coordinate. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 23 out of 24 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ### `passkeyPublicOrigin` | ||
|
|
||
| Import from `emdash/db`: | ||
| **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`. |
There was a problem hiding this comment.
The ## Database Adapters section header appears to have been removed here (the file later has ## Storage Adapters, and the ### sqlite/libsql/postgres/d1 adapter docs now sit directly under “Integration Options”). This makes the document structure inconsistent and harder to navigate. Consider reintroducing a ## Database Adapters heading (and any short intro, if desired) immediately before the adapter-specific ### sqlite(config) section.
There was a problem hiding this comment.
my bad I think I lost it in a conflict will fix
|
Great work. Can you address the failing test. Copilot's review looks legit too |
@ascorbic added missing test doc and restored doc heading. waiting on tests to rerun |
|
thanks for the assist @jeftekhari (I had a busy easter sunday) |
…igin mismatch Behind a TLS-terminating reverse proxy, url.origin returns the internal address (http://localhost:4321) instead of the public one (https://mysite.example.com). PR emdash-cms#225 fixed passkeys only via passkeyPublicOrigin. This replaces it with siteUrl which fixes all origin-dependent features: CSRF, auth redirects, OAuth, MCP discovery, setup wizard, snapshot export, theme preview, sitemap, robots.txt, and JSON-LD structured data. - Add siteUrl config option with env var fallback (EMDASH_SITE_URL > SITE_URL) - Create getPublicOrigin() pure helper (Workers-safe) - Add dual-origin CSRF matching (internal + public) - Wire through all 31 affected call sites - Update docs, skills reference, and changeset Discussion: emdash-cms#315 Closes: emdash-cms#210 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Restore the original reverse-proxy-passkey.md changeset (from emdash-cms#225). Add site-url-reverse-proxy.md for this PR's changes.
What does this PR do?
Fixes WebAuthn passkey registration and login when Astro dev (or Node SSR) sits behind a TLS-terminating reverse proxy: the server often sees
http://and an internal host while the browser useshttps://and a public host, soclientData.originno longer matches the origin EmDash used in verify.passkeyPublicOriginonemdash(), validated at integration load time, normalized toorigin, and threaded through all passkey-related and setup routes that callgetPasskeyConfig().webauthn-environmenthelpers and clearer PasskeyRegistration / PasskeyLogin copy when the page is not a secure context (e.g. plainhttp://emdash.local), instead of implying missing browser support.getPasskeyConfigcoverage, including emulated proxy URL shapes; invalidpasskeyPublicOriginnow throws instead of silently falling back.passkey-full-setup-virtual-auth.spec.tsstill exercises the full passkey registration path onhttp://localhost:4444(secure context); proxy/TLS alignment is covered bypasskey-configunit tests, not a second Playwright project.building-emdash-site/references/configuration.md— reverse proxy and passkeys (passkeyPublicOrigin,security.allowedDomains,vite.server.allowedHosts).Bug discover, repro, and fix done with examples taken from @jeftekhari
Simple repro
Symptom (before): Browser is on
https://…(TLS in front), but the server still built WebAuthnorigin/rpIdfrom the upstream URL (http://127.0.0.1:…or a different host), so passkey registration/login fails with origin / verification errors.Automated (no extra repo files — always in tree):
pnpm --filter emdash exec vitest run tests/unit/auth/passkey-config.test.tsThat file exercises
getPasskeyConfig/ forwarded-host URL shapes and invalidpasskeyPublicOrigin(throws).Manual browser (TLS-terminating reverse proxy):
http://127.0.0.1:<port>(loopback).https://<host>origin.astro.config, setsecurity.allowedDomains/vite.server.allowedHostsfor that public host andemdash({ passkeyPublicOrigin: "https://<host>" })to match the browser origin (seeskills/building-emdash-site/references/configuration.md§ Reverse proxy and passkeys).POST /_emdash/api/auth/passkey/optionswith the sameHost/Originyour browser would send).Heavier manual repro (full stack)
Goal: browser hits
https://<public-host>:<edge-port>while Astro (e.g.demos/simple) listens onhttp://127.0.0.1:4321Host/X-Forwarded-Host(if your proxy strips the port fromHost, Astro’s rebuilt URL andrpIdwill be wrong — seedemos/simple/reverse-proxy/README.md§ Forwarding).astro.config:security.allowedDomains(and devvite.server.allowedHosts) for that public hostname + schemes;emdash({ passkeyPublicOrigin: "https://<public-host>:<edge-port>" })when the internal request URL is stillhttp://or otherwise diverges from the tab’s origin (skills/building-emdash-site/references/configuration.md§ Reverse proxy and passkeys).POST /_emdash/api/auth/passkey/optionswith the sameHost/Originthe browser sends — expectrpId= hostname of the user-facing origin, no “Invalid origin” during verify.Closes #210
Type of change
Checklist
pnpm typecheckpassespnpm --silent lint:json | jq '.diagnostics | length'returns 0pnpm testpasses (or targeted tests for my change)pnpm formathas been runAI-generated code disclosure
Screenshots / test output
Optional:
pnpm --filter emdash exec vitest run tests/unit/auth/passkey-config.test.tsand/orpnpm exec playwright test e2e/tests/passkey-full-setup-virtual-auth.spec.ts.