diff --git a/stellar-payment-platform/server.js b/stellar-payment-platform/server.js index a3a6eec..00a96bb 100644 --- a/stellar-payment-platform/server.js +++ b/stellar-payment-platform/server.js @@ -926,8 +926,24 @@ app.get('/users', validateSchema({ query: usersQuerySchema }), async (req, res, // Mount v1 router for both legacy paths and explicit API versioning app.use('/', v1Router); app.use('/api/v1', v1Router); +// #492 — Strict rate limiter for auth/login endpoints. These are prime +// brute-force targets, so they get a much tighter budget than the global +// limiter. Uses the same Redis-backed store so the limit is shared across +// all distributed nodes. +const authLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 20, + store: redisClient ? new RedisStore({ + sendCommand: (...args) => redisClient.sendCommand(args), + }) : undefined, + standardHeaders: true, + legacyHeaders: true, + message: errorBody('RATE_LIMITED', 'Too many requests, please try again later.'), + keyGenerator: (req) => req.ip || (req.connection && req.connection.remoteAddress) || '', +}); + // Auth endpoints (email OTP verification) - uses Redis when available -app.use('/auth', require('./src/routes/v1/authRoutes')(redisClient)); +app.use('/auth', authLimiter, require('./src/routes/v1/authRoutes')(redisClient)); // #497 — Expose RSA public key as a JWKS document so external services can // verify RS256-signed tokens without sharing a secret. diff --git a/stellar-payment-platform/tests/rate-limit.test.js b/stellar-payment-platform/tests/rate-limit.test.js index f3984f2..d981af6 100644 --- a/stellar-payment-platform/tests/rate-limit.test.js +++ b/stellar-payment-platform/tests/rate-limit.test.js @@ -250,6 +250,33 @@ describe('Rate Limiting — express-rate-limit', () => { }); }); + // ── Strict auth/login rate limit ───────────────────────────────────────── + + describe('strict auth rate limit', () => { + it('applies a stricter limit on /auth endpoints than the global limiter', async () => { + // The auth limiter allows only 20 requests per window, so the 21st + // request should be rejected even though the global limit is 100. + for (let i = 0; i < 20; i++) { + await request(app) + .post('/auth/verify-email') + .send({ email: `user${i}@example.com` }); + } + + const res = await request(app) + .post('/auth/verify-email') + .send({ email: 'overflow@example.com' }); + + expect(res.status).toBe(429); + expect(res.body).toEqual({ + success: false, + error: { + code: 'RATE_LIMITED', + message: 'Too many requests, please try again later.', + }, + }); + }); + }); + // ── Reset between test modules ─────────────────────────────────────────── describe('rate limiter resets with new app instance', () => {