Skip to main content
Rate Limiting Every Auth Endpoint: A Security Deep Dive

Rate Limiting Every Auth Endpoint: A Security Deep Dive

Andrius LukminasAndrius LukminasFebruary 1, 20267 min read23 views

Authentication endpoints are the front door to every web application. They're also the first target for automated attacks — credential stuffing, brute force, and account enumeration. We decided to add rate limiting to every single auth route in Boottify. All 23 of them.

WHY EVERY ROUTE MATTERS

Most guides only rate-limit /sign-in and /sign-up. But attackers are smarter than that:

  • /forgot-password — Can be used for email enumeration and email bombing
  • /verify-2fa — Brute-forceable if TOTP codes aren't rate-limited (only 999,999 possibilities)
  • /oauth/[provider] — OAuth state generation can be abused for session fixation recon
  • /reset-password — Token brute-forcing if tokens are short
  • /enable-2fa — Can be called repeatedly to generate TOTP secrets

We identified 23 auth-related API routes that needed protection, each with different risk profiles.

THE 5-TIER SYSTEM

Not all endpoints need the same limits. A login attempt is more sensitive than a session heartbeat. We designed five tiers:

export const rateLimiters = {
  // Tier 1: Strictest — login, password reset
  strict:    { windowMs: 15 * 60 * 1000, max: 5  },

  // Tier 2: Auth actions — 2FA setup, OAuth
  auth:      { windowMs: 15 * 60 * 1000, max: 10 },

  // Tier 3: Standard — sign-up, profile updates
  standard:  { windowMs: 15 * 60 * 1000, max: 20 },

  // Tier 4: Relaxed — read-heavy, low risk
  relaxed:   { windowMs: 15 * 60 * 1000, max: 50 },

  // Tier 5: Permissive — heartbeats, public reads
  permissive:{ windowMs: 15 * 60 * 1000, max: 100 },
};

Tier Assignment

TierRoutesMax Requests / 15min
Strictsign-in, forgot-password, reset-password, verify-2fa5
Authenable-2fa, disable-2fa, setup-2fa, OAuth callbacks10
Standardsign-up, sign-out, backup-codes20
Relaxedme, providers, 2fa-methods50
Permissiveheartbeat, clear-session100

IN-MEMORY SLIDING WINDOW

We chose an in-memory sliding window algorithm over Redis-based solutions. For a single-server deployment like Boottify, this avoids adding infrastructure while still being accurate.

import { rateLimit } from "@/lib/rate-limit";

export async function POST(req: Request) {
  const limited = await rateLimit(req, rateLimiters.strict);
  if (limited) return limited; // Returns 429 response

  // ... handle sign-in logic
}

The rate limiter returns a Response object with proper headers (Retry-After, X-RateLimit-Remaining) or null if the request is allowed.

TESTING WITH VITEST

We wrote unit tests that verify each tier's behavior — request counting, window expiry, and header correctness:

describe("rate-limit strict tier", () => {
  it("allows 5 requests then blocks", async () => {
    for (let i = 0; i < 5; i++) {
      expect(await rateLimit(mockReq, rateLimiters.strict)).toBeNull();
    }
    const blocked = await rateLimit(mockReq, rateLimiters.strict);
    expect(blocked?.status).toBe(429);
  });
});

RESULTS

Since deploying rate limiting across all 23 auth routes:

  • 0 successful brute-force attempts (previously saw probing traffic daily)
  • Zero false positives from legitimate users (tiers are generous enough for normal usage)
  • <1ms overhead per request (in-memory Map lookup)
  • 23 routes protected with just 2 lines added to each handler

Rate limiting isn't glamorous, but it's one of the highest-impact security improvements you can make. If you're only protecting login, you're leaving the door open.

Related Articles

Comments

0/5000 characters

Comments from guests require moderation.