This library generates and verifies time-based one-time passwords (TOTP, RFC 6238) and HMAC-based one-time passwords (HOTP, RFC 4226) for two-factor authentication (2FA). It also creates the secret, the otpauth:// URI and the QR code that authenticator apps such as Google Authenticator or Authy scan.
- Generate a 2FA secret: a random base32 secret, its
otpauth://URI and a QR code of that URI. - Generate and verify TOTP tokens: 6-digit codes that change every 30 seconds, optionally accepting each token only once.
- Generate and verify HOTP tokens: 6-digit codes tied to a counter.
npm install 2fa-nodeRequires Node.js 20.19 or later.
import { generateSecret } from "2fa-node";
const { secret, uri, qr } = await generateSecret({
name: "MyApp", // issuer shown in the authenticator app
account: "user@example.com", // account shown in the authenticator app
});
console.log(secret); // store it for the user
console.log(uri); // otpauth://totp/MyApp:user%40example.com?secret=...&issuer=MyApp
console.log(qr); // PNG data URL to show to the userimport { generateSecret } from "2fa-node";
const { secret, uri, qr } = await generateSecret(
{ name: "MyApp", account: "user@example.com", counter: 0 },
"HOTP",
);import { verifyToken } from "2fa-node";
const secret = "JBSWY3DPEHPK3PXP"; // the user's stored secret
const token = "123456"; // the token typed by the user
const isValid = verifyToken(secret, token);
console.log(isValid); // true if valid, false otherwiseimport { verifyTokenOnce } from "2fa-node";
const secret = "JBSWY3DPEHPK3PXP"; // the user's stored secret
const token = "123456"; // the token typed by the user
const lastTimeStep = null; // the user's stored time step, null before the first login
const match = verifyTokenOnce(secret, token, lastTimeStep);
if (match) {
// Save match.timeStep as the user's lastTimeStep, so this token cannot be used again.
}import { generateToken } from "2fa-node";
const secret = "JBSWY3DPEHPK3PXP";
const result = generateToken(secret);
console.log(result?.token); // the current token, or undefined if the secret is invalidimport { verifyHOTPToken } from "2fa-node";
const secret = "JBSWY3DPEHPK3PXP"; // the user's stored secret
const token = "123456"; // the token typed by the user
const counter = 1; // the user's stored counter
const isValid = verifyHOTPToken(secret, token, counter);
if (isValid) {
// Save counter + 1 so this token cannot be used again.
}import { generateHOTPToken } from "2fa-node";
const secret = "JBSWY3DPEHPK3PXP";
const counter = 1;
const result = generateHOTPToken(secret, counter);
console.log(result?.token); // the token, or undefined if the secret is invalidTokens have 6 digits and use HMAC-SHA1; TOTP tokens change every 30 seconds. Secrets are case-insensitive base32 strings of at least 80 bits (16 characters).
generateSecret(options: SecretOptions, type: OtpType = "TOTP"): Promise<{ secret: string; uri: string; qr: string }>
Generates a random secret for two-factor authentication.
- Parameters:
options.name: issuer shown in the authenticator app, usually your product name.options.account: account shown in the authenticator app, usually the user's email.options.counter(optional): initial HOTP counter written to the URI. Defaults to0; ignored for TOTP.options.numberOfSecretBytes(optional): size of the secret in bytes. Defaults to20; must be an integer of at least16, because RFC 4226 requires 128-bit secrets.type:"TOTP"(default) or"HOTP".
- Returns: a Promise resolving to:
secret: the base32 secret to store for the user.uri: theotpauth://URI.qr: a PNG data URL of the URI's QR code.
- Throws:
TypeErrorfor an unknowntype;RangeErrorfor an invalidnumberOfSecretBytesor HOTPcounter.
Generates the current TOTP token.
- Returns:
{ token }, ornullif the secret is invalid.
Verifies a TOTP token.
- Parameters:
window: how many 30-second steps before and after now are also accepted. Use[past, future]for different values;0accepts only the current step.
- Returns:
trueif the token is valid;falseotherwise, including when the token is missing or malformed or the secret is invalid. - Throws:
RangeErrorifwindowis not made of non-negative integers or spans more than 98 steps in total.
verifyTokenOnce(secret: string, token?: string, lastTimeStep?: number | null, window: number | [number, number] = 1): { timeStep: number } | null
Verifies a TOTP token like verifyToken, but also rejects tokens from lastTimeStep or earlier, so each token is accepted only once.
- Parameters:
lastTimeStep: thetimeStepreturned by the user's last successful check, ornull/undefinedif there is none.window: same as inverifyToken.
- Returns:
{ timeStep }with the 30-second step the token belongs to, ornullif the token is rejected. SavetimeStepfor the user after each success. - Throws: the same errors as
verifyToken, plus aRangeErroriflastTimeStepis not a non-negative safe integer.
Generates the HOTP token for a counter.
- Returns:
{ token }, ornullif the secret is invalid. - Throws:
RangeErrorifcounteris not a non-negative safe integer.
Verifies an HOTP token against exactly the given counter.
- Returns:
trueif the token is valid;falseotherwise, including when the token is missing or malformed or the secret is invalid. - Throws:
RangeErrorifcounteris not a non-negative safe integer.
SecretOptions and OtpType ("TOTP" | "HOTP") are exported for TypeScript users.
-
verifyTokenaccepts the same token again while it is inside the window. To block replays, useverifyTokenOnce, including for the check that confirms enrollment, and save the returnedtimeSteponly if it moves forward. Do it in a single statement and treat zero updated rows as a failed login, so two simultaneous requests with the same token cannot both succeed:UPDATE users SET last_time_step = $step WHERE id = $id AND (last_time_step IS NULL OR last_time_step < $step);
For HOTP, advance the counter the same way:
UPDATE users SET counter = counter + 1 WHERE id = $id AND counter = $counter. -
Limit how many verification attempts a user can make.
-
Keep secrets encrypted at rest.
- HOTP tokens now follow RFC 4226 and match authenticator apps, so they differ from the tokens 0.x produced.
verifyTokenandverifyHOTPTokenreturnfalseinstead ofnullwhen the token is missing.verifyTokenaccepts one step (30 seconds) before and after now by default, instead of four.verifyHOTPTokenrequires the counter and no longer takes awindowargument, which never had any effect.- Secrets must be canonical base32 of at least 80 bits. Secrets that 0.x accepted, such as ones with spaces, extra
=padding or non-zero unused trailing bits, now make the generate functions returnnulland the verify functions returnfalse. numberOfSecretBytesmust be at least16. Invalid types, counters and windows throw.- The URI carries the standard
issuerparameter instead ofname. - Only the package entry point can be imported: deep imports such as
2fa-node/dist/secret.jsno longer work. - Node.js 20.19 or later is required.
- The license changed from MIT to MPL-2.0.
- otplib - A library for generating and verifying one-time passwords.
- qrcode - A library for generating QR codes.
This project is licensed under the Mozilla Public License 2.0 - see the LICENSE.md file for details.