Gate onramp + faucet behind SIWE, add rate limits and origin guard#7
Merged
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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>
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>
- 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/sessionor/api/faucetwith 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
utils/siwe.ts(iron-session schema + lazygetSessionOptions()),hooks/useSiweAuth.ts(ensureSignedIn()runs nonce → sign → verify on demand, in-flight de-duped)./api/siwe/nonce,/api/siwe/verify,/api/siwe/session. Verify validates the SIWE message domain against the requestHost, supports ERC-6492 via per-chainpublicClient(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/verifyreject any browser request whoseOrigin/Refererhost doesn't matchHost(covers prod and Vercel preview hosts without an explicit allowlist).Onramp specifics
console.logthat previously leaked the short-livedsessionTokenURL to the browser console.partnerUserRef=cg-<userAddress>to Coinbase using the SIWE-verified buyer address (not the donation recipient).Faucet specifics
body.address === session.address(faucets are self-only).Frontend wiring
DonateWithFiatButtonandTopUpModalcallensureSignedIn()before hitting their backend; both send cookies viacredentials: same-origin.Config
.env.exampledocumentsIRON_SESSION_SECRET(32+ chars,openssl rand -hex 32) as a production requirement.iron-sessiondependency.Test plan
IRON_SESSION_SECRETand runyarn start; click Donate on a program — wallet sign prompt appears, popup opens with Coinbase URL containingpartnerUserRef=cg-0x….curl -X POST https://<host>/api/onramp/session -d '{...}'without an Origin header → 403.Origin: https://evil.example→ 403.DELETE /api/siwe/session; subsequent onramp call returns 401.🤖 Generated with Claude Code