redis-otp-manager
v1.6.1
Published
Lightweight, Redis-backed OTP manager for Node.js apps
Maintainers
Readme
redis-otp-manager
Lightweight, Redis-backed OTP manager for Node.js and NestJS apps.
redis-otp-manager gives you a production-focused OTP engine with Redis TTL storage, atomic Redis verification, cryptographic hardening, observability hooks, and abuse-control policies without forcing a delivery provider.
Why Use It
- Redis-backed OTP storage with TTL cleanup
- Atomic Redis generate and verify paths
- HMAC hashing with secret rotation support
- Cooldown, rate limiting, and lockout controls
- Hook-based observability with metadata context
- Works with plain Node.js and NestJS
- Supports both ESM and CommonJS consumers
Install
npm install redis-otp-managerQuick Start
import { OTPManager, RedisAdapter } from "redis-otp-manager";
import { createClient } from "redis";
const redisClient = createClient({ url: process.env.REDIS_URL });
await redisClient.connect();
const otp = new OTPManager({
store: new RedisAdapter(redisClient),
ttl: 300,
maxAttempts: 3,
devMode: false,
hashing: {
secret: process.env.OTP_HMAC_SECRET,
},
rateLimit: {
window: 60,
max: 3,
},
});
const generated = await otp.generate({
type: "email",
identifier: "[email protected]",
intent: "login",
});
await otp.verify({
type: "email",
identifier: "[email protected]",
intent: "login",
otp: "123456",
});Feature Overview
Core Flows
- OTP generation and verification
- Verification token generation and verification
- Intent-aware keying
- In-memory adapter for tests
- NestJS module export via
redis-otp-manager/nest
Security
- Keyed HMAC support through
hashing.secret - Secret rotation with
previousSecrets - Legacy SHA-256 verification support for migrations
- Atomic Redis verification to prevent double-success races
Abuse Controls
- Cooldown policy support
- Fixed-window and Redis sliding-window rate limiting
- Scoped throttling by identifier, intent, channel, or intent + channel
- Temporary lock windows after repeated failed verification attempts
- Optional replay detection for already-used token verification links
Observability
- Lifecycle hooks for generated, verified, failed, locked, rate-limited, and cooldown-blocked events
- Additive
credentialKindin hook payloads so OTP and token flows are distinguishable - Request-scoped metadata passed to hook payloads
- Non-blocking hook error handling by default
Examples
- Plain Node: examples/node-basic/README.md
- NestJS: examples/nest-basic/README.md
- Express: examples/express-basic/README.md
- Verification Links: examples/node-token-link/README.md
- NestJS Verification Links: examples/nest-token-link/README.md
- Adapter Usage: examples/node-adapters/README.md
Configuration
OTPManager options
type OTPManagerOptions = {
store: StoreAdapter;
ttl: number;
maxAttempts: number;
otpLength?: number;
devMode?: boolean;
resendCooldown?: number;
cooldown?: {
seconds: number;
scope?: "identifier" | "intent" | "channel" | "intent_channel";
};
rateLimit?: {
window: number;
max: number;
scope?: "identifier" | "intent" | "channel" | "intent_channel";
algorithm?: "fixed_window" | "sliding_window";
};
lockout?: {
seconds: number;
afterAttempts: number;
appliesTo?: "verify" | "generate" | "both";
scope?: "identifier" | "intent" | "channel" | "intent_channel";
};
hashing?: {
secret?: string;
previousSecrets?: string[];
allowLegacyVerify?: boolean;
};
replayProtection?: {
enabled: boolean;
ttl: number;
scope?: "identifier" | "intent" | "channel" | "intent_channel";
};
};Backward compatibility notes
resendCooldownstill works and remains supported.cooldownis the richer replacement for policy-based cooldown configuration.- legacy SHA-256 verification is still supported by default when migrating to HMAC.
- CommonJS and ESM consumers are both supported.
Config precedence
cooldowntakes precedence over legacyresendCooldownwhen both are provided.rateLimit.scopedefaults tochannel.rateLimit.algorithmdefaults tofixed_window.lockout.appliesTodefaults toboth.lockout.scopedefaults tointent_channel.hooks.throwOnErrordefaults to non-blocking behavior unless explicitly set totrue.replayProtectionis opt-in and only affects token verification flow.
Stable API
v1.0.0 is the first stable production release of redis-otp-manager.
The following public surface is considered stable:
OTPManagerRedisAdapterMemoryAdapter- exported error classes
- exported hook and config types
redis-otp-manager/nest
Full stability notes: docs/stability.md
Error Reference
The package can throw these errors:
OTPRateLimitExceededErrorOTPExpiredErrorOTPInvalidErrorVerificationSecretExpiredErrorVerificationSecretInvalidErrorVerificationSecretAlreadyUsedErrorOTPMaxAttemptsExceededErrorOTPResendCooldownErrorOTPLockedError
Detailed behavior reference: docs/errors.md
Production Recommendations
- keep
devMode: falsein production - use
hashing.secretin production - prefer
RedisAdapterin production - keep Redis private and never publicly exposed
- use HTTPS for all OTP or token delivery flows
- keep
maxAttemptslow, typically3 - enable cooldown and rate limiting for public endpoints
- use lockout windows for abuse-heavy flows
More guidance: docs/production.md
Migration Guides
Version-to-version migration notes live in docs/migrations.md.
Reliability And Compatibility
Current package validation includes:
- real Redis integration tests against a live Redis server
- Redis-backed CI validation for TTL, lockout, cooldown, and sliding-window behavior
- ESM and CommonJS smoke checks against the built package output
- Nest integration smoke coverage in CI
Tested Scenarios
The package is currently validated for:
- in-memory unit and behavior tests
- fake Redis atomic behavior tests
- real Redis integration tests in CI
- NestJS provider smoke coverage
- ESM and CommonJS package smoke checks
Feedback And Support
If you run into a bug, unexpected behavior, or have a feature request, open an issue here:
- Bug reports: https://github.com/prakashu51/otp-generator/issues
- Feature requests: https://github.com/prakashu51/otp-generator/issues
When reporting, please include:
- package version
- Node.js version
- adapter used (
RedisAdapterorMemoryAdapter) - minimal reproduction steps
- expected behavior and actual behavior
Adapters
Use auditAdapter and deliveryAdapter when you want provider-agnostic integration points without changing the existing OTP or token core flow.
const otp = new OTPManager({
store: new RedisAdapter(redisClient),
ttl: 300,
maxAttempts: 3,
deliveryAdapter: {
async send(payload) {
console.log("send", payload);
},
},
auditAdapter: {
async record(event) {
console.log("audit", event);
},
},
});
await otp.generateAndSend({
type: "email",
identifier: "[email protected]",
intent: "login",
});
await otp.generateTokenAndSend(
{
type: "email",
identifier: "[email protected]",
intent: "verify-email",
},
{
baseUrl: "https://app.example.com/verify-email",
},
);Token Flow
Use generateToken() and verifyToken() when you want email verification links or magic-link style flows without changing the existing OTP API.
Token verification returns token-friendly generic secret errors such as VerificationSecretExpiredError and VerificationSecretInvalidError, while the OTP flow keeps the original OTP error classes.
If you enable replayProtection, successful token verification stores a short-lived used marker so repeated verification can return VerificationSecretAlreadyUsedError instead of the generic expired/nonexistent response.
const generated = await otp.generateToken({
type: "email",
identifier: "[email protected]",
intent: "verify-email",
});
await otp.verifyToken({
type: "email",
identifier: "[email protected]",
intent: "verify-email",
token: generated.token ?? "",
});
const replayAwareOtp = new OTPManager({
store: new RedisAdapter(redisClient),
ttl: 1800,
maxAttempts: 3,
replayProtection: {
enabled: true,
ttl: 3600,
scope: "intent_channel",
},
});Post-1.0.0 Roadmap
- Framework-specific callback and redirect examples
- Provider-specific companion packages and adapters
- Richer token and verification-link workflow helpers
