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.
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.
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
- You are maintaining v11 or v12 code and cannot schedule a migration: v13 is a complete rewrite, removes the separate authenticator API, changes verification to return an object, and moves compatibility behind adapter packages
- You want a complete MFA product with encrypted secret storage, recovery codes, rate limits, enrollment state, and account recovery; the public API covers OTP generation, verification, plugins, and otpauth URIs, so those application controls remain your responsibility
- You must accept short secrets copied from old tutorials or RFC examples without an explicit exception: v13 rejects Base32 secrets that decode below 16 bytes unless you weaken MIN_SECRET_BYTES with createGuardrails
- You need a synchronous API with an arbitrary crypto implementation: the README says sync calls only work with sync-compatible plugins, while the default design and examples are async-first
- You control neither the token settings nor the authenticator app: the documented interoperable defaults are SHA-1, six digits, and a 30-second period, and the README warns that apps may not support deviations
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
| Package | Registry | Pick it when |
|---|---|---|
| otpauth | npm | You want a focused HOTP and TOTP implementation with first-class Node.js, Deno, Bun, and browser support |
| @oslojs/otp | npm | You prefer small, explicit HOTP and TOTP primitives and are comfortable assembling the enrollment flow yourself |
| @epic-web/totp | npm | You only need TOTP and want an API designed around Web Crypto-compatible runtimes |