fix(api): skip rate limiting for non-sensitive auth endpoints
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m50s

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.
This commit is contained in:
lila 2026-04-23 22:12:38 +02:00
parent c57fc5a98b
commit 59049002fc
2 changed files with 39 additions and 9 deletions

View file

@ -5,7 +5,6 @@ import { authLimiter, gameLimiter, lobbyLimiter } from "./rateLimiters.js";
import type { Session, User } from "better-auth"; import type { Session, User } from "better-auth";
// Minimal app to test the limiter in isolation
function createTestApp() { function createTestApp() {
const app = express(); const app = express();
app.set("trust proxy", 1); app.set("trust proxy", 1);
@ -20,29 +19,51 @@ describe("authLimiter", () => {
let app: ReturnType<typeof createTestApp>; let app: ReturnType<typeof createTestApp>;
beforeEach(() => { beforeEach(() => {
// Fresh app = fresh in-memory store = counters reset between tests
app = createTestApp(); 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"); const res = await request(app).post("/api/auth/sign-in");
expect(res.status).toBe(200); 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; const limit = 20;
for (let i = 0; i < limit; i++) { for (let i = 0; i < limit; i++) {
await request(app).post("/api/auth/sign-in"); await request(app).post("/api/auth/sign-in");
} }
const res = 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.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"); const res = await request(app).post("/api/auth/sign-in");
expect(res.headers).toHaveProperty("ratelimit"); expect(res.headers).toHaveProperty("ratelimit");
}); });

View file

@ -13,6 +13,15 @@ export const authLimiter = rateLimit({
limit: 20, limit: 20,
standardHeaders: "draft-8", standardHeaders: "draft-8",
legacyHeaders: false, legacyHeaders: false,
skip: (req) => {
const path = req.path;
return (
path.includes("/get-session") ||
path.includes("/sign-out") ||
path.startsWith("/callback/") ||
path.includes("/callback/")
);
},
message: { message: {
success: false, success: false,
error: "Too many requests, please try again later.", error: "Too many requests, please try again later.",