Skip to content

Let login work on Vercel preview deployments - #17

Merged
sjmiller609 merged 1 commit into
mainfrom
hypeship/preview-origin-login
Jul 27, 2026
Merged

Let login work on Vercel preview deployments#17
sjmiller609 merged 1 commit into
mainfrom
hypeship/preview-origin-login

Conversation

@sjmiller609

@sjmiller609 sjmiller609 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Signing in on a preview deployment fails with "Request rejected (bad origin)". originOk compares the Origin header against the hardcoded canonical ORIGIN, and a preview is served from its own hostname, so every login form POST is treated as cross-site. Local dev hits the same wall — which is why DEVELOPMENT.md tells you to paste a session cookie into devtools instead of using the login form.

Two things were broken, not one

Fixing the origin check alone still leaves preview login broken: the magic link is also built from ORIGIN (app/login/route.ts), so you'd submit the form on the preview and receive an email pointing at production. Both now resolve through the same set of hostnames this deployment actually answers on.

Where that set comes from, and why it matters

The allowlist is built from Vercel's own runtime env — VERCEL_URL (per-deployment host) and VERCEL_BRANCH_URL (branch alias) — and is empty in production, where ORIGIN remains the only accepted answer. Local dev (no VERCEL_ENV) adds localhost/127.0.0.1.

It is deliberately not derived from the request's Host header. linkOrigin puts a single-use login token into an email, so trusting Host there would be host-header injection: an attacker could POST with Host: evil.com and have the victim's login link delivered to their domain. An unrecognized Host always falls back to ORIGIN.

Cross-site POSTs stay rejected everywhere. A browser sets Origin itself, so an attacker page can never present one of our deployment origins.

Verification

8 new unit tests in lib/auth/request.test.ts cover production strictness (including with VERCEL_URL present), preview deployment + branch hosts, localhost only outside Vercel, and linkOrigin refusing a forged Host — including a look-alike host that merely ends with the right suffix.

Reproduced and confirmed against a local dev server. Before, on main:

POST /login  Origin: http://localhost:3000   -> 403  "Request rejected (bad origin)."

After, with VERCEL_ENV=preview and the two host vars set (500 = the origin check passed and it reached the email send, which fails on a dummy Resend key):

preview deployment host        -> 500  COULDN'T SEND
preview branch host            -> 500  COULDN'T SEND
canonical origin               -> 500  COULDN'T SEND
cross-site (evil.example)      -> 403  bad origin
localhost (while in preview)   -> 403  bad origin

Full suite (149 tests), tsc --noEmit, spec:check and next build all pass.

One thing to check on the Vercel side

This makes the app accept and complete a preview login, but a preview still needs PLANETSCALE_URL and RESEND_API_KEY present in the Preview environment (not just Production), or the flow fails at the DB or email step instead. Worth confirming those are set for Preview before testing.

Note that previews share whatever database PLANETSCALE_URL points at, so a preview login is a real login against that data.

🤖 Generated with Claude Code


Note

Medium Risk
Touches CSRF origin checks and where single-use login links are built; behavior is constrained by Vercel env rather than client Host, with new tests, but auth paths warrant careful review.

Overview
Fixes "bad origin" on Vercel previews and local dev by widening originOk to accept ORIGIN plus deployment-specific origins from VERCEL_URL, VERCEL_BRANCH_URL, and localhost when not on Vercel; production keeps ORIGIN only.

Magic-link URLs in /login now use linkOrigin(req) instead of a hardcoded ORIGIN, so a sign-in started on a preview finishes on that preview rather than production.

linkOrigin only trusts forwarded host/proto when they match that same server-side allowlist; forged or unknown hosts fall back to ORIGIN so emailed tokens cannot be pointed at attacker domains.

Adds lib/auth/request.test.ts covering preview/production strictness, localhost rules, and host-forgery cases.

Reviewed by Cursor Bugbot for commit f5a9c79. Bugbot is set up for automated code reviews on this repo. Configure here.

Signing in on a preview failed with "Request rejected (bad origin)":
originOk compared the Origin header against the hardcoded canonical
ORIGIN, and a preview is served from its own hostname. Local dev hit the
same wall, which is why DEVELOPMENT.md tells you to paste a session
cookie instead.

Fixing only that check would still leave preview login broken, because
the magic link was also built from ORIGIN — you would submit the form on
a preview and get an email pointing at production. Both now resolve
through the set of hostnames this deployment actually answers on.

That set comes from Vercel's own runtime env (VERCEL_URL,
VERCEL_BRANCH_URL), never from the request, and is empty in production.
Deriving it from the Host header would be host-header injection: an
attacker could POST with Host: evil.com and have the victim's single-use
login token emailed as a link to their domain. Cross-site POSTs stay
rejected everywhere, since a browser sets Origin itself and an attacker
page cannot present one of our deployment origins.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
justhtml Ready Ready Preview, Comment Jul 27, 2026 5:08pm

@sjmiller609
sjmiller609 marked this pull request as ready for review July 27, 2026 17:12
@sjmiller609
sjmiller609 merged commit 25e1b32 into main Jul 27, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant