Adds rate limiting to /api/auth/login and /api/auth/register and removes
their wide-open CORS allowlist.
Rate limiting (@upstash/ratelimit + @upstash/redis):
- 5 attempts per 15-minute sliding window per IP, prefix "tcgvault:auth"
- new lib/rate-limit.js, lazy singleton, single source of truth
- reads KV_REST_API_URL / KV_REST_API_TOKEN (Vercel Upstash Marketplace
convention — auto-provisioned, no manual env-var setup needed)
- fail-closed in production if env vars are missing (better to error
one login than silently disable brute-force protection on live)
- fail-open in dev/test if env vars are missing (single console.warn)
- fail-open on Upstash backend outage (defense-in-depth — don't lock
the entire userbase out if Upstash is down)
- IP extracted from x-forwarded-for first hop, with socket fallback;
NOT req.body.email (rotates) or Authorization header (absent on
unauthenticated login)
CORS:
- Removed Access-Control-Allow-Origin: * + companion headers + OPTIONS
preflight from login.js and register.js
- These are first-party endpoints called from the same-origin SPA; the
"*" allowlist was a development convenience that shipped to prod
- verify.js is OUT OF SCOPE per architect's "cors-tighten" deferral
(see convoy plan § Architect's calls)
Other handler ordering preserved verbatim per brief: method gate first,
then rate-limit check (returns 429 with Retry-After header), then the
existing try/catch + body parsing + DB work.
Pre-merge requirements: KV_REST_API_URL + KV_REST_API_TOKEN must be set
in Vercel Production (already done — Upstash marketplace integration
auto-provisioned both, confirmed by maintainer 2026-05-23).
Convoy: fix-auth-bypass / Brief 4
Co-authored-by: Cursor <cursoragent@cursor.com>
69 lines
2.5 KiB
JavaScript
69 lines
2.5 KiB
JavaScript
import { Ratelimit } from '@upstash/ratelimit';
|
|
import { Redis } from '@upstash/redis';
|
|
|
|
// Lazy singleton. Module-load init would throw in environments without
|
|
// Upstash env vars (local dev pre-onboarding, tests that transitively
|
|
// import the auth handlers, Vercel build-time bundling). Defer construction
|
|
// until the first request actually arrives.
|
|
let cached = null;
|
|
|
|
function init() {
|
|
// Env-var names match Vercel's Upstash Marketplace integration, which
|
|
// auto-provisions KV_REST_API_URL and KV_REST_API_TOKEN. See
|
|
// https://upstash.com/docs/redis/howto/vercelintegration. Single-source-
|
|
// of-truth — do NOT alias to UPSTASH_REDIS_REST_*.
|
|
const url = process.env.KV_REST_API_URL;
|
|
const token = process.env.KV_REST_API_TOKEN;
|
|
|
|
if (url && token) {
|
|
const redis = new Redis({ url, token });
|
|
const ratelimit = new Ratelimit({
|
|
redis,
|
|
limiter: Ratelimit.slidingWindow(5, '15 m'),
|
|
prefix: 'tcgvault:auth',
|
|
});
|
|
return { mode: 'live', ratelimit };
|
|
}
|
|
|
|
if (process.env.NODE_ENV === 'production') {
|
|
// Fail-closed in production. A single failed login is a better outcome
|
|
// than silently disabling brute-force protection on the live site.
|
|
throw new Error(
|
|
'[rate-limit] Upstash not configured. Set KV_REST_API_URL and KV_REST_API_TOKEN in the deployment environment (auto-provisioned by the Vercel Upstash Marketplace integration) before serving auth traffic.'
|
|
);
|
|
}
|
|
|
|
console.warn(
|
|
'[rate-limit] KV_REST_API_URL / KV_REST_API_TOKEN not set — rate limiting disabled (dev/test only)'
|
|
);
|
|
return { mode: 'noop' };
|
|
}
|
|
|
|
function extractIdentifier(req) {
|
|
const xff = req.headers?.['x-forwarded-for'];
|
|
const firstHop = Array.isArray(xff) ? xff[0] : xff?.split(',')[0]?.trim();
|
|
return firstHop || req.socket?.remoteAddress || 'anonymous';
|
|
}
|
|
|
|
export async function checkAuthRateLimit(req) {
|
|
if (!cached) {
|
|
cached = init();
|
|
}
|
|
|
|
if (cached.mode === 'noop') {
|
|
return { allowed: true, remaining: Infinity, reset: 0 };
|
|
}
|
|
|
|
const identifier = extractIdentifier(req);
|
|
|
|
try {
|
|
const { success, remaining, reset } = await cached.ratelimit.limit(identifier);
|
|
return { allowed: success, remaining, reset };
|
|
} catch (err) {
|
|
// Fail-open on Upstash outage. A hard outage at the rate-limit backend
|
|
// should not lock the entire user base out of login. Brute-force
|
|
// protection lives behind defense-in-depth (Vercel firewall, etc.).
|
|
console.error('[rate-limit]', err);
|
|
return { allowed: true, remaining: Infinity, reset: 0 };
|
|
}
|
|
}
|