mrkeyoor.com_
Sat 08 Aug 22:51 UTC
npmSecurityupdated 08 Aug 2026

otplib

otplib generates and verifies the time-based codes used by authenticator apps, plus counter-based HOTP codes for hardware tokens and similar systems. Version 13 is a TypeScript-first rewrite with functional and class APIs, built-in Base32 handling, otpauth URI generation, and crypto plugins that run across Node.js, Bun, Deno, and browsers. It supplies the cryptographic OTP primitive, not a complete two-factor login system: your application still owns enrollment, secret storage, recovery, throttling, and replay policy.

Verdict

A strong current choice for teams that want portable, typed HOTP and TOTP primitives with unusually clear drift and replay controls. Do not mistake the clean API for a finished MFA system, and budget real migration work if your code targets otplib v12 or earlier.

API stability3/5The current 13.4.1 surface is small and strongly typed, with functional and class entry points plus explicit subpath exports for both ESM and CommonJS. However, the project itself labels v13 a complete rewrite: it removed the separate authenticator package and legacy crypto adapters, changed verification to a result object, and provides v11 and v12 adapters specifically because old application code is not source-compatible.
Docs5/5The README documents installation for Node.js, Bun, Deno, browsers, and script tags, then calls out the v13 break, Base32 assumptions, the 16-byte secret guardrail, authenticator-app defaults, and the non-boolean verification result. The linked documentation site adds getting-started, runtime, security, troubleshooting, migration, danger-zone, and generated API sections, and the published type declarations include usable examples for tolerance and replay protection.
Maintenance5/5Version 13.4.1 was published on May 30, 2026, the repository was pushed on August 8, 2026, and it is neither archived nor disabled. The GitHub snapshot lists five open issues and pull requests, which is a small visible backlog for a security package with a recent ground-up major release. The monorepo also carries tests and separately versioned core, TOTP, HOTP, URI, plugin, CLI, and migration-adapter packages.
Ecosystem5/5The package recorded 3,153,495 downloads for the measured week and the repository has 2,281 stars. Its default SHA-1, six-digit, 30-second TOTP settings match the compatibility guidance for common authenticator apps, while otpauth URI output handles provisioning. Official packages cover Noble, Node, and Web Crypto implementations, Base32 codecs, a CLI, and adapters for two previous major-version APIs.

Use it if

  • You need RFC 6238 TOTP codes that work with Google Authenticator, Authy, Microsoft Authenticator, 1Password, and similar apps
  • You need both TOTP and counter-based HOTP behind one typed API, including a verification delta for drift or counter resynchronization
  • Your code must share an OTP implementation across Node.js, Bun, Deno, and browser builds
  • You want explicit clock-tolerance and replay-protection controls instead of a verifier that only returns true or false
Skip it if

Setup reality

Installation is one package and there are no peer dependencies, credentials, native extensions, or configuration files. The deceptively easy part is generating a secret and a code; production enrollment needs more decisions. Version 13 expects Base32 strings and rejects decoded secrets shorter than 16 bytes, so common ten-byte tutorial secrets now throw SecretTooShortError. Generate a fresh secret with generateSecret() instead of copying an example. Store that secret encrypted at rest and reveal it only during enrollment. generateURI() returns an otpauth URI, not a QR image, so add a QR encoder or display the URI through another trusted channel. Verification is async and returns an object with valid, and on success delta, epoch, and timeStep; treating the object itself as a boolean accepts every attempt. Clock tolerance is zero by default. If you allow drift, choose epochTolerance deliberately and prefer a past-only tuple when future tokens should not pass. Preventing reuse also takes database work: save the successful timeStep per account and supply it as afterTimeStep next time. HOTP has a separate persistent counter that must be advanced atomically after success. The v13 rewrite removed the old authenticator object and legacy crypto adapters, so existing v11 or v12 examples need the migration guide or an adapter package rather than mechanical import changes.

Patterns

Generate a new Base32 secretgenerate-secret

import { generateSecret } from 'otplib';

const secret = generateSecret();
await saveEncryptedSecret(userId, secret);

The default is 20 random bytes encoded as Base32. Encrypt the secret at rest and never log it.

Generate the current TOTP codegenerate-totp

import { generate } from 'otplib';

const token = await generate({ secret });
console.log(token);

TOTP is the default strategy. Strings are treated as Base32, not as raw passphrases.

Verify a TOTP code correctlyverify-totp

import { verify } from 'otplib';

const result = await verify({ secret, token: submittedCode });
if (!result.valid) {
  throw new Error('Invalid code');
}
console.log(result.delta, result.timeStep);

verify() returns an object, not a boolean. Always test result.valid before reading success-only fields.

Build an authenticator enrollment URIgenerate-provisioning-uri

import { generateURI } from 'otplib';

const uri = generateURI({
  issuer: 'Acme',
  label: user.email,
  secret,
});

This returns an otpauth URI, not an image. Pass it to a QR-code library and avoid exposing it after enrollment.

Allow limited past clock driftallow-clock-drift

const result = await verify({
  secret,
  token: submittedCode,
  epochTolerance: [30, 0],
});

The tuple is [pastSeconds, futureSeconds]. A single number permits a symmetric window, including future codes.

Reject an already accepted time stepprevent-token-replay

const lastTimeStep = await loadLastTimeStep(userId);
const result = await verify({
  secret,
  token: submittedCode,
  afterTimeStep: lastTimeStep ?? undefined,
  epochTolerance: [30, 0],
});

if (result.valid) {
  await saveLastTimeStep(userId, result.timeStep);
}

Persist the successful timeStep atomically with the login. afterTimeStep is a step counter, not a Unix timestamp.

Generate a counter-based HOTP codegenerate-hotp

import { generate } from 'otplib';

const token = await generate({
  strategy: 'hotp',
  secret,
  counter: 42,
});

HOTP requires an explicit counter. Do not reuse a counter value after the corresponding code is accepted.

Verify HOTP with a look-ahead windowresync-hotp-counter

const result = await verify({
  strategy: 'hotp',
  secret,
  token: submittedCode,
  counter: storedCounter,
  counterTolerance: 5,
});

if (result.valid) {
  await saveCounter(userId, storedCounter + result.delta + 1);
}

A numeric tolerance looks ahead only. Update the stored counter atomically to the matched counter plus one.

Set algorithm, digits, and periodcustomize-token-settings

const token = await generate({
  secret,
  algorithm: 'sha256',
  digits: 8,
  period: 60,
});

The authenticator app must use exactly the same settings. The broadly compatible defaults are SHA-1, six digits, and 30 seconds.

Keep shared options in an OTP instanceuse-class-api

import { OTP } from 'otplib';

const otp = new OTP({ strategy: 'totp' });
const secret = otp.generateSecret();
const token = await otp.generate({ secret });
const result = await otp.verify({ secret, token });

The functional API is recommended for simple calls; the class API is useful when an application shares strategy or plugin configuration.

Generate and verify synchronouslyuse-sync-api

import { generateSync, verifySync } from 'otplib';

const token = generateSync({ secret });
const result = verifySync({ secret, token });

Sync calls require a crypto plugin that implements synchronous HMAC. They throw if the selected plugin is async-only.

Pass raw secret bytes explicitlyuse-binary-secret

import { generate, stringToBytes } from 'otplib';

const rawSecret = stringToBytes(process.env.OTP_PASSPHRASE);
const token = await generate({ secret: rawSecret });

A string secret is decoded as Base32. Use Uint8Array when the source is raw bytes or a passphrase, and still meet the minimum secret length.

Alternatives

PackageRegistryPick it when
otpauthnpmYou want a focused HOTP and TOTP implementation with first-class Node.js, Deno, Bun, and browser support
@oslojs/otpnpmYou prefer small, explicit HOTP and TOTP primitives and are comfortable assembling the enrollment flow yourself
@epic-web/totpnpmYou only need TOTP and want an API designed around Web Crypto-compatible runtimes