npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

otp-validator-totp

v1.0.2

Published

A zero-dependency, production-ready TOTP (Time-Based One-Time Password) generator and validator using Node.js native crypto.

Downloads

353

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-totp

Quick Start

import { generateOTP, validateOTP } from 'otp-validator-totp';

const SECRET = process.env.OTP_SECRET!;
const TTL    = 300; // 5 minutes

// --- Generate ---
const otp = generateOTP('[email protected]', 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: '[email protected]',
  userProvidedOtp: otp,
  secretKey: SECRET,
  ttlSeconds: TTL,
});
console.log(isValid); // true (first use)

// Same OTP again → blocked (replay attack)
const isReplay = await validateOTP({
  userId: '[email protected]',
  userProvidedOtp: otp,
  secretKey: SECRET,
  ttlSeconds: TTL,
});
console.log(isReplay); // false

API 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 | ❌ | Anti-replay store. Defaults to built-in MemoryStore. Pass false to disable. |

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: '[email protected]',
  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: '[email protected]',
  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: '[email protected]',
  userProvidedOtp: otp,
  secretKey: SECRET,
  ttlSeconds: 300,
  store: false, // Math-only validation, no replay check
});

How It Works

  1. Time BlockMath.floor(Date.now() / 1000 / ttlSeconds) divides time into fixed-size windows.
  2. HMAC Key — The secretKey and userId are concatenated to form a per-user HMAC key.
  3. HMAC-SHA256 — The time block is encoded as an 8-byte Big Endian buffer and hashed.
  4. 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.
  5. Validation — The validator regenerates OTPs for blocks [current−1, current, current+1] and compares using crypto.timingSafeEqual.
  6. 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-exports

Security Notes

  • Never expose secretKey to 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