# Rate limiting enforced on authentication endpoints

- **Pattern:** `ab-002408` (`security-hardening.auth-session.auth-rate-limiting`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002408
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002408)

## Why it matters

Authentication endpoints without rate limiting are open to brute-force and credential stuffing attacks at internet speed. CWE-799 (Improper Control of Interaction Frequency) and OWASP A07 (Identification & Authentication Failures) cover this class of vulnerability. NIST 800-53 AC-7 requires automated lockout mechanisms on authentication functions. Password-reset endpoints are particularly dangerous: an unrate-limited `/api/forgot-password` lets an attacker spam reset emails to exhaust users and potentially enumerate valid accounts. Unlike account-level lockout, IP-level rate limiting also stops automated registration abuse and prevents enumeration via timing attacks.

## Severity rationale

High because unprotected auth endpoints allow automated credential attacks at machine speed, with no throttle on how quickly an attacker can test millions of password candidates.

## Remediation

Apply rate limits to every auth endpoint — login, register, forgot-password, reset-password, and MFA verify. Using Upstash for edge-compatible sliding-window limits:

```ts
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'

const loginLimiter = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(5, '15 m'),
})

export async function POST(req: Request) {
  const ip = req.headers.get('x-forwarded-for') ?? '127.0.0.1'
  const { success, reset } = await loginLimiter.limit(ip)
  if (!success) {
    return Response.json({ error: 'Too many attempts' }, {
      status: 429,
      headers: { 'Retry-After': String(Math.ceil((reset - Date.now()) / 1000)) },
    })
  }
}
```

Protect `/api/auth/forgot-password` with a tighter limit (3 per 15 minutes) than login (10 per minute).

## Detection

- **ID:** `auth-rate-limiting`
- **Severity:** `high`
- **What to look for:** Enumerate every authentication endpoint (login, register, password reset, token refresh). For each, check `/api/auth/*`, `/api/login`, `/api/register`, `/api/forgot-password`, `/api/reset-password`, and `/api/verify-email` endpoints (and any custom equivalents) for rate limiting. Look for `@upstash/ratelimit`, `express-rate-limit`, `rate-limiter-flexible`, or equivalent middleware. Verify limits are per IP or per identifier (not just global).
- **Pass criteria:** Login, registration, password reset, and 2FA verification endpoints all have rate limits configured. Limits are restrictive enough to meaningfully slow brute force — no more than 10 requests per minute per IP on login, and no more than 5 requests per minute on password reset. Report: "X auth endpoints found, all Y have rate limiting configured."
- **Fail criteria:** Auth endpoints have no rate limiting. Or only one of the auth endpoints is protected while others are left open.
- **Skip (N/A) when:** All auth is handled by a managed provider (Clerk, Auth0, Supabase Auth) with built-in rate limiting that you have confirmed is enabled.
- **Detail on fail:** Name the unprotected endpoints. Example: `"POST /api/auth/login has no rate limiting — unlimited credential stuffing attempts possible"` or `"Rate limiting applied to login but not to /api/auth/forgot-password — password reset endpoint spammable"`
- **Remediation:** Apply rate limiting to all auth endpoints. Using Upstash for edge-compatible rate limiting:

  ```ts
  // lib/rate-limit.ts
  import { Ratelimit } from '@upstash/ratelimit'
  import { Redis } from '@upstash/redis'

  export const authLimiter = new Ratelimit({
    redis: Redis.fromEnv(),
    limiter: Ratelimit.slidingWindow(5, '15 m'),  // 5 per 15 minutes
    analytics: true,
  })

  // In your login route handler:
  export async function POST(req: Request) {
    const ip = req.headers.get('x-forwarded-for') ?? '127.0.0.1'
    const { success, reset } = await authLimiter.limit(ip)

    if (!success) {
      return Response.json(
        { error: 'Too many attempts. Try again later.' },
        {
          status: 429,
          headers: { 'Retry-After': String(Math.ceil((reset - Date.now()) / 1000)) },
        }
      )
    }
    // ... login logic
  }
  ```

## External references

- cwe CWE-799
- owasp A07
- nist AC-7

Taxons: access-control

HTML version: https://auditbuffet.com/patterns/ab-002408
