Local Development with OAuth #
Logging in locally works on a loopback origin — no tunnel, no signing key, no
published metadata. That is the atproto spec's localhost exception: when the
client_id is http://localhost?..., the authorization server synthesises the
client metadata from the client_id's own query parameters instead of fetching a
metadata document.
Two things to get right:
1. Open the app on 127.0.0.1, not localhost. The spec allows the
client_id to be http://localhost but requires redirect URIs to be on
127.0.0.1 or [::1], so the app builds its redirect as
http://127.0.0.1:<port>/auth/callback. Browse the same origin, otherwise the
session cookie is set on a host you are not on.
2. Point each dev server's own env file at that origin. Both are gitignored, so a stale value is easy to miss; in particular, if either still points at the deployed URL, clicking Login hands you to production and the session lands on the wrong origin.
| Dev server | Reads | Start | PUBLIC_BASE_URL |
|---|---|---|---|
wrangler pages dev (port 8788) |
.dev.vars |
npx wrangler pages dev --port 8788 |
http://127.0.0.1:8788 |
npm run dev (Vite, port 5173) |
.env |
npm run dev |
http://127.0.0.1:5173 |
wrangler pages dev serves the built worker, so run npm run build first (or
npm run watch); it also provides the D1 and KV bindings that the Vite dev
server does not have.
Loopback client vs. deployed client #
| Loopback | Deployed | |
|---|---|---|
| Client type | public (token_endpoint_auth_method: none) |
confidential (private_key_jwt) |
client_id |
http://localhost?scope=...&redirect_uri=..., built by the library at startup |
<PUBLIC_BASE_URL>/oauth/v2/client-metadata.json |
| Metadata | synthesised by the authorization server | published by this app, fetched by the authorization server |
PRIVATE_KEY_JWK |
unused | required |
getOAuthClient picks between the two from PUBLIC_BASE_URL alone.
Testing the deployed client configuration locally #
The deployed client publishes its metadata and signs its requests, so the authorization server has to be able to reach it over HTTPS. To exercise that path locally, tunnel the dev server and use the tunnel origin in the env file (this is the only reason a tunnel is needed):
npx wrangler pages dev --port 8788
npx ngrok http 8788 # https://abc123.ngrok-free.app
# .dev.vars: PUBLIC_BASE_URL=https://abc123.ngrok-free.app
The free ngrok URL changes on every restart, so update the env file and restart
the dev server each time. PRIVATE_KEY_JWK must be set (npm run setup:key).
Troubleshooting #
- You end up on the production site when clicking Login.
PUBLIC_BASE_URLin the env file the running dev server reads still points at production. Missing sec-fetch-site headerfrombsky.social. You opened theauthorizationUrloutside a browser (curl, a script). The authorization endpoint requires normal browser navigation; the login endpoint returns the URL for the page to navigate to rather than redirecting.- Logged out immediately after logging in. The callback landed on a
different origin than the one you are browsing (
localhostvs127.0.0.1).
Production #
PUBLIC_BASE_URL is the deployed origin (https://go.sky.boo), set in
wrangler.toml. PRIVATE_KEY_JWK, SESSION_SECRET and the KV bindings are
Cloudflare secrets/bindings — see CLOUDFLARE_DEPLOYMENT.md.