Package Exports
- otp-validator-totp
- otp-validator-totp/dist/index.js
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (otp-validator-totp) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
otp-validator-totp
A zero-dependency, production-ready npm package for generating and validating Time-Based One-Time Passwords (TOTP) with built-in replay attack protection.
Built entirely on Node.js native crypto module β no otplib, no speakeasy, no external libraries.
Features
- π HMAC-SHA256 hashing with RFC 4226 Dynamic Truncation
- β±οΈ Time-based β stateless core, no database required
- π‘οΈ Anti-replay protection via pluggable storage adapters (IoC pattern)
- π Timing-safe comparison via
crypto.timingSafeEqual - π Β±1 time-window drift tolerance for clock skew / network latency
- π¦ Zero runtime dependencies
- π· Written in TypeScript with full type declarations
Installation
npm install otp-validator-totpQuick Start
import { generateOTP, validateOTP } from 'otp-validator-totp';
const SECRET = process.env.OTP_SECRET!;
const TTL = 300; // 5 minutes
// --- Generate ---
const otp = generateOTP('user@example.com', TTL, SECRET);
console.log(`Your OTP is: ${otp}`); // e.g. "482913"
// Send this to the user via SMS, email, etc.
// --- Validate (with built-in anti-replay) ---
const isValid = await validateOTP({
userId: 'user@example.com',
userProvidedOtp: otp,
secretKey: SECRET,
ttlSeconds: TTL,
});
console.log(isValid); // true (first use)
// Same OTP again β blocked (replay attack)
const isReplay = await validateOTP({
userId: 'user@example.com',
userProvidedOtp: otp,
secretKey: SECRET,
ttlSeconds: TTL,
});
console.log(isReplay); // falseAPI Reference
generateOTP(userId, ttlSeconds, secretKey)
Generates a 6-digit TOTP. Synchronous.
| Parameter | Type | Description |
|---|---|---|
userId |
string |
Unique identifier for the user |
ttlSeconds |
number |
Validity window of the OTP in seconds |
secretKey |
string |
Backend's private secret used for hashing |
Returns: string β A 6-digit OTP, zero-padded.
validateOTP(options)
Validates a user-provided OTP with optional anti-replay protection. Asynchronous.
| Option | Type | Required | Description |
|---|---|---|---|
userId |
string |
β | Unique identifier for the user |
userProvidedOtp |
string |
β | The 6-digit OTP from the client |
secretKey |
string |
β | Backend's private secret |
ttlSeconds |
number |
β | Validity window in seconds |
store |
`IStore | false` | β |
Returns: Promise<boolean> β true if valid and not replayed, false otherwise.
Anti-Replay Protection
By default, validateOTP uses a built-in in-memory store to block replay attacks β the same OTP cannot be used twice. This is perfect for single-process apps.
For multi-server deployments, you can plug in any storage backend by implementing the IStore interface.
The IStore Interface
import type { IStore } from 'otp-validator-totp';
interface IStore {
checkAndStore(
userId: string,
timeBlock: number,
ttlSeconds: number,
): boolean | Promise<boolean>;
}- Return
trueβ first use (valid) - Return
falseβ already consumed (replay attack)
Custom Adapter: Redis
import { createClient } from 'redis';
import type { IStore } from 'otp-validator-totp';
import { validateOTP } from 'otp-validator-totp';
const redis = createClient();
await redis.connect();
class RedisStore implements IStore {
async checkAndStore(
userId: string,
timeBlock: number,
ttlSeconds: number,
): Promise<boolean> {
const key = `otp:${userId}:${timeBlock}`;
// SET with NX (only if not exists) + automatic expiry
const result = await redis.set(key, '1', {
NX: true,
EX: ttlSeconds * 3, // 3Γ TTL covers Β±1 drift window
});
return result === 'OK'; // null means key already existed β replay
}
}
// Use it:
const isValid = await validateOTP({
userId: 'user@example.com',
userProvidedOtp: '482913',
secretKey: process.env.OTP_SECRET!,
ttlSeconds: 300,
store: new RedisStore(),
});Custom Adapter: Prisma / PostgreSQL
import { PrismaClient } from '@prisma/client';
import type { IStore } from 'otp-validator-totp';
import { validateOTP } from 'otp-validator-totp';
const prisma = new PrismaClient();
class PrismaStore implements IStore {
async checkAndStore(
userId: string,
timeBlock: number,
ttlSeconds: number,
): Promise<boolean> {
try {
// Unique constraint on (userId, timeBlock) prevents duplicates
await prisma.usedOTP.create({
data: {
userId,
timeBlock,
expiresAt: new Date(Date.now() + ttlSeconds * 3 * 1000),
},
});
return true; // Insert succeeded β first use
} catch (error: any) {
if (error.code === 'P2002') {
return false; // Unique constraint violation β replay
}
throw error; // Re-throw unexpected errors
}
}
}
// Prisma schema addition:
// model UsedOTP {
// id Int @id @default(autoincrement())
// userId String
// timeBlock Int
// expiresAt DateTime
// @@unique([userId, timeBlock])
// }
const isValid = await validateOTP({
userId: 'user@example.com',
userProvidedOtp: '482913',
secretKey: process.env.OTP_SECRET!,
ttlSeconds: 300,
store: new PrismaStore(),
});Disabling Replay Protection
For testing or stateless scenarios, pass store: false:
const isValid = await validateOTP({
userId: 'user@example.com',
userProvidedOtp: otp,
secretKey: SECRET,
ttlSeconds: 300,
store: false, // Math-only validation, no replay check
});How It Works
- Time Block β
Math.floor(Date.now() / 1000 / ttlSeconds)divides time into fixed-size windows. - HMAC Key β The
secretKeyanduserIdare concatenated to form a per-user HMAC key. - HMAC-SHA256 β The time block is encoded as an 8-byte Big Endian buffer and hashed.
- Dynamic Truncation (RFC 4226) β A 4-byte slice is extracted from the hash, masked to 31 bits, then reduced modulo 10βΆ to produce a 6-digit code.
- Validation β The validator regenerates OTPs for blocks
[currentβ1, current, current+1]and compares usingcrypto.timingSafeEqual. - Anti-Replay β On a successful math match, the store marks the
(userId, timeBlock)pair as consumed, blocking reuse.
Project Structure
src/
βββ core.ts # Shared helpers: validation, time blocks, HMAC truncation
βββ generator.ts # generateOTP (synchronous)
βββ validator.ts # validateOTP (asynchronous, with store integration)
βββ store.ts # IStore interface + MemoryStore implementation
βββ index.ts # Public API re-exportsSecurity Notes
- Never expose
secretKeyto the client. OTP generation and validation should happen server-side only. - The Β±1 window drift means an OTP is valid for up to 3Γ the TTL in the worst case. Choose your TTL accordingly.
- All comparisons use constant-time equality to mitigate timing side-channel attacks.
- Always enable anti-replay in production to prevent OTP reuse.
License
MIT