Skip to content

feat(auth): derive the OAuth redirect_uri from the request host - #73

Merged
ralyodio merged 1 commit into
masterfrom
feat/oauth-per-host-redirect
Jul 31, 2026
Merged

feat(auth): derive the OAuth redirect_uri from the request host#73
ralyodio merged 1 commit into
masterfrom
feat/oauth-per-host-redirect

Conversation

@ralyodio

Copy link
Copy Markdown
Collaborator

Makes "Log in with CoinPay" work on pit.moshcode.sh, not just moshcoding.com.

The bug

redirect_uri was pinned to APP_BASE_URL (lib/oauth.ts:7), so starting login on pit.moshcode.sh produced:

redirect_uri=https%3A%2F%2Fmoshcoding.com%2Fauth%2Fcoinpay%2Fcallback

This isn't just "lands on the wrong host" — it cannot succeed at all:

  1. /auth/login sets cp_pkce / cp_state as host-only cookies (path:"/", no domain) on pit.moshcode.sh.
  2. The IdP sends the browser to moshcoding.com.
  3. Those cookies aren't there → callback returns Login failed: state mismatch before the token exchange.

A shared cookie can't fix it: moshcoding.com and moshcode.sh are different registrable domains.

The fix

New lib/oauth-origin.ts. Authorize and callback both resolve the origin the same way, so the redirect_uri matches byte for byte at token exchange (OAuth requires this), and the post-login redirect returns to the host the user started on — where the session cookie actually applies.

authorizeUrl() and exchangeCode() take an optional redirectUri, defaulting to the old constant, so nothing else changes behaviour.

Security

The Host header is attacker-controlled. Reflecting it into a redirect_uri unchecked would hand the authorization code to whatever host an attacker names, so a host is allowlisted before it can become one:

  • APP_BASE_URL's own host — always
  • anything in the new OAUTH_ALLOWED_HOSTS (comma-separated)
  • anything else → falls back to APP_BASE_URL rather than being reflected

The IdP's own redirect_uri registration is a second line of defense here, not the first. x-forwarded-host is preferred over host (the platform edge sets it; bare host can be the internal upstream) but is allowlisted just the same.

Covered by tests: suffix attacks (evilpit.moshcode.sh, pit.moshcode.sh.evil.example), appended ports, comma-joined header spoofs in both positions, case folding, empty allowlist entries, and a malformed APP_BASE_URL.

Verification

  • bun test tests/62 pass, 0 fail (16 new)
  • tsc --noEmit → clean
  • bun run build → clean

Deploy steps (not done by this PR)

  1. Register https://pit.moshcode.sh/auth/coinpay/callback as a second redirect URI on CoinPay client cp_f246043e2ae8a463673d5ece.
  2. Set OAUTH_ALLOWED_HOSTS=pit.moshcode.sh on the moshcoding.com Railway service.

Until both are done, pit falls back to the current behaviour — no regression.

🤖 Generated with Claude Code

The app answers on more than one hostname -- moshcoding.com and
pit.moshcode.sh both route to this service -- but redirect_uri was pinned
to APP_BASE_URL, so CoinPay login only worked on moshcoding.com.

Starting login from pit.moshcode.sh was not merely landing on the wrong
host, it could not succeed at all. /auth/login sets cp_pkce and cp_state
as host-only cookies on pit.moshcode.sh, then the IdP sends the browser
to moshcoding.com, where those cookies are absent -- so the callback
failed "state mismatch" before reaching the token exchange.

Both the authorize step and the callback now resolve the origin through
lib/oauth-origin, so the redirect_uri matches byte for byte at token
exchange, and the post-login redirect returns to the host the user
started from, where the session cookie actually applies.

The Host header is attacker-controlled, so it is allowlisted before it
can become a redirect_uri: APP_BASE_URL's own host always, plus anything
in OAUTH_ALLOWED_HOSTS. An unlisted host falls back to APP_BASE_URL
rather than being reflected, which keeps an authorization code from being
sent to a spoofed host. The IdP's own redirect_uri registration is a
second line of defense here, not the first. OAUTH_REDIRECT_URI still
pins a single URI for anyone who wants the old behaviour.

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

Copy link
Copy Markdown

vu1nz Security Review

0 finding(s) in PR #?

No security issues found.

@ralyodio
ralyodio merged commit 11485af into master Jul 31, 2026
2 checks passed
@ralyodio
ralyodio deleted the feat/oauth-per-host-redirect branch July 31, 2026 02:24
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