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 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-13 09:28:38 +00:00
co-authored by Claude Opus 4.6
parent 9de47fd760
commit d4c51f0aac
2 changed files with 64 additions and 4 deletions
+61 -1
View File
@@ -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<typeof Hono>
let cleanup: ReturnType<typeof setInterval>
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)
})
})
+3 -3
View File
@@ -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')