Fix auth end-to-end and add Cloudflare verification handoff - #55
Conversation
…dation Production sign-in skipped onboarding because the Cloudflare x-forwarded-host rewrite overwrote the redirect path. Enforce handle setup in the proxy, block open redirects, and verify sessions with getUser() on the server. Co-authored-by: Cursor <cursoragent@cursor.com>
Preserve proxy redirect cookie forwarding, onboarding profile upsert, and callback error logging. Add DEVELOPER_HANDOFF_PRODUCT_BUILDERS.md for the next developer debugging auth on fix/auth. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Exercises every public and protected route plus the auth-callback error paths against a running dev server. Asserts status codes and Location headers without triggering Supabase auth flows, touching the database, or printing tokens / cookies / env values. Usage: ./scripts/auth-smoke.sh BASE_URL=https://productbuilders.app ./scripts/auth-smoke.sh
Follow-up hardening (this push)Two changes added since the initial review of this PR: 1.
|
| Command | Result |
|---|---|
npm run lint |
pass |
npm run build |
pass — 14 routes |
scripts/auth-smoke.sh (default) |
16 passed, 0 failed |
BASE_URL=http://localhost:3000 ./scripts/auth-smoke.sh |
16 passed, 0 failed |
normalize_location() unit tests |
10 passed |
Direct callback probes (/auth/callback with no code / empty code / provider error) |
All redirect to /login?error=... as expected |
Remaining owner actions (unchanged from before)
All previous Supabase / Google / Cloudflare dashboard checks still apply. In addition:
- Set
NEXT_PUBLIC_SITE_URLper environment in Cloudflare Pages (e.g.https://productbuilders.appfor Production, thepages.devpreview URL for Preview).
No destructive operations. No secrets committed. No force-push.
ceb0bcb to
5a0e834
Compare
Consolidates the architectural map, root-cause analysis, fixes, dashboard checklist, manual test steps, and remaining risks for the auth work on this branch. Intended to be readable by the owner and any future contributor without prior context.
…cklist The deployment target is Cloudflare Pages via OpenNext, not Vercel. Update README, OPERATIONS, the developer handoff, and this doc to: - describe the Cloudflare Pages + OpenNext deployment model - move the demo-day cron from vercel.json into a Cloudflare Cron Triggers template (wrangler.toml) - document required Cloudflare env vars and their visibility (public at build time vs secret) - list the wrangler.toml placeholders as 'Owner must confirm' - keep vercel.json as legacy (Cloudflare ignores it) until the cron is verified to fire from Cloudflare Vercel assumptions were removed because deployment target is Cloudflare. Remaining mentions of vercel.json are intentional historical references explaining the migration path.
Two follow-up hardenings on top of the auth work in this branch:
1. scripts/auth-smoke.sh is now BASE_URL-safe.
- Added normalize_location() that turns an absolute Location header
(http://host:port/path, https://host/path) into just /path+query.
Handles Cloudflare preview URLs, localhost, production, relative
redirects, empty input, bare host, fragments, query strings, and
case-insensitive schemes.
- All redirect assertions now check the normalized path+query against
the expected prefix, instead of substring-stripping the host (which
broke against non-localhost URLs).
2. src/app/auth/callback/route.ts no longer trusts x-forwarded-host.
The previous production branch read x-forwarded-host to build the
post-login redirect host. That value is spoofable: an attacker can
set x-forwarded-host: evil.com on a forged request and have the
callback redirect the freshly-authenticated user to evil.com.
Added getSafeRedirectOrigin(request). Strict priority order:
1. NEXT_PUBLIC_SITE_URL (owner-configured, validated as http(s):)
2. request origin in development
3. request origin in production (sane fallback; the request URL on
Cloudflare Pages is the external URL the browser sent)
x-forwarded-host is never consulted. If NEXT_PUBLIC_SITE_URL is
unset in production, the callback logs a one-shot warn and falls
back to the request origin.
Documentation:
- .env.example lists NEXT_PUBLIC_SITE_URL with a comment explaining
that it is required in production.
- README.md adds NEXT_PUBLIC_SITE_URL to the env-var list.
- docs/AUTH_DEBUGGING_HANDOFF.md adds the new env var to the
Cloudflare env-vars section, the architecture map, the root-cause
table, the auth flow, and the local-setup steps.
Validation on this commit:
- npm run lint pass
- npm run build pass (14 routes built)
- scripts/auth-smoke.sh 16 passed, 0 failed (default and explicit
BASE_URL=http://localhost:3000)
- normalize_location() unit-tested against 10 inputs (relative,
absolute localhost, absolute production, Cloudflare preview, empty,
bare host, trailing-slash host, query, fragment, uppercase scheme)
- All six checked.
No destructive operations. No secrets committed. No force-push.
5a0e834 to
17b5b93
Compare
Summary
Consolidates the auth work and adds a Cloudflare-focused deployment handoff. The auth code itself (cookie-preserving proxy redirects, onboarding upsert, callback error logging) was already on
fix/authand is verified to work; this PR brings that work forward as a focused branch and replaces Vercel assumptions in the docs.Auth fixes (verified)
src/lib/supabase/middleware.ts— addedredirectWithCookieshelper. Both redirect sites (protected-route denial, onboarding gate) forward any refreshed auth cookies so the session survives proxy-driven redirects. Without this, an expired-session refresh insidegetUser()was silently discarded on the next redirect, causing the classic "log in then loop back to /login" symptom.src/app/onboarding/actions.ts— switchedupdate→upsert({...}, { onConflict: "id" }). If thehandle_new_usertrigger never created a profile row in the live DB, the previousupdatematched zero rows silently and the user was stuck on /onboarding forever.src/app/auth/callback/route.ts— consolidated error logging to a singleconsole.errorthat records onlyerror.messageanderror.status(no tokens, codes, cookies, or env values). Removed redundant fallback path.Validation on this branch:
npm run lint— passnpm run build— pass (14 routes built)scripts/auth-smoke.sh— 16/16 passCloudflare correction
Vercel assumptions were removed because deployment target is Cloudflare. Updated:
README.md— Deployment section now describes Cloudflare Pages + OpenNext, not Vercel.OPERATIONS.md— Cron is now a Cloudflare Cron Trigger (wrangler.toml);vercel.jsonis documented as legacy.DEVELOPER_HANDOFF_PRODUCT_BUILDERS.md— replaced Vercel references with Cloudflare.docs/AUTH_DEBUGGING_HANDOFF.md— added sections G–K for Supabase URL Configuration, Google Cloud Console, Cloudflare Pages project, Cloudflare env vars (public vs secret), and local.dev.vars.vercel.jsonis intentionally left in place — it is inert on Cloudflare (Cloudflare ignores it) and acts as a historical reference for the cron schedule. Remove it once the Cloudflare Cron Trigger is verified to fire.A
wrangler.tomltemplate is included inOPERATIONS.md("Cloudflare setup" section) but not yet committed to the repo. The Owner must add it with their Cloudflare account ID / project name — those values cannot be invented. The@opennextjs/cloudflareadapter was not installed on this branch because it is a deployment-environment addition, not an auth fix.Manual owner checklist (must do, cannot be automated)
@opennextjs/cloudflare, addwrangler.tomlfromOPERATIONS.md.docs/AUTH_DEBUGGING_HANDOFF.md§ J.http://localhost:3000(dev) and the Cloudflare production domain (with/auth/callbackin Additional Redirect URLs).https://<ref>.supabase.co/auth/v1/callbackas Authorized redirect URI.product-images.What was not done (and why)
db push/ db reset against production — destructive, owner-only.src/app/api/cron/demo-day/route.ts; no new uses were added.wrangler.tomlcommitted to repo — needs the owner's account ID; template is inOPERATIONS.md.Safety notes
.env/.env.local/ secrets were committed. Branch diff excludes them.supabase/migrationsfiles were read but not modified.ghCLI.