npm.io
1.1.0 • Published yesterday

express-secure-limiter

Licence
MIT
Version
1.1.0
Deps
1
Size
221 kB
Vulns
0
Weekly
0

express-secure-limiter

npm version npm downloads license bundle size

A production-ready, highly configurable, storage-agnostic rate-limiting library for Node.js and Express, written in TypeScript.

It supports multiple algorithms (fixed-window, sliding-window, and token-bucket), tiered pricing/plan structures, brute-force protection (login lockout), email-based One-Time Password (OTP) 2FA, and works with both in-memory and Redis stores.


Features

  • Zero-dependency by default — In-memory rate limiting with active/passive memory cleanup.
  • Atomic Redis Adapter — Fully atomic, concurrent-safe rate limiting using Redis Lua scripts (compatible with both redis and ioredis).
  • Multi-Algorithm Support — Switch between fixed-window, sliding-window (default), and token-bucket using config.
  • Brute-Force Protection — Pre-built SignInLimiter with custom failed attempts tracking and customizable account lockouts.
  • Tiered Request Limiting — Pre-built RequestLimiter supporting dynamic limit resolution based on request context (e.g. user plans, API keys).
  • Email-Based OTP 2FA — Pre-built OtpService using nodemailer to trigger and verify single-use 6-digit OTP codes via Gmail SMTP.
  • Express Middleware — Out-of-the-box support for Express, providing standard headers (X-RateLimit-*, Retry-After) and whitelist skipping.

Installation

npm install express-secure-limiter

(Optional) If you want to use the Redis store, ensure you have ioredis or redis installed:

npm install ioredis
# OR
npm install redis

Quick Start

1. API Request Limiting (Tier-Based)

Configure general API rate limiting using the sliding-window algorithm, with limits resolved dynamically depending on the user's plan.

import express from 'express';
import { createRequestLimiter } from 'express-secure-limiter';

const app = express();

const apiLimiter = createRequestLimiter({
  algorithm: 'sliding-window', // 'sliding-window' | 'fixed-window' | 'token-bucket'
  windowMs: 60000,             // 1 minute window
  max: (req) => {
    // Dynamic rate limits based on user plan
    const plan = req.headers['x-user-plan'] || 'free';
    if (plan === 'enterprise') return 1000;
    if (plan === 'pro') return 100;
    return 10; // Free tier
  }
});

// Apply as middleware
app.get('/api/data', apiLimiter.middleware({
  keyGenerator: (req) => req.headers['x-api-key'] || req.ip
}), (req, res) => {
  res.json({ data: "Sensitive API response" });
});
2. Sign-in Protection (Brute-Force & Lockout)

Track failed attempts and temporarily lock out clients after consecutive login failures.

import express from 'express';
import { createSignInLimiter } from 'express-secure-limiter';

const app = express();
app.use(express.json());

const signInLimiter = createSignInLimiter({
  maxAttempts: 5,        // Allow 5 failed attempts
  windowMs: 900000,      // Track attempts within a 15-minute window
  lockoutMs: 1800000,    // Lock out client for 30 minutes on limit breach
});

app.post('/login', async (req, res) => {
  const { email, password } = req.body;
  const key = email || req.ip;

  // 1. Check if they are locked out
  const status = await signInLimiter.isLocked(key);
  if (status.locked) {
    return res.status(429).json({
      error: "Too many login attempts. Account temporarily locked.",
      retryAfterSeconds: Math.ceil(status.resetMs / 1000)
    });
  }

  // 2. Perform authentication logic
  const isAuthSuccessful = authService.verify(email, password);

  if (isAuthSuccessful) {
    // Reset attempts on successful sign-in
    await signInLimiter.reset(key);
    return res.json({ token: "JWT_TOKEN" });
  }

  // 3. Record failed attempt if validation fails
  const attempt = await signInLimiter.recordFailedAttempt(key);
  if (!attempt.allowed) {
    return res.status(429).json({
      error: "Brute force protection triggered. Locked out for 30 minutes.",
      retryAfterSeconds: Math.ceil(attempt.resetMs / 1000)
    });
  }

  return res.status(401).json({
    error: "Invalid email or password.",
    remainingAttempts: attempt.remainingAttempts
  });
});
3. Email-Based OTP 2FA (via Gmail)

You can protect critical flows like login with an email-based One-Time Password (OTP) sent using Gmail SMTP.

Flow Sequence
  1. Password Authentication: Client sends login request to /login with credentials.
  2. OTP Step Trigger: If credentials are valid, the server generates a 6-digit OTP code, stores its SHA-256 hash in MemoryStore, emails the code to the user, and responds with { otpRequired: true, email }.
  3. Verification: Client submits the code to /login/verify-otp. If verification succeeds, the server issues a session token.
Usage Example
import { MemoryStore, OtpService } from 'express-secure-limiter';

const store = new MemoryStore();
const otpService = new OtpService({
  store,
  gmailUser: process.env.GMAIL_USER,
  gmailAppPassword: process.env.GMAIL_APP_PASSWORD,
  otpExpiryMs: 300000,   // 5 minutes expiry
  maxAttempts: 5,        // Max 5 verification attempts per code
  lockoutAttempts: 5,    // Lock out IP/Email after 5 consecutive failures
});

// Send OTP
await otpService.sendOtp('user@example.com');

// Verify OTP
const result = await otpService.verifyOtp('user@example.com', '123456');
if (result.success) {
  // Successful verification
} else {
  // Handle result.error ('RateLimitExceeded', 'InvalidOtp', 'ExpiredOtp', etc.)
}
Gmail App Password Setup
  1. Enable 2-Step Verification on your Gmail account.
  2. Visit Google App Passwords.
  3. Generate a new App Password (e.g. for "Mail" / "Other").
  4. Copy the generated 16-character code and set it as GMAIL_APP_PASSWORD in your environment.
  5. Set GMAIL_USER to your Gmail address.

Gmail SMTP has a daily sending cap (roughly 500/day on free accounts). This is sufficient for development, testing, and low-volume staging. For production-scale workloads, we strongly recommend swapping Gmail for a transactional email provider like AWS SES, SendGrid, Resend, or Mailgun.

4. Redis Store Integration

Integrate seamlessly with Redis to manage state across multiple instances/servers.

import Redis from 'ioredis';
import { RateLimiter, RedisStore } from 'express-secure-limiter';

const redisClient = new Redis("redis://localhost:6379");

const limiter = new RateLimiter({
  windowMs: 60000,
  max: 100,
  store: new RedisStore(redisClient, { prefix: 'api-limit:' }),
});

Configuration Reference

RateLimiterOptions (Core Configuration)
Parameter Type Default Description
windowMs number Required Time duration of the rate limit window in milliseconds.
max number | (key) => number Required Maximum number of allowed requests in the window.
algorithm 'fixed-window' | 'sliding-window' | 'token-bucket' 'sliding-window' Rate limiting algorithm to use.
cost number | (key) => number 1 Weight of each request.
store Store MemoryStore Storage adapter to use for saving rate limit counters.
maxTokens number | (key) => number max Capacity of the token bucket (Only for token-bucket).
refillRate number | (key) => number maxTokens / windowMs Refill rate in tokens per millisecond (Only for token-bucket).
OtpServiceOptions
Parameter Type Default Description
store Store Required The MemoryStore instance to use for temporary OTP state storage.
transporter any undefined Custom Nodemailer transporter (useful for mocking/testing).
gmailUser string process.env.GMAIL_USER Gmail username for SMTP connection.
gmailAppPassword string process.env.GMAIL_APP_PASSWORD Gmail App Password for SMTP connection.
otpExpiryMs number 300000 Expiration time of a generated code in milliseconds (5 mins).
maxAttempts number 5 Maximum verification attempts per single code before invalidating.
lockoutAttempts number 5 Lockout threshold for consecutive wrong OTP verifications.
MiddlewareOptions (Express Factory)
Parameter Type Default Description
keyGenerator (req) => string req.ip Resolves a unique key from the incoming Express request.
skip (req) => boolean () => false Skip rate-limiting check entirely if this returns true.
onLimitExceeded (req, res, next, info) => void Sends 429 response Custom callback handler when rate-limiting is breached.
statusCode number 429 HTTP status code for default exceeded response.
message any 'Too Many Requests' JSON response body for default exceeded response.

License

MIT

Keywords