@amine_boutouil/bouncer
v1.0.0
Published
Production-grade security middleware for Express.js - audit logging, RBAC, OTP/2FA, and rate limiting
Downloads
100
Maintainers
Readme
@amine_boutouil/bouncer
🛡️ Production-grade security middleware for Express.js — audit logging, RBAC, OTP/2FA, and rate limiting.
A reusable, hardened security middleware package designed for high-security and compliance-sensitive Express.js environments.
Features
| Module | Capabilities | | ----------------- | -------------------------------------------------------------------------------------------------------- | | Audit Logging | SHA256 hash-chained logs, HMAC signing, tamper detection, pluggable storage, log rotation | | RBAC Engine | JWT verification (HS256/RS256), role hierarchy (GOD > ADMIN > MODERATOR > USER), permission-based access | | OTP / 2FA | TOTP generation & verification, QR codes, recovery codes, replay attack protection | | Rate Limiting | Configurable rate limits, anti-bruteforce guards, request fingerprinting | | Core | Zod config validation, centralized error handling, structured Winston logging, full TypeScript types |
Installation
npm install @amine_boutouil/bouncerPeer Dependencies
npm install expressQuick Start
import express from "express";
import {
createAuditMiddleware,
createRBACMiddleware,
createRateLimitMiddleware,
createBruteforceGuard,
create2FAManager,
bouncerErrorHandler,
MemoryStorageAdapter,
} from "@amine_boutouil/bouncer";
const app = express();
app.use(express.json());
// ── Rate Limiting (global) ──────────────────────────────
const rateLimit = createRateLimitMiddleware({
points: 100,
duration: 60,
});
app.use(rateLimit);
// ── Audit Logging ───────────────────────────────────────
const audit = createAuditMiddleware({
storage: new MemoryStorageAdapter(), // Use FileStorageAdapter in production
hmacEnabled: true,
hmacSecret: process.env.HMAC_SECRET!, // min 32 chars
});
app.use(audit);
// ── RBAC ────────────────────────────────────────────────
const rbac = createRBACMiddleware({
secret: process.env.JWT_SECRET!,
algorithm: "HS256",
issuer: "my-app",
audience: "my-api",
});
// ── Protected Route ─────────────────────────────────────
app.get("/admin", rbac.authenticate, rbac.requireRole("ADMIN"), (req, res) => {
res.json({ message: `Hello ${req.user!.userId}` });
});
// ── Error Handler (must be last) ────────────────────────
app.use(bouncerErrorHandler);
app.listen(3000);Modules
1. Audit Logging
Intercepts state-changing HTTP methods (POST, PUT, PATCH, DELETE) and produces structured, hash-chained audit logs.
import {
createAuditMiddleware,
verifyAuditIntegrity,
FileStorageAdapter,
} from "@amine_boutouil/bouncer";
// File-based storage with rotation
const storage = new FileStorageAdapter({
filePath: "./logs/audit.log",
maxFileSize: 10 * 1024 * 1024, // 10MB
maxFiles: 5,
});
const audit = createAuditMiddleware({
storage,
hmacEnabled: true,
hmacSecret: process.env.HMAC_SECRET!,
sensitiveFields: ["password", "token", "secret", "creditCard", "otp"],
methods: ["POST", "PUT", "PATCH", "DELETE"],
});
app.use(audit);
// Verify log integrity
const result = await verifyAuditIntegrity(
"./logs/audit.log",
process.env.HMAC_SECRET,
);
if (!result.valid) {
console.error(
`Tamper detected at entry ${result.brokenAt}: ${result.reason}`,
);
}Audit entry structure:
{
"id": "uuid-v4",
"timestamp": "2026-01-01T00:00:00.000Z",
"userId": "user-123",
"role": "ADMIN",
"ip": "192.168.1.1",
"method": "POST",
"route": "/api/users",
"body": { "username": "john", "password": "[REDACTED]" },
"statusCode": 201,
"previousHash": "sha256-of-previous-entry",
"hash": "sha256-of-this-entry",
"hmac": "hmac-signature"
}Custom storage adapter:
import { AuditStorageAdapter, AuditEntry } from "@amine_boutouil/bouncer";
class DatabaseAdapter implements AuditStorageAdapter {
async append(entry: AuditEntry): Promise<void> {
/* INSERT into DB */
}
async readAll(): Promise<AuditEntry[]> {
/* SELECT * */
}
async getLastEntry(): Promise<AuditEntry | null> {
/* SELECT ... ORDER BY ... LIMIT 1 */
}
async close(): Promise<void> {
/* cleanup */
}
}2. RBAC Engine
JWT-based authentication and role/permission-based authorization.
import { createRBACMiddleware, DefaultRole } from "@amine_boutouil/bouncer";
const rbac = createRBACMiddleware({
secret: process.env.JWT_SECRET!,
algorithm: "HS256", // or 'RS256' with public key
audience: "my-api",
issuer: "my-auth-server",
// Custom hierarchy (optional)
roleHierarchy: {
GOD: 100,
ADMIN: 75,
MODERATOR: 50,
USER: 25,
},
});
// Authenticate (verifies JWT, sets req.user)
app.use("/api", rbac.authenticate);
// Require minimum role
app.get("/admin", rbac.requireRole("ADMIN"), handler);
// GOD mode only
app.delete("/system/reset", rbac.requireGodMode(), handler);
// Permission-based
app.post("/articles", rbac.requirePermission("articles:write"), handler);Expected JWT payload:
{
"userId": "user-123",
"role": "ADMIN",
"permissions": ["read", "write", "delete"],
"aud": "my-api",
"iss": "my-auth-server",
"exp": 1735689600
}3. OTP / 2FA
Full TOTP-based two-factor authentication with recovery codes.
import { create2FAManager, TwoFactorUser } from "@amine_boutouil/bouncer";
const twoFactor = create2FAManager({
issuer: "MyApp",
step: 30,
window: 1,
recoveryCodeCount: 10,
recoveryCodeLength: 16,
});
// Enable 2FA for a user
const result = await twoFactor.enable2FA(user);
// result.secret — store in DB
// result.qrCodeDataURL — show to user
// result.recoveryCodes — show ONCE, user must save
// Verify TOTP token (with replay protection)
const isValid = twoFactor.verify2FA(user, "123456");
// Use recovery code (one-time)
const recovered = twoFactor.verifyAndConsumeRecoveryCode(
user,
"ABCD-1234-EFGH-5678",
);
// Disable 2FA
twoFactor.disable2FA(user);Security features:
- Replay attack protection (hashes last successful OTP)
- Recovery codes are SHA256 hashed before storage
- Cryptographically secure random generation via Node
crypto - Configurable time window tolerance
4. Rate Limiting
import {
createRateLimitMiddleware,
createBruteforceGuard,
} from "@amine_boutouil/bouncer";
// Global rate limit
app.use(
createRateLimitMiddleware({
points: 100, // requests
duration: 60, // per 60 seconds
blockDuration: 0,
message: "Rate limit exceeded",
}),
);
// Anti-bruteforce for login
app.post(
"/login",
createBruteforceGuard({
maxAttempts: 5,
blockDuration: 900, // 15 min block
freeRetries: 2,
}),
async (req, res) => {
const success = await authenticate(req.body);
if (success) {
await req.bruteforceReset?.(); // Clear counter on success
res.json({ token: "..." });
} else {
res.status(401).json({ error: "Invalid credentials" });
}
},
);Response headers set automatically:
X-RateLimit-LimitX-RateLimit-RemainingX-RateLimit-ResetRetry-After(when blocked)
5. Utilities
import {
sha256,
hmacSha256,
secureRandomHex,
secureRandomAlphanumeric,
timingSafeCompare,
generateId,
generateRequestFingerprint,
} from "@amine_boutouil/bouncer";
// Request fingerprinting (IP + UA + headers)
const fingerprint = generateRequestFingerprint(req);
// Timing-safe comparison (prevents timing attacks)
const match = timingSafeCompare(inputHash, storedHash);
// Secure random generation
const apiKey = secureRandomHex(32); // 64-char hex string
const code = secureRandomAlphanumeric(16);6. Error Handling
All Bouncer errors follow a consistent pattern:
import { bouncerErrorHandler } from "@amine_boutouil/bouncer";
// Mount as the LAST middleware
app.use(bouncerErrorHandler);| Error | Status | Code |
| --------------------- | ------ | -------------------------- |
| AuthenticationError | 401 | AUTHENTICATION_REQUIRED |
| AuthorizationError | 403 | INSUFFICIENT_PERMISSIONS |
| RateLimitError | 429 | RATE_LIMIT_EXCEEDED |
| OTPError | 400 | INVALID_OTP |
| ConfigurationError | 500 | CONFIGURATION_ERROR |
| IntegrityError | 500 | INTEGRITY_VIOLATION |
Non-Bouncer errors return a generic 500 response without leaking internal details.
Architecture
src/
├── index.ts # Public API barrel export
├── error-handler.ts # Centralized Express error handler
├── core/
│ ├── config.ts # Zod-validated configuration schemas
│ ├── errors.ts # Error class hierarchy
│ └── logger.ts # Winston structured logger
├── audit/
│ ├── middleware.ts # createAuditMiddleware()
│ ├── sanitizer.ts # Recursive sensitive field stripping
│ ├── hash-chain.ts # SHA256 chain + HMAC
│ ├── integrity.ts # verifyAuditIntegrity()
│ └── storage/
│ ├── file-adapter.ts # Append-only file storage with rotation
│ └── memory-adapter.ts # In-memory adapter (testing)
├── rbac/
│ ├── jwt.ts # JWT verification (HS256/RS256)
│ ├── middleware.ts # authenticate, requireRole, requireGodMode, requirePermission
│ └── hierarchy.ts # Role hierarchy engine
├── otp/
│ ├── totp.ts # TOTP generation & verification
│ ├── qrcode.ts # QR code generation
│ ├── recovery.ts # Recovery code generation & verification
│ └── two-factor.ts # High-level 2FA manager
├── rate-limit/
│ ├── middleware.ts # Rate limiting middleware
│ └── bruteforce.ts # Anti-bruteforce guard
├── utils/
│ ├── crypto.ts # SHA256, HMAC, secure random, timing-safe compare
│ └── fingerprint.ts # Request fingerprinting
└── types/
└── index.ts # All TypeScript type definitionsSecurity Notes
- No hardcoded secrets. All secrets must be provided via configuration or environment variables.
- No insecure defaults. HMAC requires 32+ character secrets. JWT expiration is strictly enforced.
- Timing-safe comparisons used for all secret/hash comparisons to prevent timing attacks.
- Sensitive data stripping is recursive and case-insensitive.
- Recovery codes are hashed (SHA256) before storage — plaintext is shown once.
- OTP replay protection prevents reuse of the same TOTP token within its time window.
- Error responses never leak internal details for non-Bouncer errors.
- Append-only audit logs with hash chain integrity verification and optional HMAC signing.
- OWASP-aligned security decisions throughout.
Configuration Validation
All module configurations are validated using Zod schemas at initialization time. Invalid configurations throw ConfigurationError immediately rather than failing silently at runtime.
Development
# Install dependencies
npm install
# Type check
npx tsc --noEmit
# Run tests
npm test
# Build
npm run build
# Lint
npm run lintLicense
MIT © Amine Boutouil
