A modular, policy-driven, in-process rate limiter for abuse prevention in sensitive auth flows
npm install @ramblip/auth-action-rate-limiterA modular, policy-driven, in-process rate limiter for Node.js, designed for abuse prevention in sensitive authentication flows like password reset, registration, and OTP verification.


This library provides rate limiting within a single Node.js process. Rate limits are NOT shared across multiple instances, containers, or servers.
For distributed rate limiting across multiple instances, you need an external shared store like Redis. This library is intentionally designed for:
- Single-instance deployments
- Development and testing environments
- Prototyping and early-stage projects
- Scenarios where a gateway/WAF handles distributed rate limiting
- Why Rate Limit Auth Flows?
- Features
- Installation
- Quickstart
- Configuration
- Default Policies
- API Reference
- Security Considerations
- Fail-Open vs Fail-Closed
- Troubleshooting
- Limitations
- Examples
- Contributing
- License
Authentication endpoints are prime targets for abuse:
| Attack | Target | Impact |
|--------|--------|--------|
| Credential stuffing | Login | Account takeover |
| Brute force | Login, OTP verify | Account compromise |
| Account enumeration | Password reset, Registration | User discovery |
| SMS/Email bombing | OTP send | Cost, user annoyance |
| Registration spam | Registration | Database pollution |
Rate limiting these endpoints reduces attack surface and protects both users and infrastructure.
- ๐ชฃ Token Bucket Algorithm - Allows bursts while maintaining average rate limits
- ๐ Policy-Driven - Configure rules per action with JSON/objects
- ๐ AND Semantics - Multiple rules per action, all must pass
- ๐ Security-First - HMAC hashing for identifiers, enumeration-safe patterns
- โก Challenge Mode - Step-up authentication (captcha) instead of hard blocks
- ๐ Observability - Structured logging (pino), Prometheus metrics support
- ๐งช Well Tested - Comprehensive unit tests with deterministic time control
- ๐ฆ Zero External Dependencies - No Redis, no external services required
``bash`
npm install @ramblip/auth-action-rate-limiter
Peer dependencies:
`bash`
npm install express # If using Express middleware
`typescript
import express from 'express';
import {
createRateLimiter,
MemoryStore,
defaultPolicies,
} from '@ramblip/auth-action-rate-limiter';
const app = express();
app.use(express.json());
// Create the rate limiter
const store = new MemoryStore();
const rateLimiter = createRateLimiter({
store,
policies: defaultPolicies,
hashSecret: process.env.HASH_SECRET || 'change-me-in-production',
});
// Apply to password reset endpoint
app.post('/auth/password-reset/request',
rateLimiter.forAction({
action: 'password_reset_request',
getEmail: (req) => req.body.email,
}),
(req, res) => {
// IMPORTANT: Always return same response to prevent enumeration
res.json({
message: 'If an account exists, you will receive a reset email.',
});
}
);
// Apply to registration endpoint
app.post('/auth/register',
rateLimiter.forAction({
action: 'register',
getEmail: (req) => req.body.email,
}),
(req, res) => {
// Check if challenge is required
if (req.rateLimitDecision?.outcome === 'CHALLENGE') {
return res.status(428).json({
error: 'CHALLENGE_REQUIRED',
challenge: 'captcha_required',
});
}
// ... registration logic
}
);
// Clean shutdown
process.on('SIGTERM', async () => {
await rateLimiter.shutdown();
process.exit(0);
});
app.listen(3000);
`
`typescript`
interface ActionPolicy {
id: string; // Action identifier
rules: RateLimitRule[]; // Array of rules (AND semantics)
failMode: 'open' | 'closed'; // Behavior on store errors
}
`typescript`
interface RateLimitRule {
name: string; // Rule identifier
key: KeyDimension[]; // Dimensions for rate limit key
capacity: number; // Burst capacity (max tokens)
refillTokens: number; // Tokens added per interval
refillIntervalMs: number; // Refill interval in ms
cost?: number; // Tokens per request (default: 1)
mode?: 'block' | 'challenge'; // What to do when exceeded
ttlMs?: number; // TTL for stored state
}
| Dimension | Description |
|-----------|-------------|
| ip | Client IP address |emailHash
| | HMAC-hashed email |phoneHash
| | HMAC-hashed phone |userId
| | Authenticated user ID |sessionId
| | Session identifier |action
| | Action name |route
| | Request path |
`typescript
import { ActionPolicy, TIME } from '@ramblip/auth-action-rate-limiter';
const customPasswordResetPolicy: ActionPolicy = {
id: 'password_reset_request',
rules: [
{
name: 'per_ip',
key: ['ip'],
capacity: 10, // 10 requests burst
refillTokens: 10, // Refill all 10
refillIntervalMs: TIME.MINUTE, // Every minute
mode: 'block',
},
{
name: 'per_email',
key: ['emailHash'],
capacity: 3, // 3 per email
refillTokens: 3,
refillIntervalMs: TIME.MINUTE * 15,
mode: 'block',
},
],
failMode: 'closed', // Security > availability
};
`
The library includes sensible defaults for common auth endpoints:
Creates the rate limiter middleware factory.
`typescript`
const rateLimiter = createRateLimiter({
store: RateLimitStore, // Required: store instance
policies: Record
hashSecret?: string, // Secret for HMAC hashing
logger?: Logger, // Pino-compatible logger
onDecision?: (decision) => void, // Decision callback
enableMetrics?: boolean, // Enable Prometheus metrics
});
Creates middleware for a specific action.
`typescript`
app.post('/endpoint',
rateLimiter.forAction({
action: 'action_name', // Must match policy ID
getEmail?: (req) => string, // Extract email from request
getPhone?: (req) => string, // Extract phone from request
getSessionId?: (req) => string,// Extract session ID
getUserId?: (req) => string, // Extract user ID
skip?: (req) => boolean, // Skip rate limiting
}),
handler
);
In-process rate limit store.
`typescript
const store = new MemoryStore({
sweepIntervalMs?: number, // Cleanup interval (default: 60000)
highWaterMark?: number, // Max entries (default: 100000)
evictionCount?: number, // Entries to evict (default: 10000)
});
// Get stats
const stats = store.getStats();
// { size, highWaterMark, utilizationPercent }
// Clean shutdown
await store.shutdown();
`
`typescript
import { hashIdentifier, hashEmail, hashPhone } from '@ramblip/auth-action-rate-limiter';
// Hash any identifier
const hash = hashIdentifier('user@example.com', secret);
// Specialized hashers
const emailHash = hashEmail('user@example.com', secret);
const phoneHash = hashPhone('+1-234-567-8900', secret);
`
For password reset, always return the same response regardless of whether the email exists:
`typescript`
app.post('/auth/password-reset/request', rateLimiter, (req, res) => {
const user = await findUserByEmail(req.body.email);
if (user) {
await sendPasswordResetEmail(user);
}
// Log for internal monitoring, but don't expose to user
// ALWAYS same response
res.json({
message: 'If an account exists with this email, you will receive a reset link.',
});
});
Never store raw emails or phone numbers in rate limit keys:
`typescriptrate:${email}
// โ Bad - leaks PII if store is compromised
const key = ;
// โ
Good - uses HMAC hash
const key = rate:${hashEmail(email, secret)};`
The library handles this automatically when you use getEmail and getPhone extractors with a hashSecret.
If behind a reverse proxy, ensure proper IP extraction:
`typescript
// Trust first IP in X-Forwarded-For
app.set('trust proxy', 1);
// Or configure custom extraction
rateLimiter.forAction({
action: 'login',
// Custom IP extraction if needed
});
`
| Mode | Behavior | Use When |
|------|----------|----------|
| fail-closed | Block on errors | Security critical (password reset, OTP) |
| fail-open | Allow on errors | Availability critical (registration) |
Check store utilization:
`typescriptStore: ${stats.size}/${stats.highWaterMark} (${stats.utilizationPercent}%)
const stats = store.getStats();
console.log();`
Tune the high water mark:
`typescript`
const store = new MemoryStore({
highWaterMark: 50_000, // Reduce max entries
evictionCount: 5_000, // Evict more per trigger
sweepIntervalMs: 30_000, // Sweep more frequently
});
1. Check action ID matches policy:
`typescript`
rateLimiter.forAction({ action: 'password_reset_request' }) // Must match policy
2. Verify dimensions are available:
`typescript`
// If rule uses emailHash, email must be provided
rateLimiter.forAction({
action: 'password_reset_request',
getEmail: (req) => req.body.email, // Required!
});
3. Check IP extraction (behind proxy):
`typescript`
app.set('trust proxy', true);
Adjust policy configuration:
`typescript`
const policies = customizePolicies({
password_reset_request: {
rules: [
{
name: 'per_ip',
key: ['ip'],
capacity: 20, // More lenient
refillTokens: 20,
refillIntervalMs: TIME.MINUTE,
},
],
},
});
1. In-Process Only: Not suitable for multi-instance deployments without shared storage
2. Memory Bound: All state is in memory; restart clears limits
3. No Persistence: Rate limits don't survive process restarts
4. Single Process: Cannot coordinate limits across cluster workers
Consider:
- API Gateway rate limiting (AWS API Gateway, Kong, etc.)
- WAF rules (CloudFlare, AWS WAF)
- Redis-backed rate limiter (not included in this library)
- Distributed rate limiting service
See the examples/express-demo directory for a complete working example with:
- Password reset (enumeration-safe)
- User registration with challenge mode
- Login with rate limiting
- OTP send/verify flow
Run the demo:
`bash`
npm run dev
Test rate limiting:
`bash`Password reset (will hit limit after 5 requests)
for i in {1..10}; do
curl -X POST http://localhost:3000/auth/password-reset/request \
-H "Content-Type: application/json" \
-d '{"email": "test@example.com"}'
echo
done
1. Fork the repository
2. Create your feature branch (git checkout -b feature/amazing-feature)npm test
3. Run tests ()npm run lint
4. Run linter ()git commit -m 'Add amazing feature'
5. Commit your changes ()git push origin feature/amazing-feature`)
6. Push to the branch (
7. Open a Pull Request
MIT License - see LICENSE for details.
---
Note: This library is designed for in-process rate limiting. For production deployments with multiple instances, implement distributed rate limiting using Redis or similar, or rely on infrastructure-level rate limiting (API Gateway, WAF, etc.).