From d4c51f0aac59ebcc6be2e144dc830181e6aaceb0 Mon Sep 17 00:00:00 2001 From: Dorian Date: Fri, 13 Mar 2026 09:28:38 +0000 Subject: [PATCH] fix: tighten auth rate limits to 10/min and add rate limit tests Reduce login and nostr/session rate limits from 30 to 10 requests per minute per IP to prevent brute-force attacks. Add tests verifying 429 response after exceeding the limit. Co-Authored-By: Claude Opus 4.6 --- server/src/routes/auth.test.ts | 62 +++++++++++++++++++++++++++++++++- server/src/routes/auth.ts | 6 ++-- 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/server/src/routes/auth.test.ts b/server/src/routes/auth.test.ts index 398dbd8..57c931f 100644 --- a/server/src/routes/auth.test.ts +++ b/server/src/routes/auth.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, vi } from 'vitest' +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { Hono } from 'hono' import { authRouter } from './auth.js' import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools' @@ -157,3 +157,63 @@ describe('auth routes', () => { expect(res.status).toBe(401) }) }) + +describe('auth rate limiting', () => { + let prodApp: InstanceType + let cleanup: ReturnType + + beforeEach(async () => { + vi.resetModules() + process.env.NODE_ENV = 'production' + process.env.JWT_SECRET = 'test-secret-for-rate-limit-testing' + const rateLimitMod = await import('../middleware/rate-limit.js') + cleanup = rateLimitMod.cleanupInterval + const authMod = await import('./auth.js') + prodApp = new Hono() + prodApp.route('/api/auth', authMod.authRouter) + }) + + afterEach(() => { + process.env.NODE_ENV = 'test' + delete process.env.JWT_SECRET + clearInterval(cleanup) + }) + + it('login: returns 429 after exceeding 10 requests per minute', async () => { + // Send 10 requests (within limit) + for (let i = 0; i < 10; i++) { + const res = await prodApp.request('/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ pubkey: '0'.repeat(64) }), + }) + expect(res.status).not.toBe(429) + } + + // 11th request should be rate limited + const res = await prodApp.request('/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ pubkey: '0'.repeat(64) }), + }) + expect(res.status).toBe(429) + const body = await res.json() as { error: string; retryAfterSec: number } + expect(body.error).toContain('Too many requests') + expect(body.retryAfterSec).toBeGreaterThan(0) + }) + + it('nostr/session: returns 429 after exceeding 10 requests per minute', async () => { + // Send 10 requests (within limit) + for (let i = 0; i < 10; i++) { + await prodApp.request('/api/auth/nostr/session', { + method: 'POST', + }) + } + + // 11th request should be rate limited + const res = await prodApp.request('/api/auth/nostr/session', { + method: 'POST', + }) + expect(res.status).toBe(429) + }) +}) diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index e6a0e08..69e9c4c 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -25,8 +25,8 @@ authRouter.get("/check-name/:name", async (c) => { return c.json({ available: existing.length === 0 }) }) -// Login with Nostr pubkey (rate limited: 30 per minute per IP) -authRouter.post('/login', rateLimit(60_000, 30), async (c) => { +// Login with Nostr pubkey (rate limited: 10 per minute per IP) +authRouter.post('/login', rateLimit(60_000, 10), async (c) => { const parsed = loginSchema.safeParse(await c.req.json().catch(() => ({}))) if (!parsed.success) { return c.json({ error: 'Invalid pubkey.' }, 400) @@ -365,7 +365,7 @@ import { verifyNip98Token } from '../middleware/nip98.js' import { createJwt, extractPubkeyFromAuth } from '../middleware/jwt.js' // POST /nostr/session — authenticate with NIP-98, receive JWT -authRouter.post('/nostr/session', rateLimit(60_000, 30), async (c) => { +authRouter.post('/nostr/session', rateLimit(60_000, 10), async (c) => { // Extract NIP-98 token from headers (try multiple header names) const authHeader = c.req.header('Authorization') || c.req.header('nostr-authorization')