XOTP(/zɔːtipi/) is a robust One-Time Password (HOTP/TOTP) library for Node.js, Bun, and Deno environments with zero dependencies. It's perfect for implementing two-factor authentication (2FA) / multifactor authentication (MFA) systems and is fully compatible with Google Authenticator and other well-known authentication apps and devices.
XOTP implements both RFC 4226 (HOTP) and RFC 6238 (TOTP) and has been fully tested against the test vectors provided in their respective RFC specifications: RFC 4226 Dataset and RFC 6238 Dataset.
Tip
You can try XOTP with the demo available at xotp.dev!
- Installation
- Try it
- Quick start
- Enrollment (bound instance)
- Usage
- Key URI & QR Code Generation
- References
npm i xotp
bun add xotp
deno add npm:xotpWorks with both import (ESM) and require() (CommonJS):
import { TOTP } from "xotp";
// or: const { TOTP } = require("xotp");import { Secret, TOTP } from "xotp";
const secret = Secret.from("JBSWY3DPEHPK3PXP", "base32");
const totp = new TOTP();
const token = totp.generate({ secret });
const ok = totp.validate({ secret, token });
console.log({ token, ok }); // { token: '...', ok: true }No install? Try the live demo at xotp.dev.
Shared TOTP engine; pass each user's secret per call (API login, multi-tenant apps):
import { Secret, TOTP } from "xotp";
const secret = Secret.from("JBSWY3DPEHPK3PXP", "base32");
const totp = new TOTP();
const token = totp.generate({ secret });
totp.validate({ secret, token }); // trueSee Usage for secret storage, options, and token delta.
Generate a secret and otpauth:// URI for one user (2FA setup, QR onboarding):
import { TOTP } from "xotp";
const totp = TOTP.create({ account: "user@example.com", issuer: "MyApp" });
console.log(totp.toKeyUri());
console.log(totp.secret!.toString()); // base32 — persist before discarding the instanceSee Enrollment (bound instance) and Key URI & QR Code Generation.
TOTP.create({ account, issuer }) binds a generated secret to the instance — use it for enrollment flows. Equivalent:
new TOTP({ generateSecret: true, account: "user@example.com", issuer: "MyApp" });After generate() or toKeyUri(), persist totp.secret (e.g. totp.secret!.toString() for base32 storage).
Tip
For server-side validation of many users, use a shared engine without a bound secret and pass each user's secret per call: totp.validate({ secret: userSecret, token }). Do not reuse one bound instance across users.
import { Secret, TOTP } from "xotp";The following walks through the server-side validation flow in more detail:
First, you need a secret key with which to generate or verify a OTP token.
If you already have a secret key as a string in any supported encoding, you can use it like this:
const secret = Secret.from("<YOUR_SECRET_KEY>");Otherwise, use the Secret constructor to generate a cryptographically strong 20-byte random key:
const secret = new Secret();If you need to generate a secret from a native Buffer type, or store it in a particular encoding, see the Secret reference section.
Next, generate a OTP token with the secret you've created:
const totp = new TOTP(/* options, if any! */);
const token = totp.generate({ secret });You can customize token generation by passing optional arguments to the new TOTP() constructor. All available options and their default values are detailed in the TOTP Options section. While the new TOTP() constructor accepts options, you can override these by passing specific values to the generate({secret, ...options}) method for individual token requests.
When a user submits a token—either one you generated with XOTP or one from an authentication app like Google Authenticator—you'll need to verify it:
const isValidToken = totp.validate({ secret, token: "<USER_SUBMITTED_TOKEN>" });Similar to all TOTP and HOTP methods, you can pass new option values to the validate({secret, token, ...options}) method to override those set during the TOTP instance initialization.
To determine the difference between the current time step and the time step when a given token was generated, use the compare method:
const delta = totp.compare({ secret, token: "<USER_SUBMITTED_TOKEN>" });This method returns 0 if the token is for the current time step, or null if the token is not found within the search window. Otherwise, it returns the difference in the window.
You can adjust the search window through the options passed to the method, or by modifying the default value in the options passed to the TOTP constructor. The default window value is 1, meaning it checks one time step before and one time step after the current time step to see if the token was generated in any of those steps.
toKeyUri() returns an otpauth:// URI string. For a structured object, use URI.parse() (see Import below).
const uri = totp.toKeyUri({
secret,
account: "<fullname, username or email>",
issuer: "MyApp", // default is "xotp" if omitted
});The account and issuer fields are display labels shown in authenticator apps like Google Authenticator.
You can override options per call even when they differ from the TOTP instance defaults.
Import from a scanned QR code or pasted key URI:
import { URI, TOTP, HOTP } from "xotp";
const totp = TOTP.fromKeyUri(
"otpauth://totp/Issuer:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Issuer",
);
const token = totp.generate();
const isValid = totp.validate({ token: "<USER_SUBMITTED_TOKEN>" });
// Low-level parse / format
const scannedUri =
"otpauth://totp/Issuer:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Issuer";
const keyUri = URI.parse(scannedUri);
const uriAgain = URI.format(keyUri);
const otp =
keyUri.type === "totp"
? TOTP.fromKeyUri(scannedUri)
: HOTP.fromKeyUri(scannedUri);
// Or build a Key URI without parsing
const uri = URI.format({
type: "totp",
secret,
account: "user@example.com",
issuer: "MyApp",
});HOTP.fromKeyUri(uri) works the same for otpauth://hotp/... URIs. The imported instance binds secret from the URI so you do not pass secret on each call.
The toKeyUri method returns a standard otpauth:// URI string, which you can encode into a QR code for authenticator apps. XOTP stays zero-dependency — pair it with a QR library such as qrcode:
npm install qrcode
npm install @types/qrcode # TypeScriptimport QRCode from "qrcode";
import { TOTP } from "xotp";
const totp = TOTP.create({
account: "user@example.com",
issuer: "MyApp",
});
const uri = totp.toKeyUri();
const qrCodeDataURL = await QRCode.toDataURL(uri); // web <img src="...">
await QRCode.toFile("qrcode.png", uri); // or save to diskimport { TOTP } from "xotp";
import QRCode from "qrcode";
async function setup2FA(userEmail: string) {
const totp = TOTP.create({ account: userEmail, issuer: "MyApp" });
const uri = totp.toKeyUri();
const qrCodeDataURL = await QRCode.toDataURL(uri);
const secretKey = totp.secret!.toString(); // base32 — store securely
return { secretKey, qrCodeDataURL };
}Caution
Always store the secret key securely and never expose it to the client-side after the initial setup.
More QR options (qr-image, in-browser display)
qr-image
npm install qr-image
npm install @types/qr-image # TypeScriptimport qr from "qr-image";
import fs from "fs";
const uri = totp.toKeyUri();
qr.image(uri, { type: "png" }).pipe(fs.createWriteStream("qrcode.png"));
const qrSvg = qr.imageSync(uri, { type: "svg" });In-browser display
const qrCodeDataURL = await QRCode.toDataURL(uri);
const img = document.createElement("img");
img.src = qrCodeDataURL;
img.alt = "QR Code for 2FA Setup";
document.body.appendChild(img);Public API: TOTP, HOTP, Secret, URI, and types (KeyUri, TOTPKeyUri, HOTPKeyUri, Algorithm, Encoding, option types). Full signatures ship in dist/index.d.ts.
The Secret class allows you to generate and retrieve your secret keys in various encodings. Let's explore some of its key functions:
Use the Secret constructor to generate a cryptographically strong random key of a desired size in bytes.
const secret = new Secret({ size: 64 });The default size is 20 bytes.
const secret = new Secret();
// Equivalent to:
const secret = new Secret({ size: 20 });If you're unsure about the appropriate size and only know the algorithm you plan to use, call the for static method to get a Secret instance tailored for a specific supported algorithm.
const secret = Secret.for("sha512");Note
XOTP uses sha1 as the default algorithm for generating both TOTP and HOTP tokens.
If you already have a secret key in binary, you can initialize a Secret instance using a native Buffer object or a JavaScript ArrayBuffer. For example:
// This defines a dummy buffer of random 42-byte binary.
// You would replace it with your buffer.
const buffer = Buffer.from(
Array.from({ length: 42 }, () => Math.round(Math.random())),
);
const secret = new Secret({ data: buffer });Alternatively, use the from static method to retrieve a Secret instance from a buffer:
const secret = Secret.from(buffer);You can also use from static method to get a Secret instance from a string in various encodings.
const secret = Secret.from("LBHVIUBAFBKE6VCQF5EE6VCQFE======", "base32");Almost all applications need to store the secret key to generate and verify the user's token later. To do this, use the toString method to get the secret key in one of the available encodings:
const secretKey = secret.toString("hex");The default encoding for toString() is base32 because most authentication apps, including Google Authenticator, use base32 as the default encoding for the secret key.
Note
The default encoding for the from method is utf-8, while the default encoding for toString is base32. Therefore, you need to pass the second argument in one of these two functions. This means:
const base32SecretKey = secret.toString();
const clonedSecret = Secret.from(base32SecretKey, "base32");Or vice versa:
const utf8SecretKey = secret.toString("utf-8");
const clonedSecret = Secret.from(utf8SecretKey);We recommend the former!
| Option | Type | Default | Description |
|---|---|---|---|
| algorithm | string |
"sha1" | The algorithm used for calculating the HMAC, see supported algorithms! |
| digits | number |
6 | The length of the OTP token. |
| window | number |
1 | The number of window(s) within which to validate the token. If the token isn't validated in the current time step, XOTP attempts to validate it in the previous and future windows. |
| duration | number |
30 | The duration (in seconds) for which a token is valid. |
| issuer | string |
"xotp" | The provider or service associated with the token (e.g., "Github"). This is just a display field to display the issuer's name in authenticator apps like Google Authenticator. |
| account | string |
The account associated with the token (e.g., the user's email). This is also a display field to display the account name in authenticator apps like Google Authenticator. | |
| secret | Secret |
Binds a secret to the instance. When set, generate, validate, and related methods can omit secret in each call. |
|
| generateSecret | boolean |
false |
Set to true when enrolling new 2FA (no secret yet) so one random secret is created at construction — use TOTP.create() or persist instance.secret. Keep false (default) for server validators that pass each user's secret per call. |
XOTP also supports RFC 4226 HOTP (counter-based OTP). Unlike TOTP, HOTP uses a counter instead of a time step — pass counter to generate and validate.
import { HOTP, Secret } from "xotp";
const hotp = new HOTP();
const secret = Secret.from("<YOUR_SECRET_KEY>", "base32");
const token = hotp.generate({ secret, counter: 0 });
const isValid = hotp.validate({ secret, token, counter: 0 });Enrollment and key URIs work the same as TOTP: HOTP.create(), HOTP.fromKeyUri(), and hotp.toKeyUri(). HOTP key URIs require a counter query parameter.
When a bound HOTP instance generates without an explicit counter, the instance counter increments automatically.
| Option | Type | Default | Description |
|---|---|---|---|
| algorithm | string |
"sha1" | HMAC algorithm; see supported algorithms. |
| digits | number |
6 | Token length (6 or 8). |
| window | number |
1 | Counter values before/after the expected counter to accept during validation. |
| counter | number |
0 | Current counter value. Required in HOTP key URIs. |
| issuer | string |
"xotp" | Display label for the service name in authenticator apps. |
| account | string |
Display label for the user (e.g. email). | |
| secret | Secret |
Binds a secret to the instance so methods can omit secret per call. |
|
| generateSecret | boolean |
false |
Set to true for enrollment, or use HOTP.create(). Keep false for shared server validators. |
base32base64base64urlutf8/utf-8utf16le/utf-16le/ucs2/ucs-2latin1asciibinaryhex
If you require an encoding not listed here, please let us know by opening an issue!
Tip
Google Authenticator uses base32 encoding for the secret key!
sha1sha224sha256sha512sha384sha-512/224sha-512/256sha3-224sha3-256sha3-384sha3-512
If you need an algorithm that is not listed, please open an issue for it!
Tip
Google Authenticator ignores the algorithm type and defaults to sha1.
XOTP thrives on community input! Got an idea that could make OTP handling even better?
- Open an issue with your feature suggestion/request and we will provide feedback lightning-fast.
Let's build the best OTP library together!
XOTP is MIT licensed