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:
co-authored by
Claude Opus 4.6
parent
9de47fd760
commit
d4c51f0aac
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user