|
| 1 | +# Single-Domain Migration: Collapse `auth.<host>` into the PDS origin |
| 2 | + |
| 3 | +**Status:** Proposal (tracked in [#200](https://github.com/hypercerts-org/ePDS/issues/200)) |
| 4 | + |
| 5 | +This document designs the migration from the current two-origin architecture |
| 6 | +(`auth.pds.example` + `pds.example`) to a single origin, where the Auth Service |
| 7 | +is served under a path prefix (e.g. `pds.example/auth/*`) on the PDS host. |
| 8 | + |
| 9 | +For _why_ this is worth doing, see the "Auth Service on a subdomain" decision |
| 10 | +in [../architecture.md](../architecture.md) and the cost inventory in |
| 11 | +[pds-white-boxing.md](pds-white-boxing.md) items 5, 14, 15. This doc assumes |
| 12 | +that case is accepted and focuses on _how_. |
| 13 | + |
| 14 | +## Summary of the change |
| 15 | + |
| 16 | +| Concern | Today (two origins) | After (one origin) | |
| 17 | +| --- | --- | --- | |
| 18 | +| Auth UI location | `https://auth.pds.example/oauth/authorize`, `/auth/*`, `/account/*` | `https://pds.example/auth/*` (path-prefixed) | |
| 19 | +| `authorization_endpoint` (AS metadata) | `https://auth.<host>/oauth/authorize` | `https://<host>/auth/oauth/authorize` | |
| 20 | +| `sec-fetch-site` on `/oauth/authorize` | `same-site`, rewritten to `same-origin` (item 5) | naturally `same-origin` — rewrite **deleted** | |
| 21 | +| Device-session cookies | `Domain=<parent>` so sibling subdomain can read them (items 14–15) | host-only on the single origin — **plumbing deleted** | |
| 22 | +| Auth session cookie | `magic_account_session` / better-auth `session` on `auth.<host>` | same names, path-scoped `/auth` on `<host>` | |
| 23 | +| CSP | per-route in each service (unchanged approach) | per-route, unchanged | |
| 24 | +| Deploy units | two services | still two processes, one origin (Caddy path-routes) — or merged later | |
| 25 | + |
| 26 | +The routing model is exactly pds-gatekeeper's: **Caddy routes by path**, sending |
| 27 | +`/auth/*` (and the auth-owned `/oauth/authorize`, `/account/*`) to the |
| 28 | +auth-service process and everything else to the PDS. The auth-service stays a |
| 29 | +separate process; only its _origin_ changes. |
| 30 | + |
| 31 | +## What gets deleted |
| 32 | + |
| 33 | +The whole point. These exist solely because of cross-origin-same-site: |
| 34 | + |
| 35 | +1. **`packages/pds-core/src/lib/sec-fetch-site-rewrite.ts`** — the `same-site` → |
| 36 | + `same-origin` rewrite (white-boxing item 5). On one origin the header is |
| 37 | + `same-origin`, which upstream `@atproto/oauth-provider` already accepts. |
| 38 | + Delete the middleware and its `_router.stack` injection. |
| 39 | +2. **Cross-subdomain device-cookie plumbing** (white-boxing items 14–15) — the |
| 40 | + `Domain=<parent>` broadening in `pds-core` and the sibling-reading logic in |
| 41 | + `auth-service/src/lib/session-reuse.ts`. On one origin the device session is |
| 42 | + directly readable; cookies stay host-only. |
| 43 | +3. **`authHostname.endsWith('.' + pdsHostname)` special-casing** — e.g. |
| 44 | + `session-reuse.ts:195-200`, the `parentCookieDomain` derivation in |
| 45 | + `pds-core/src/index.ts:930`, and `authOrigin` derivation in |
| 46 | + `chooser-enrichment.ts`. The two hostnames become one. |
| 47 | + |
| 48 | +## What stays |
| 49 | + |
| 50 | +- The `authorization_endpoint` **metadata override** (`pds-core/src/index.ts:643`). |
| 51 | + Still needed — it just points at a path on the same host instead of a |
| 52 | + subdomain. The upstream sign-in UI is still never shown. |
| 53 | +- The **HMAC-signed `/oauth/epds-callback`** bridge. The trust boundary between |
| 54 | + auth-service and pds-core is unchanged; they still communicate over |
| 55 | + authenticated HTTP, not shared memory. |
| 56 | +- **Per-route CSP** in both packages. No change. |
| 57 | +- The **handle picker, consent, OTP** flows. No change to their logic. |
| 58 | + |
| 59 | +## Migration blockers (external addressability of `auth.<host>`) |
| 60 | + |
| 61 | +None of the three stated benefits is a blocker; these published-URL references |
| 62 | +are the real work. |
| 63 | + |
| 64 | +### 1. `authorization_endpoint` is cached by OAuth clients |
| 65 | + |
| 66 | +`pds-core/src/index.ts:643` publishes `https://auth.<host>/oauth/authorize` in |
| 67 | +the AS metadata document. Clients fetch this and may cache it. A hard switch |
| 68 | +would strand in-flight and cached clients. |
| 69 | + |
| 70 | +**Mitigation:** keep `auth.<host>` resolving during a transition window. Serve a |
| 71 | +301/302 (or continue proxying) from `auth.<host>/oauth/authorize` → |
| 72 | +`<host>/auth/oauth/authorize` until metadata TTLs expire and clients re-fetch. |
| 73 | +Only retire the subdomain DNS/cert after the window. |
| 74 | + |
| 75 | +### 2. `EPDS_LINK_BASE_URL` appears in live email links |
| 76 | + |
| 77 | +`auth-service/.env.example:94` — `EPDS_LINK_BASE_URL=https://auth.pds.example/auth/verify`. |
| 78 | +Verification/recovery links already delivered to inboxes point at the subdomain. |
| 79 | + |
| 80 | +**Mitigation:** same transition redirect covers these. Update the env var to the |
| 81 | +new path-based URL for _new_ emails; keep the subdomain redirecting for the |
| 82 | +lifetime of the longest-lived link (OTP/verification TTLs are short — 600s per |
| 83 | +`auth-flow.ts` — so this window is small). |
| 84 | + |
| 85 | +### 3. `AUTH_HOSTNAME` is load-bearing config |
| 86 | + |
| 87 | +Referenced in `pds-core/src/index.ts:138`, `auth-service/src/index.ts:138`, |
| 88 | +`session-reuse.ts`, `chooser-enrichment.ts` (`authOrigin`), |
| 89 | +`sec-fetch-site-rewrite.ts` allowlist, `demo/.env.example` (`AUTH_ENDPOINT`). |
| 90 | + |
| 91 | +**Approach:** introduce an `AUTH_PATH_PREFIX` (default `/auth`) and derive the |
| 92 | +auth origin as `PDS_HOSTNAME + AUTH_PATH_PREFIX`. Keep `AUTH_HOSTNAME` as an |
| 93 | +optional legacy override so existing subdomain deployments keep working during |
| 94 | +transition (config-gated, not a hard cutover). |
| 95 | + |
| 96 | +### Non-blockers (confirmed) |
| 97 | + |
| 98 | +- **No AT Protocol identity coupling.** DIDs are `did:web:<pds-host>` (the PDS |
| 99 | + host), never `auth.<host>`. Grep for `did:web` shows only PDS-host and test |
| 100 | + fixtures. |
| 101 | +- **OAuth `redirect_uri` is always the client's**, never the auth service's. |
| 102 | + Nothing to migrate there. |
| 103 | +- **CORS.** The only `Access-Control-Allow-Origin: *` headers are on the demo's |
| 104 | + public JSON documents (`client-metadata.json`, `jwks.json`) and a pds-core |
| 105 | + metadata response — none depend on the auth origin being distinct. |
| 106 | + |
| 107 | +## Phased rollout |
| 108 | + |
| 109 | +1. **Config plumbing.** Add `AUTH_PATH_PREFIX`; derive auth origin from it; |
| 110 | + keep `AUTH_HOSTNAME` as a legacy override. No behaviour change yet. |
| 111 | +2. **Caddy path routing.** Add a single-origin Caddyfile variant that routes |
| 112 | + `/auth/*`, `/oauth/authorize`, `/account/*` to auth-service and the rest to |
| 113 | + the PDS. Stand it up in a test/preview env. |
| 114 | +3. **Delete the cross-origin workarounds** (items 5, 14, 15 above) _behind the |
| 115 | + single-origin config path_ so subdomain deployments are unaffected until they |
| 116 | + flip. |
| 117 | +4. **Transition redirects.** Serve `auth.<host>/*` → `<host>/auth/*` for the |
| 118 | + metadata-TTL + email-link-TTL window. |
| 119 | +5. **Cut over** `authorization_endpoint` and `EPDS_LINK_BASE_URL` to the new |
| 120 | + path-based URLs. |
| 121 | +6. **Retire** the `auth.<host>` DNS record, TLS cert, and the legacy |
| 122 | + `AUTH_HOSTNAME` code paths after the transition window. |
| 123 | + |
| 124 | +## Open questions |
| 125 | + |
| 126 | +- Do we keep two processes (Caddy path-routes) indefinitely, or eventually merge |
| 127 | + auth-service into the pds-core process? The process split is cheap to keep and |
| 128 | + preserves the HMAC trust boundary; recommend keeping it and only collapsing |
| 129 | + the _origin_. |
| 130 | +- Preview/e2e environments assume the subdomain in several fixtures |
| 131 | + (`preview.ts`, `preview-emails.ts`, `demo/.env.example`). These need updating |
| 132 | + in lockstep with step 1; enumerate them before starting. |
| 133 | +- Does any downstream trusted client hardcode `auth.<host>` anywhere other than |
| 134 | + by fetching AS metadata? Needs a check with the known trusted-client operators |
| 135 | + before retiring the subdomain (step 6). |
0 commit comments