From c2eb7513834eeb5adfa53fff897f585de87e4821 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Tue, 30 Dec 2025 22:08:47 +0900 Subject: feat(security): add rate limiting and CORS middleware MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add rate limiting to login endpoint (5 requests/minute per IP) - Configure CORS middleware with environment-based origin control - Expose rate limit headers in CORS for client visibility - Update hono to 4.11.3 for rate limiter peer dependency 🤖 Generated with [Claude Code](https://claude.ai/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/server/routes/auth.test.ts | 50 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) (limited to 'src/server/routes/auth.test.ts') diff --git a/src/server/routes/auth.test.ts b/src/server/routes/auth.test.ts index 5bf9f86..c3b0158 100644 --- a/src/server/routes/auth.test.ts +++ b/src/server/routes/auth.test.ts @@ -62,6 +62,56 @@ describe("POST /login", () => { app.route("/api/auth", auth); }); + it("returns rate limit headers on login request", async () => { + vi.mocked(mockUserRepo.findByUsername).mockResolvedValue(undefined); + + const res = await app.request("/api/auth/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Forwarded-For": "192.168.1.1", + }, + body: JSON.stringify({ + username: "testuser", + password: "somepassword", + }), + }); + + expect(res.headers.get("RateLimit-Limit")).toBe("5"); + expect(res.headers.get("RateLimit-Remaining")).toBeDefined(); + }); + + it("returns 429 after exceeding rate limit", async () => { + vi.mocked(mockUserRepo.findByUsername).mockResolvedValue(undefined); + + const makeRequest = () => + app.request("/api/auth/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Forwarded-For": "10.0.0.1", + }, + body: JSON.stringify({ + username: "testuser", + password: "wrongpassword", + }), + }); + + // Make 5 requests (the limit) + for (let i = 0; i < 5; i++) { + const res = await makeRequest(); + expect(res.status).toBe(401); + } + + // 6th request should be rate limited + const rateLimitedRes = await makeRequest(); + expect(rateLimitedRes.status).toBe(429); + const body = (await rateLimitedRes.json()) as { + error?: { code: string; message: string }; + }; + expect(body.error?.code).toBe("RATE_LIMIT_EXCEEDED"); + }); + it("returns access token for valid credentials", async () => { vi.mocked(mockUserRepo.findByUsername).mockResolvedValue({ id: "user-uuid-123", -- cgit v1.2.3-70-g09d2