From 59049002fc670c08d3f88acc2c6dec8822252a2e Mon Sep 17 00:00:00 2001 From: lila Date: Thu, 23 Apr 2026 22:12:38 +0200 Subject: [PATCH] fix(api): skip rate limiting for non-sensitive auth endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The authLimiter was blocking legitimate users because Better Auth's client polls /get-session frequently (on mount, route changes, focus), and /sign-out was also getting blocked after repeated session polls. Skip rate limiting for: - /get-session — read-only, requires valid cookie, no attack surface - /sign-out — no attack value in blocking logout - /callback/* — OAuth callbacks from providers Brute force protection remains on /sign-in, /sign-up, and other sensitive endpoints. --- apps/api/src/middleware/rateLimiters.test.ts | 39 +++++++++++++++----- apps/api/src/middleware/rateLimiters.ts | 9 +++++ 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/apps/api/src/middleware/rateLimiters.test.ts b/apps/api/src/middleware/rateLimiters.test.ts index 29a219f..3bcccbb 100644 --- a/apps/api/src/middleware/rateLimiters.test.ts +++ b/apps/api/src/middleware/rateLimiters.test.ts @@ -5,7 +5,6 @@ import { authLimiter, gameLimiter, lobbyLimiter } from "./rateLimiters.js"; import type { Session, User } from "better-auth"; -// Minimal app to test the limiter in isolation function createTestApp() { const app = express(); app.set("trust proxy", 1); @@ -20,29 +19,51 @@ describe("authLimiter", () => { let app: ReturnType; beforeEach(() => { - // Fresh app = fresh in-memory store = counters reset between tests app = createTestApp(); }); - it("allows requests under the limit through", async () => { + it("allows requests under the limit through on sensitive endpoints", async () => { const res = await request(app).post("/api/auth/sign-in"); expect(res.status).toBe(200); }); - it("returns 429 after exceeding the limit", async () => { + it("returns 429 after exceeding the limit on sensitive endpoints", async () => { const limit = 20; for (let i = 0; i < limit; i++) { await request(app).post("/api/auth/sign-in"); } const res = await request(app).post("/api/auth/sign-in"); expect(res.status).toBe(429); - expect(res.body).toEqual({ - success: false, - error: "Too many requests, please try again later.", - }); }); - it("sets RateLimit headers on responses", async () => { + it("does not rate limit /get-session", async () => { + const limit = 20; + for (let i = 0; i < limit + 5; i++) { + await request(app).get("/api/auth/get-session"); + } + const res = await request(app).get("/api/auth/get-session"); + expect(res.status).toBe(200); + }); + + it("does not rate limit /sign-out", async () => { + const limit = 20; + for (let i = 0; i < limit + 5; i++) { + await request(app).post("/api/auth/sign-out"); + } + const res = await request(app).post("/api/auth/sign-out"); + expect(res.status).toBe(200); + }); + + it("does not rate limit OAuth callbacks", async () => { + const limit = 20; + for (let i = 0; i < limit + 5; i++) { + await request(app).get("/api/auth/callback/google"); + } + const res = await request(app).get("/api/auth/callback/google"); + expect(res.status).toBe(200); + }); + + it("sets RateLimit headers on sensitive responses", async () => { const res = await request(app).post("/api/auth/sign-in"); expect(res.headers).toHaveProperty("ratelimit"); }); diff --git a/apps/api/src/middleware/rateLimiters.ts b/apps/api/src/middleware/rateLimiters.ts index 479be03..2f2eaf6 100644 --- a/apps/api/src/middleware/rateLimiters.ts +++ b/apps/api/src/middleware/rateLimiters.ts @@ -13,6 +13,15 @@ export const authLimiter = rateLimit({ limit: 20, standardHeaders: "draft-8", legacyHeaders: false, + skip: (req) => { + const path = req.path; + return ( + path.includes("/get-session") || + path.includes("/sign-out") || + path.startsWith("/callback/") || + path.includes("/callback/") + ); + }, message: { success: false, error: "Too many requests, please try again later.",