Nest Authbeta

Rate Limiting

Throttling auth endpoints against credential stuffing and abuse.

Nest Auth ships built-in rate limiting, soft account lockout, and a CAPTCHA hook for its sensitive endpoints. They're opt-in (default off, no behavior change until enabled) — except the admin console, which is brute-force-protected by default (see Admin Console → Security). You can also bring @nestjs/throttler alongside or instead (below).

Built-in rate limiting (security.rateLimit)

Turn it on and the library throttles its own login / signup / forgot-password / passwordless-send / verify-otp / mfa-verify / admin-login routes — no extra controllers, and it runs ahead of NestAuthAuthGuard so bots are rejected before any auth work:

NestAuthModule.forRoot({
  security: {
    rateLimit: {
      enabled: true,        // opt-in
      keyBy: 'both',        // 'ip' | 'identifier' | 'both' (default 'both')
      // Per-bucket overrides (merged over the built-in defaults):
      buckets: {
        login: { windowMs: 60_000, max: 5 },
        signup: { windowMs: 60_000, max: 5 },
        adminLogin: { windowMs: 60_000, max: 5 },
        adminReset: { windowMs: 15 * 60_000, max: 5 }, // admin secret-key endpoints
      },
      // store: myRedisStore,  // IRateLimitStore; defaults to in-memory (per instance)
    },
  },
});

Exceeding a bucket returns 429 with a Retry-After header and code: 'RATE_LIMITED'. In-memory counters are per-process — supply a shared store for multi-instance deployments.

Built-in soft lockout (security.lockout)

Complements rate limiting: after N failed logins for an identifier+IP, further attempts are refused for a cooldown (a longer, softer window than the per-minute rate limit). It's event-driven (counts LOGIN_FAILED, clears on LOGGED_IN) and now covers admin login too:

NestAuthModule.forRoot({
  security: {
    lockout: {
      enabled: true,
      maxFailedAttempts: 10,   // per identifier+IP
      window: '15m',           // counting window
      lockDuration: '15m',     // cooldown once tripped
    },
  },
});

A locked identifier gets 429 + code: 'ACCOUNT_LOCKED'. Keyed by identifier and IP so an attacker can't lock a victim out from another IP.

Alternative: @nestjs/throttler

You can still bring your own edge throttler (e.g. to cap total request volume before auth logic runs), on its own or in addition to the built-ins:

pnpm add @nestjs/throttler
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
import { APP_GUARD } from '@nestjs/core';
 
@Module({
  imports: [
    ThrottlerModule.forRoot([{
      ttl: 60_000,        // 1 minute window
      limit: 100,         // 100 requests/min default
    }]),
  ],
  providers: [
    { provide: APP_GUARD, useClass: ThrottlerGuard },
  ],
})
export class AppModule {}

The throttler guard runs ahead of NestAuthAuthGuard, so unauthenticated bots are rejected before any auth work happens.

Per-endpoint thresholds

Override the default for high-risk endpoints with @Throttle():

import { Throttle } from '@nestjs/throttler';
 
// On a custom controller that wraps auth endpoints:
@Throttle({ default: { ttl: 60_000, limit: 5 } })
@Post('auth/login')
async login(@Body() dto) { … }

Recommended starting points:

EndpointWindowLimit
/auth/login60s5 per IP
/auth/signup60s3 per IP
/auth/passwordless/send60s3 per identifier
/auth/forgot-password60s3 per identifier
/auth/mfa/challenge60s5 per session
/auth/refresh-token60s60 per session — high, since auto-refresh happens often
/auth/reset-password60s5 per IP

Tune from there using your own auth-failure dashboard.

Per-identifier vs per-IP

Per-IP alone is insufficient — an attacker behind a CDN looks like one IP. Per-identifier (email/phone) protects the targeted user. The strongest setup limits both:

  • Per-IP: blocks distributed brute force from one source.
  • Per-identifier: blocks credential stuffing against one account.

@nestjs/throttler v6+ supports custom trackers. Use a key that combines both:

import { ThrottlerGuard } from '@nestjs/throttler';
 
@Injectable()
export class AuthThrottlerGuard extends ThrottlerGuard {
  protected async getTracker(req: Request): Promise<string> {
    return `${req.ip}:${req.body?.credentials?.email ?? ''}`;
  }
}

Soft vs hard limits

For login specifically:

  • Hard limit — return 429 after N attempts. Protects the system.
  • Soft limit — silently delay the response after N attempts. Slows attackers without revealing the threshold.

A common combination: instant lockout after 5 failures within 15 minutes, with a captcha challenge on the 4th attempt.

Storing counters

In-memory works for a single instance. For multi-instance, use the Redis storage adapter for @nestjs/throttler so the counters are shared.

Audit + telemetry

Wire your throttler's "limit exceeded" event into your audit hook so you have a record of every block. Spikes are a leading indicator of an in-progress attack.

On this page