CLI tool
yeojz/otplib avatar
yeojz/otplib

otplib v13: one-time passwords split into small interchangeable packages

One Time Password (OTP) / 2FA for Node.js and Browser - Supports HOTP, TOTP and Google Authenticator

2,292 stars146 forksTypeScriptMIT

At a glance

What is it?
A TypeScript rewrite of the long-lived OTP library, where hashing and base32 became pluggable backends, every operation returns a promise, and the root manifest still claims to be version 12.
Who is it for?
otplib v13 is a good fit if you want an OTP implementation with an auditable dependency story and you are willing to read a plugin interface rather than accept a single opinionated package. Two facts should shape your plan.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 15 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 28, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What the library does and who maintains it

otplib implements one-time passwords for Node and the browser, covering both HOTP and TOTP, and the repository description states the compatibility target plainly: Google Authenticator. It is MIT licensed, written in TypeScript, and sits at 2292 stars and 146 forks with three open issues.

Three open issues on a library with that adoption is a very low number, and it is the kind of signal that is either excellent maintenance or a tracker nobody uses. The readme points contributing guidance at a CONTRIBUTING.md and the tree also carries a SECURITY.md, and the releases show dependabot activity, so the maintenance is real rather than quiet.

What makes v13 different from the twelve versions before it is stated as a warning rather than a changelog. The readme says v13 is a complete rewrite with breaking changes, and gives two examples. The separate authenticator package was removed, because TOTP now covers all authenticator functionality with default plugins. And outdated plugins were removed, with legacy crypto adapters replaced by modern audited alternatives.

The homepage is otplib.yeojz.dev, which also hosts a web demo. The readme describes that demo as a place to scan and test TOTP and HOTP QR code samples with your own authenticator app, which is a genuinely useful way to check a library against the phone in your pocket before you depend on it.

The topic list is unusually thorough for this category, twelve entries covering the acronym itself, both RFC algorithm names, Google Authenticator, the runtime names, and both spellings of two-factor authentication. Whatever SEO discipline the maintainer applied here, it worked.

Ten packages instead of one

The structural decision that defines v13 is that this is no longer one package. The readme carries a table of ten published packages, and it is worth reading as a dependency graph rather than a list.

At the bottom sits otplib, the umbrella you actually install, and then the algorithm and protocol packages: @otplib/core, @otplib/totp, @otplib/hotp and @otplib/uri. The uri package is what generates the otpauth URI that an authenticator app scans, so it is a separate concern from token generation and it is treated as one.

Then there are the three plugin packages, and the naming makes the axis explicit. There is @otplib/plugin-base32-scure for encoding and decoding, and three separate crypto plugins: @otplib/plugin-crypto-node, @otplib/plugin-crypto-web and @otplib/plugin-crypto-noble. Three crypto plugins rather than one is the interesting part, and the readme's feature list gives the reason by naming the default, stating that default crypto uses @noble/hashes and @scure/base, both independently audited, and listing security-audited plugins as a feature in its own right.

The design consequence is that the abstraction boundary is the right one. Base32 is a codec, not a security primitive, so having exactly one audited implementation of it is correct. Cryptography has three real implementations across runtimes, so having a plugin per runtime is correct. The umbrella package wires the defaults together so most people never touch the plugins, and people who need Node's native crypto, or a Web Crypto implementation in a browser bundle, swap one package.

There is also otplib-cli in the same table, which makes sense given the demo and gives you a way to generate and inspect codes from a terminal.

Every call is async, and that is the API's main opinion

The feature list says async-first API, with all operations async by default and sync variants available for compatible plugins. That is a deliberate departure from the v12 shape, where the library was synchronous, and it is the change most likely to surprise an existing user.

The quick start is three imports and three calls.

typescript
import { generateSecret, generate, verify, generateURI } from "otplib";
typescript
const secret = generateSecret();
typescript
const token = await generate({ secret });
typescript
const result = await verify({ secret, token });
console.log(result.valid); // true

Everything arrives through a single options object, so there is no positional argument order to get wrong, and every operation that touches a pluggable backend is a promise. generateSecret is the interesting asymmetry, since it calls the crypto plugin and so is itself awaitable even though the name suggests otherwise.

Why async matters here is the plugin contract. If a user swaps in a crypto backend that is itself asynchronous, such as a call to a hardware security module or a remote signing service, a synchronous API cannot express it. Designing async-first means the slowest possible backend still fits the signature. The cost is that the common path, local HMAC on a secret you already hold, pays for an abstraction the majority of users do not need. If you are on a hot path where you verify thousands of tokens, check whether the sync variants are exported and use them.

The four named exports in that import line are also the whole surface a first implementation needs: make a secret, make a token, check a token, and build the URI for enrollment.

The manifest says 12.0.1 and the release says 13.5.0

Here is the contradiction to know about before you plan an upgrade.

The root package manifest on main reads name otplib-repo, version 12.0.1, with private set to true. The published releases are v13.5.0 from 2026-08-21, v13.4.1 from 2026-05-30 and v13.4.0 from 2026-03-19. Both statements come from the repository and they disagree about the version.

The reason is the monorepo layout, and it is not an error. The root is a pnpm workspace, with pnpm-workspace.yaml and turbo.json in the tree, and every real package lives under packages. The root manifest is a private workspace stub used to hold the scripts that orchestrate everything, so its version field was never the version you get from npm and its name is a repository name rather than a package name. It also carries the release command, publishing recursively across the packages directory with provenance and no git checks.

The practical consequence is narrow but it bites people doing dependency auditing. Any tool that reads versions from source trees will report 12.0.1 and may flag it against advisories or against your lockfile. Anything reading installed package metadata gets the real number. So trust the installed package and the tag, and treat the manifest as infrastructure.

The same manifest is also a good map of the project's engineering priorities. There are per-operation benchmark scripts, four of them for base32, HOTP, TOTP and utils, plus a size check script that runs a production build and inspects bundle size. Vitest handles tests, with separate distribution test files for Bun and for Deno, which means the packages are tested as installed artifacts in each runtime rather than only from source. lefthook installs git hooks on postinstall, Prettier formats, ESLint lints, and an audit script gates production dependencies at the moderate level.

What the release notes say about dependency risk

The three most recent releases are almost entirely about supply chain, which tells you what the maintainer considers the main threat model for a security library.

v13.4.0 from March 2026 resolves a markdown-it ReDoS vulnerability, which is a documentation-site dependency rather than a runtime one, then bumps a minor dependency group and moves dependabot to a monthly cadence. That cadence change is itself a signal: weekly was producing noise, monthly is a deliberate reduction.

v13.4.1 from May 2026 is three dependency alerts and a test refactor. It adds overrides for flatted, picomatch, yaml and brace-expansion. Four overrides in one release is a lot, and overrides are the tool you reach for when you cannot upgrade the parent package. That pattern usually means a transitive dependency is held back by something you depend on, so it is worth checking whether these overrides still exist in the version you install.

v13.5.0 from August 2026 leads with something different: a mutation testing audit that closes test gaps, adds Stryker tooling, and adds a manual CI job. Mutation testing is the practice of deliberately breaking your code and confirming the tests notice. For a library whose entire job is producing a six-digit number correctly, that is an unusually appropriate investment, because the failure mode is a library that returns plausible wrong numbers rather than an error.

Read together with the feature list, the picture is coherent. The library claims RFC 6238 and RFC 4226 compliance, names audited primitives as the default, and is now checking that its tests can detect subtle implementation errors. The open question for an adopter is narrower: whether the plugins you need are audited the same way, since the claim about auditing is specifically about the default crypto.

Choosing how to use it, and what to check first

There are three reasonable adoption shapes and they differ more than the readme suggests.

If you are building a server-side enrollment flow, use the umbrella package with defaults, call generateSecret, then generateURI to produce the otpauth string, show it as a QR code, and store the secret. The readme's getting started guide is the place to confirm the current parameter names, and the API reference is linked separately for that reason.

If you are verifying tokens in a hot path, plan for the sync variants. The default path is asynchronous by design, and awaiting a promise per verification is a real cost when you are checking thousands of codes per second. Verify that the sync form is exported from the version you install before designing around it.

If you are bundling for the browser, the crypto plugin choice is the decision that matters. The default is the audited noble implementation, which is the safest starting point, and @otplib/plugin-crypto-web exists as the alternative if you have a reason to prefer the platform's Web Crypto. Whatever you choose, the base32 codec is a single audited implementation and there is no reason to vary it.

Two things to verify before you commit to v13. Read the migration guide linked in the readme, because the removed authenticator package and the removed legacy crypto adapters are both things a v12 codebase will reference directly. And read the security considerations page, since a library that generates your second factor deserves a look at how it expects you to store secrets.

Editorial conclusion

otplib v13 is a good fit if you want an OTP implementation with an auditable dependency story and you are willing to read a plugin interface rather than accept a single opinionated package. Two facts should shape your plan. Version 13 is a complete rewrite with breaking changes, including the removal of the separate authenticator package and of the legacy crypto adapters, so a v12 codebase needs the migration guide rather than a version bump. And the root package manifest reads name otplib-repo at version 12.0.1 with private set to true, while the published releases are at v13.5.0, so the manifest is a workspace stub and never the version you installed. Install the package named otplib and read the installed package metadata, not the repository file.

Frequently asked questions

Is OTP authentication safe?

It is one of the stronger second factors available, because the code is generated from a shared secret rather than transmitted, so an attacker who intercepts traffic never sees the credential. A time-based code also expires within its window, which limits replay. The real risk moves to secret storage: anyone who obtains your shared secret can generate valid codes indefinitely, so it must be stored like a password and never logged. otplib generates those secrets and codes, but it does not decide how you store them, and its documentation has a dedicated security considerations page for that.

What is an OTP authentication code?

It is a short numeric string derived from a shared secret and the current time, or a counter in the HOTP variant. The server holds the same secret and recomputes the expected code, then compares. otplib's verify function returns a result object whose valid property reports the outcome, and the default window accepts codes slightly outside the current period so that clock drift between phone and server does not lock people out. The README names RFC 6238 for TOTP and RFC 4226 for HOTP as the standards it implements.

How do I get my 6 digit Authenticator code?

From an authenticator app that has been enrolled with a shared secret, usually by scanning a QR code. The app computes the current code locally from that secret and the current time, which is why it works with no network. otplib is the server-side half of the same computation and has a generateURI function that builds the otpauth string an app scans, plus a demo at otplib.yeojz.dev where you can scan test QR codes and compare what your phone shows against what the library produces.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. yeojz/otplib on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/yeojz-otplib.svg)](https://hysenlabs.com/projects/yeojz-otplib)