Skip to content

Gate onramp + faucet behind SIWE, add rate limits and origin guard#7

Merged
PierreBesson merged 12 commits into
mainfrom
session-handling
May 9, 2026
Merged

Gate onramp + faucet behind SIWE, add rate limits and origin guard#7
PierreBesson merged 12 commits into
mainfrom
session-handling

Conversation

@PierreBesson

@PierreBesson PierreBesson commented May 8, 2026

Copy link
Copy Markdown
Member

Summary

Closes the unauthenticated-mint gap flagged by the Coinbase Onramp security review (docs.cdp.coinbase.com/onramp/security-requirements). Before this change, anyone could POST to /api/onramp/session or /api/faucet with an arbitrary address and burn CDP quota. This PR introduces a SIWE-backed session, per-address rate limits, and a same-origin guard on every session-bearing route.

What changed

SIWE auth

  • New utils: utils/siwe.ts (iron-session schema + lazy getSessionOptions()), hooks/useSiweAuth.ts (ensureSignedIn() runs nonce → sign → verify on demand, in-flight de-duped).
  • New routes: /api/siwe/nonce, /api/siwe/verify, /api/siwe/session. Verify validates the SIWE message domain against the request Host, supports ERC-6492 via per-chain publicClient (mainnet, sepolia, baseSepolia, hardhat).

Rate limiting (utils/rateLimit.ts, in-memory, per-instance)

  • /api/onramp/session: 5 / 10 min per signed-in address.
  • /api/faucet: 1 / hour per (signed-in address, currency) — eth/usdc/eurc buckets are independent.

Origin guard (utils/origin.ts)

  • /api/onramp/session, /api/faucet, /api/siwe/nonce, /api/siwe/verify reject any browser request whose Origin/Referer host doesn't match Host (covers prod and Vercel preview hosts without an explicit allowlist).

Onramp specifics

  • Drops console.log that previously leaked the short-lived sessionToken URL to the browser console.
  • Passes partnerUserRef=cg-<userAddress> to Coinbase using the SIWE-verified buyer address (not the donation recipient).

Faucet specifics

  • Endpoint enforces body.address === session.address (faucets are self-only).

Frontend wiring

  • DonateWithFiatButton and TopUpModal call ensureSignedIn() before hitting their backend; both send cookies via credentials: same-origin.

Config

  • .env.example documents IRON_SESSION_SECRET (32+ chars, openssl rand -hex 32) as a production requirement.
  • Adds iron-session dependency.

Test plan

  • Set IRON_SESSION_SECRET and run yarn start; click Donate on a program — wallet sign prompt appears, popup opens with Coinbase URL containing partnerUserRef=cg-0x….
  • Click Donate again immediately — no second sign prompt (session reused).
  • On testnet, open Wallet → top-up modal → Get test ETH/USDC/EURC; second click within an hour returns 429 with the right message; other currencies still work.
  • Try curl -X POST https://<host>/api/onramp/session -d '{...}' without an Origin header → 403.
  • Try the same with Origin: https://evil.example → 403.
  • Try without a signed-in cookie → 401.
  • Sign in, then sign-out via DELETE /api/siwe/session; subsequent onramp call returns 401.

🤖 Generated with Claude Code

PierreBesson and others added 4 commits May 8, 2026 22:19
The full Coinbase Onramp URL — including the short-lived sessionToken
query parameter — was logged to the browser console on every donate
click. Removing the log eliminates a credential-leak path to extensions
and third-party scripts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the unauthenticated-mint gap flagged by the Coinbase Onramp
security review: any caller could previously POST to /api/onramp/session
or /api/faucet with an arbitrary address and burn through CDP quota.

Backend
- utils/siwe.ts — iron-session schema (nonce, address, chainId,
  isLoggedIn, signedInAt) with a lazy getSessionOptions() factory so
  next build doesn't trip on a missing IRON_SESSION_SECRET.
- utils/rateLimit.ts — best-effort in-memory token-bucket limiter,
  per-instance only (documented).
- app/api/siwe/nonce — GET, generates a SIWE nonce and stores it in
  the session.
- app/api/siwe/verify — POST, verifies the SIWE message + signature
  using viem/siwe with strict Host-header domain check and per-chain
  publicClient (mainnet, sepolia, baseSepolia, hardhat) so smart-wallet
  ERC-6492 signatures resolve correctly. On success it sets the
  signed-in session.
- app/api/siwe/session — GET reads the current state, DELETE signs out.
- app/api/onramp/session — now requires session.isLoggedIn; rate-limit
  5 requests / 10 min per signed-in address. Destination address is
  not forced to match (the recipient may be a campaign contract).
- app/api/faucet — requires session.isLoggedIn; enforces
  body.address === session.address (faucets are self-only); rate-limit
  is now 1 request / hour per (address, currency) so eth/usdc/eurc
  buckets are independent.

Frontend
- hooks/useSiweAuth.ts — reads /api/siwe/session on mount, exposes
  ensureSignedIn() that runs the nonce → sign → verify flow on demand
  (de-duped via an in-flight ref) and signOut().
- DonateWithFiatButton.tsx — calls ensureSignedIn() after opening the
  popup but before hitting /api/onramp/session; sends cookies via
  credentials: same-origin. Popup is closed if the user rejects the
  signature.
- TopUpModal.tsx — same flow for /api/faucet calls from the wallet
  page.

Config
- .env.example — documents IRON_SESSION_SECRET (32+ chars, generate
  with openssl rand -hex 32) as a production requirement.
- package.json — adds iron-session.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pass the signed-in wallet address as Coinbase partnerUserRef so onramp
transactions can be correlated back to the donor across sessions and
campaigns. Uses the address returned by ensureSignedIn() (the buyer),
not the donation target.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an Origin/Referer-vs-Host check to /api/onramp/session,
/api/faucet, /api/siwe/nonce, and /api/siwe/verify so simple
cross-origin POSTs (which CORS does not block from being delivered)
get rejected with 403 before any session work or upstream call.

The guard requires Origin/Referer host == Host header, which holds
for both production and Vercel preview deployments without an
explicit allowlist. Implemented as a shared utils/origin.ts helper
to keep route handlers small.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented May 8, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
chain-giving-nextjs Ready Ready Preview, Comment May 8, 2026 10:04pm

Mirrors the backend rate-limit window (1 hour per address+currency)
in the UI: a successful claim or a 429 response writes a per-(address,
currency) cooldown timestamp to localStorage. While active, the
"Get test \$\$\$" button disables and shows "Available in 12m / 1h"; a
setTimeout re-enables it when the window expires. Surviving page
reloads avoids re-tripping the limit on every modal open.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PierreBesson and others added 2 commits May 8, 2026 23:05
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Programs: GiftIcon → HeartIcon (warmer, more aligned with charitable
giving). Organizations on the home page: BuildingOffice2Icon →
UserGroupIcon (the entity is a community of people, not a building).
Header.tsx still uses BuildingOfficeIcon for Organizations — left
untouched per the scoped request.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PierreBesson and others added 2 commits May 9, 2026 00:00
- DonateWithFiatButton now renders a "Sign in" button (envelope icon)
  that opens the AppKit modal when no wallet is connected, and switches
  back to the regular Donate button once connected.
- Card donation tab gains a blue info card matching the crypto-tab
  style: "To pay with card, sign in with your email or any available
  option first." Adds gap-2 between the intro paragraph, the info
  card, and the warning card.
- Renames "Sign in with Email" → "Sign in" everywhere (ConnectButton,
  CGProgramView SignInActions) for consistency with the new flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@PierreBesson PierreBesson merged commit 198725e into main May 9, 2026
3 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