mrkeyoor.com_
Wed 23 Sept 00:34 UTC
npmSecurityupdated 22 Sept 2026

otplib review

otplib implements counter-based HOTP and time-based TOTP for authenticator apps and hardware-token workflows. Version 13 is a TypeScript rewrite with functional and class APIs, Base32 codecs, otpauth URI creation, and crypto plugins for Node, Bun, Deno, and browsers. Version 13.5.0 makes hash selection strict: unsupported algorithm strings now throw AlgorithmUnsupportedError across every crypto plugin instead of behaving differently or silently falling back. The library generates and checks codes. Your application still owns enrollment state, encrypted secret storage, attempt limits, recovery, and replay prevention. Our package test found bundled types and working require() and ESM imports.

Verdict

otplib 13.5.0 installed with 0 audit findings and made an 11.9 KB gzipped browser bundle in our sandbox; its strict algorithm checks and replay parameters are good foundations for an application-owned MFA flow. Do not install it expecting secret storage, throttling, recovery codes, or a drop-in v12 upgrade.

We installed it

Lab card: what happened when we installed otplibScreenshot of otplib documentation
Install✓ · 3.7s9 packages on disk · 3 MB
ImportESM import works · require() works · ESM package with exports map
Browser11.9 KBgzipped (35.8 KB minified), bundled with esbuild
TypesTypeScript types bundled
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does otplib install cleanly?

Yes. In a fresh container with an empty cache, npm install otplib finished in 4 seconds, leaving 9 packages and 3 MB on disk. npm audit reported no known vulnerabilities.

How much does otplib add to a browser bundle?

11.9 KB gzipped (35.8 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.

Does otplib work with both ESM and CommonJS?

Yes. Both import 'otplib' and require('otplib') worked in Node 22 in our run. The package is published as ESM with an exports map.

Does otplib include TypeScript types?

Yes, type declarations ship inside the package, so no @types install is needed.

otplib or otpauth: which should you use?

otpauth: Use it for a focused HOTP and TOTP implementation with portable runtime support. otplib 13.5.0 installed with 0 audit findings and made an 11.9 KB gzipped browser bundle in our sandbox; its strict algorithm checks and replay parameters are good foundations for an application-owned MFA flow.

When should you not use otplib?

A v11 or v12 codebase cannot budget a migration; v13 removed the separate authenticator object and changed imports, plugins, secrets, and verify results

API stability3/5Version 13.5.0 has a compact typed API split into root, class, and functional exports, and both module systems map to declarations. The current design is internally consistent, yet version 13 was a full rewrite that removed the old authenticator package and legacy crypto adapters and changed verify() into a result object. Even 13.5.0 tightens invalid algorithm behavior in a bugfix release, so security fixtures should cover every enrolled configuration before upgrades.
Docs5/5The documentation covers Node, Bun, Deno, browsers, CDN use, functional and class calls, HOTP, the 16-byte secret floor, Base32 input, 6-digit and 30-second compatibility defaults, URI provisioning, sync-plugin limits, drift, replay controls, security considerations, migration adapters, and troubleshooting. The 13.5.0 release notes explain the old per-plugin algorithm mismatch and give a recovery path for accounts accidentally enrolled with SHA-512 fallback behavior.
Maintenance5/5npm published 13.5.0 on August 21, 2026, and GitHub recorded a push on August 23. The repository is unarchived, has 2,289 stars, and currently shows 0 open issues plus 5 pull requests. The latest release standardized hash validation across noble, Node, and Web Crypto plugins and added a named error. The monorepo also tests separate core, HOTP, TOTP, URI, Base32, crypto, CLI, and prior-version adapter packages.
Ecosystem5/5npm counted 3,481,059 downloads in the week ending August 24, 2026. The default package brings 6 direct modules for core logic, HOTP, TOTP, URI handling, Base32, and noble crypto, while optional packages cover Node and Web Crypto, alternate encodings, a CLI, and v11 or v12 adapters. Its default SHA-1, 6-digit, 30-second profile matches common authenticator apps, and our checks found typed import and require paths.

Use it if

  • An account system needs RFC 6238 codes compatible with mainstream authenticator apps
  • HOTP counters and TOTP time steps should share one typed API with verification deltas
  • OTP code must run across Node, Bun, Deno, and browsers through explicit crypto plugins
  • The login flow will persist accepted time steps or counters to block token replay
Skip it if

Setup reality

Our fresh otplib 13.5.0 install finished in 3.7 seconds under Node 22. It left 9 packages and 3 MB on disk, with 0 known vulnerabilities from npm audit. The package is 668 KB unpacked, has 6 direct dependencies and 0 peers, and includes TypeScript declarations. It is ESM with an exports map, but both require() and ESM import worked. Our all-exports browser bundle measured 35.8 KB minified and 11.9 KB gzipped.

There is no service credential or config file, but every account needs a secret. generateSecret() produces a 20-byte random secret encoded as Base32. Store it encrypted and never log it. Version 13 rejects Base32 input shorter than 16 decoded bytes, so the common JBSWY3DPEHPK3PXP tutorial value throws SecretTooShortError. generateURI() creates an otpauth URI rather than a QR image. Render it with another library and stop exposing the URI once enrollment is confirmed.

verify() is async-first and returns { valid, ... }, never a bare boolean. Read result.valid. TOTP tolerance is 0 by default; epochTolerance can be a [pastSeconds, futureSeconds] tuple when past-only drift is desired. To stop replay, save the last successful timeStep per account and pass it back as afterTimeStep. Persist that update atomically with the login. HOTP needs the same discipline for its counter, advancing to the matched counter plus 1 after successful verification.

Version 13.5.0 accepts sha1, sha256, and sha512 with case-insensitive single-separator aliases such as SHA-1, then throws AlgorithmUnsupportedError for everything else. Older noble-plugin behavior could silently compute SHA-512 for an unknown name, so accounts enrolled through that bug may stop matching after upgrade and need corrected sha512 configuration or re-enrollment. Keep the issuer, digits, period, algorithm, secret encoding, and HOTP counter identical on both sides. Rate-limit attempts and protect recovery routes outside otplib.

Patterns

Create a fresh Base32 enrollment secret generate-secret

import { generateSecret } from 'otplib'

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

generateSecret() defaults to 20 random bytes in version 13.5.0. Encrypt the value at rest and exclude it from logs.

Produce the current TOTP generate-totp

import { generate } from 'otplib'

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

The default strategy is TOTP, and string secrets are decoded as Base32. Raw passphrases need an explicit byte conversion or alternate plugin.

Read the verification result correctly verify-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 in version 13. Testing the object itself always sees a truthy value, even when valid is false.

Create an authenticator URI build-enrollment-uri

import { generateURI } from 'otplib'

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

The result is an otpauth URI, not a QR image. Keep the issuer and label stable, then render the URI with a separate QR package.

Accept one past 30-second step allow-past-drift

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

The tuple means past and future seconds. [30, 0] avoids accepting the next 30-second token early.

Reject a reused time step prevent-totp-replay

const lastStep = await loadLastTimeStep(userId)
const result = await verify({
  secret,
  token: submittedCode,
  afterTimeStep: lastStep ?? undefined,
  epochTolerance: [30, 0],
})
if (result.valid) {
  await saveLastTimeStep(userId, result.timeStep)
}

afterTimeStep is a counter rather than a Unix timestamp. Save the successful step atomically with the authenticated session.

Generate from a stored HOTP counter generate-hotp

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

HOTP requires an explicit counter. Counter 42 must not be reused after its token is accepted.

Look ahead across 5 HOTP counters resync-hotp

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 searches forward. Advance the stored counter to the matched position plus 1 in the same transaction.

Use an explicit supported digest set-algorithm

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

Version 13.5.0 accepts sha1, sha256, and sha512 plus documented aliases. Unsupported strings throw AlgorithmUnsupportedError.

Reject bad external hash configuration catch-invalid-algorithm

import { AlgorithmUnsupportedError, generate } from 'otplib'

try {
  return await generate({ secret, algorithm: configuredAlgorithm })
} catch (error) {
  if (error instanceof AlgorithmUnsupportedError) {
    throw new Error('Unsupported OTP algorithm')
  }
  throw error
}

Do not silently substitute a digest. Version 13.5.0 makes all crypto plugins reject an unknown algorithm consistently.

Share strategy through an OTP instance use-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 simpler for isolated calls. OTP is useful when the same strategy or plugin setup belongs to several operations.

Mark raw secret bytes explicitly pass-binary-secret

import { generate, stringToBytes } from 'otplib'

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

String inputs are Base32 by default. Uint8Array distinguishes raw bytes, but the 16-byte minimum guardrail still applies.

Alternatives

PackageRegistryPick it when
otpauthnpmUse it for a focused HOTP and TOTP implementation with portable runtime support
@oslojs/otpnpmUse it when small explicit OTP primitives fit an application-owned enrollment flow
@epic-web/totpnpmUse it for TOTP-only work built around Web Crypto-compatible runtimes

More security guides

cryptography · pyjwt · jose · requests-oauthlib · oauthlib · dompurify · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.