The selected packages form the ecosystem for implementing Two-Factor Authentication (2FA) using Time-based One-Time Passwords (TOTP) and generating the necessary QR codes for user setup. speakeasy, otplib, notp, node-2fa, and authenticator handle the cryptographic generation and validation of tokens, while qrcode is the standard utility for rendering the provisioning URI into a scannable image. speakeasy and otplib are the most robust, modern libraries for the crypto logic, whereas notp is an older, minimal alternative. node-2fa and authenticator act as higher-level wrappers that simplify the API but may offer less flexibility. qrcode stands alone as the definitive solution for turning text data into visual QR codes in both Node.js and browser environments.
Implementing Two-Factor Authentication (2FA) in a Node.js application requires two distinct technical capabilities: generating and validating cryptographic tokens (TOTP/HOTP) and creating QR codes for users to scan with their authenticator apps. The packages speakeasy, otplib, notp, node-2fa, and authenticator address the cryptography, while qrcode handles the visual encoding. Understanding the difference between low-level crypto engines and high-level wrappers is key to building a secure, maintainable system.
The heart of 2FA is the algorithm that generates the 6-digit code. You have three main architectural choices here: a modular modern system (otplib), a comprehensive classic system (speakeasy), or a minimal legacy system (notp).
otplib is built for modern JavaScript. It splits functionality into small, interchangeable plugins. You import exactly what you need (e.g., just the TOTP logic), which helps keep your bundle size down and makes testing easier. It also has first-class TypeScript definitions.
// otplib: Modular import
import { authenticator } from 'otplib';
// Configure options globally or per call
authenticator.options = { step: 30, digits: 6 };
const secret = authenticator.generateSecret();
const token = authenticator.generate(secret);
const isValid = authenticator.verify({ token, secret });
speakeasy is the industry workhorse. It provides a single, large object with methods for every possible variation (TOTP, HOTP, counter-based). It is less modular but extremely stable and widely trusted in production environments for years.
// speakeasy: Comprehensive single import
import speakeasy from 'speakeasy';
const secret = speakeasy.generateSecret({ name: "MyApp" });
const token = speakeasy.totp({
secret: secret.base32,
encoding: 'base32',
step: 30
});
const isValid = speakeasy.totp.verify({
secret: secret.base32,
encoding: 'base32',
token: token,
window: 1
});
notp is the minimal option. It does one thing and nothing else. It lacks the helper functions for generating secrets or creating URIs that the others have, meaning you often have to write more boilerplate code yourself.
// notp: Minimalist approach
import notp from 'notp';
// You must manage your own secret generation externally
const secret = "JBSWY3DPEHPK3PXP";
const token = notp.totp.gen(secret);
const verification = notp.totp.verify(token, secret, { window: 1 });
// verification returns null if invalid, or an object if valid
Some packages try to make life easier by wrapping the crypto logic into a single "setup" function. This trades flexibility for speed.
node-2fa is a wrapper designed to get you running in seconds. It handles secret generation, URI creation, and verification in one go. However, if you need to change the algorithm (e.g., from SHA1 to SHA256) or adjust the time step dynamically, you might hit its limits.
// node-2fa: High-level wrapper
import { generateSecret, generateAuthenticatorUrl, verifyToken } from 'node-2fa';
// Generates secret and URL in one step
const { secret, url } = generateAuthenticatorUrl({
name: "MyApp",
account: "user@example.com"
});
const isValid = verifyToken(secret, userInputToken);
authenticator follows a similar pattern, offering a simplified interface for common tasks. It is useful for basic implementations but, like node-2fa, you should check its update history before relying on it for high-security contexts, as wrappers can sometimes lag behind core security patches.
// authenticator: Simplified interface
import authenticator from 'authenticator';
// Generate a secret key
const secret = authenticator.generateKey();
// Generate a token
const token = authenticator.generateToken(secret);
// Verify a token
const isValid = authenticator.verifyToken(secret, token);
In contrast, speakeasy and otplib force you to be explicit. You must generate the secret, build the URI string yourself (or use their helpers), and pass every parameter to the verify function. This verbosity is a feature, not a bug—it prevents hidden magic from causing security gaps.
To let a user scan a code, you must create a specific URL format (otpauth://...). The lower-level libraries give you the tools to build this; the wrappers often do it for you.
speakeasy includes a dedicated helper to build this URL correctly, ensuring all parameters (issuer, algorithm, digits) are encoded properly.
// speakeasy: Built-in URI generator
const otpauthUrl = speakeasy.otpauthURL({
secret: secret.base32,
label: "MyApp:user@example.com",
issuer: "MyApp",
algorithm: 'SHA1',
digits: 6,
period: 30
});
// Returns: otpauth://totp/MyApp:user@example.com?secret=...
otplib expects you to construct the URL or use a plugin, giving you full control over the string format if you need custom parameters.
// otplib: Manual or plugin-based URI construction
import { urlencoder } from 'otplib-plugin-urlencoder';
// Or manually construct:
const label = encodeURIComponent("MyApp:user@example.com");
const secret = encodeURIComponent(mySecret);
const otpauthUrl = `otpauth://totp/${label}?secret=${secret}&issuer=MyApp`;
node-2fa hides this entirely, returning the URL as part of its setup object.
// node-2fa: URL returned automatically
const { url } = generateAuthenticatorUrl({ name: "MyApp", account: "user" });
// You get the string ready to use immediately
Once you have the otpauth URL from any of the crypto libraries, you need to turn it into an image. This is where qrcode comes in. It is the standard for this task and works independently of which crypto library you chose.
qrcode can output to the terminal, a data URL (for web images), or SVG. It is robust and handles long strings well.
// qrcode: Generating a Data URL for web display
import QRCode from 'qrcode';
async function generateQRImage(otpauthUrl) {
try {
// Returns a 'data:image/png;base64,...' string
const imageUrl = await QRCode.toDataURL(otpauthUrl);
return imageUrl;
} catch (err) {
console.error('Failed to generate QR', err);
}
}
// Usage with speakeasy output
const qrImage = await generateQRImage(otpauthUrl);
// <img src="${qrImage}" />
You can also render directly to the console for CLI tools:
// qrcode: Terminal output
import QRCode from 'qrcode';
QRCode.toString(otpauthUrl, { type: 'terminal' }, (err, code) => {
console.log(code);
// Prints an ASCII art QR code to the console
});
When selecting a crypto library, maintenance is a security feature.
notp: While functional, it has seen very little activity in recent years. For new projects, avoid notp unless you have a specific constraint requiring its minimal footprint. The lack of recent updates means it might not reflect the latest best practices or environment compatibility fixes.authenticator and node-2fa: These are convenient, but always verify their GitHub repositories for recent commits. If a wrapper hasn't been updated in over a year, prefer speakeasy or otplib directly, as they have larger communities auditing the core logic.speakeasy and otplib: Both are actively maintained and trusted. speakeasy has a longer track record; otplib has a more modern codebase.For most professional applications, the best approach is to combine a robust crypto engine with the standard QR generator.
otplib if you are starting a new TypeScript project and value modularity and type safety.speakeasy if you need a proven, "batteries-included" solution with extensive documentation and community examples.qrcode for the visual component, regardless of your crypto choice.Avoid relying on high-level wrappers like node-2fa for critical infrastructure unless you have verified they meet your specific security policy requirements, as they can obscure important configuration details.
| Feature | speakeasy | otplib | notp | node-2fa | qrcode |
|---|---|---|---|---|---|
| Primary Role | Crypto Engine | Crypto Engine | Crypto Engine | Wrapper | QR Generator |
| Architecture | Monolithic | Modular | Minimal | Opinionated | Utility |
| TypeScript | Community Types | Native Support | Community Types | Community Types | Native Support |
| Secret Gen | Built-in | Built-in | External Needed | Built-in | N/A |
| URI Builder | Built-in | Plugin/Manual | Manual | Built-in | N/A |
| Maintenance | Active | Active | Low Activity | Varies | Active |
| Best For | Stability & Features | Modern Stacks | Legacy/Scripts | Rapid Prototyping | Visual Encoding |
Security code should be clear, not magical. Using speakeasy or otplib alongside qrcode gives you full visibility into how tokens are generated and how URIs are constructed. This transparency makes it easier to audit your code, rotate secrets, and adapt to future security standards without being locked into a black-box wrapper.
Choose authenticator if you are looking for a straightforward, high-level interface that abstracts away the complexity of TOTP logic for basic use cases. However, verify its current maintenance status carefully, as simpler wrappers often lag behind core crypto libraries in security updates and feature parity compared to speakeasy or otplib.
Choose node-2fa if you want a quick, opinionated wrapper that bundles secret generation, URI creation, and verification into a single, simple function call. It is suitable for rapid prototyping or small services where you do not need fine-grained control over the underlying cryptographic parameters or algorithm selection.
Choose notp only if you are maintaining a legacy system that already depends on it or if you need an extremely minimal, dependency-free implementation for a simple script. Avoid using it for new, complex applications as it lacks the active maintenance, feature depth, and modular extensibility of speakeasy or otplib.
Choose otplib if you prioritize a modern, modular architecture with excellent TypeScript support and a clean, promise-friendly API. It is the best fit for new projects where type safety, tree-shaking, and a clear separation of concerns (plugins for crypto, digest, and authenticator) are critical architectural requirements.
Choose qrcode as the definitive standard for generating QR codes in any JavaScript environment, whether server-side or client-side. It offers the most reliable API for converting TOTP provisioning URIs into scannable images, supporting various output formats like terminal strings, SVG, and PNG data URLs out of the box.
Choose speakeasy if you need a battle-tested, comprehensive library that handles TOTP, HOTP, and counter-based algorithms with extensive configuration options. It is ideal for projects requiring strict adherence to RFC standards and detailed control over token generation, encoding, and verification steps without extra abstraction layers.
| Sponsored by ppl
Two- and Multi- Factor Authenication (2FA / MFA) for node.js

There are a number of apps that various websites use to give you 6-digit codes to increase security when you log in:
There are many Services that Support MFA, including Google, Microsoft, Facebook, and Digital Ocean for starters.
This module uses notp which implements TOTP (RFC 6238)
(the Authenticator standard), which is based on HOTP (RFC 4226)
to provide codes that are exactly compatible with all other Authenticator apps and services that use them.
You may also be interested in
node.js api
npm install authenticator --save
command line
npm install authenticator-cli --global
node.js api
'use strict';
var authenticator = require('authenticator');
var formattedKey = authenticator.generateKey();
// "acqo ua72 d3yf a4e5 uorx ztkh j2xl 3wiz"
var formattedToken = authenticator.generateToken(formattedKey);
// "957 124"
authenticator.verifyToken(formattedKey, formattedToken);
// { delta: 0 }
authenticator.verifyToken(formattedKey, '000 000');
// null
authenticator.generateTotpUri(formattedKey, "john.doe@email.com", "ACME Co", 'SHA1', 6, 30);
//
// otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30
command line
# see help
authenticator --help
# generate a key and display qr code
authenticator --qr
generateKey() // generates a 32-character (160-bit) base32 key
generateToken(formattedKey) // generates a 6-digit (20-bit) decimal time-based token
verifyToken(formattedKey, formattedToken) // validates a time-based token within a +/- 30 second (90 seconds) window
// returns `null` on failure or an object such as `{ delta: 0 }` on success
// generates an `OTPAUTH://` scheme URI for QR Code generation.
generateTotpUri(formattedKey, accountName, issuer, algorithm, digits, period)
OTPAuth Scheme
otpauth://totp/<<ISSUER>>:<<ACCOUNT_NAME>>?secret=<<BASE32_KEY>>&issuer=<<ISSUER>>otpauth://totp/<<ISSUER>>:<<ACCOUNT_NAME>>?secret=<<BASE32_KEY>>&issuer=<<ISSUER>>&algorithm=<<ALGO>>&digits=<<INT>>&period=<<SECONDS>>Note that ISSUER is specified twice for backwards / forwards compatibility.
See https://davidshimjs.github.io/qrcodejs/ and https://github.com/soldair/node-qrcode.

Example use with qrcode.js in the browser:
'use strict';
var el = document.querySelector('.js-qrcode-canvas');
var link = "otpauth://totp/{{NAME}}?secret={{KEY}}";
var name = "Your Service";
// remove spaces, hyphens, equals, whatever
var key = "acqo ua72 d3yf a4e5 uorx ztkh j2xl 3wiz".replace(/\W/g, '').toLowerCase();
var qr = new QRCode(el, {
text: link.replace(/{{NAME}}/g, name).replace(/{{KEY}}/g, key)
});
All non-alphanumeric characters are ignored, so you could just as well use hyphens or periods or whatever suites your use case.
These are just as valid:
0, 1, 8, and 9 also not used (so that base32). To further avoid confusion with O, o, L, l, I, B, and g you may wish to display lowercase instead of uppercase.
TODO: should this library replace 0 with o, 1 with l (or I?), 8 with b, 9 with g, and so on?
The window is set to +/- 1, meaning each token is valid for a total of 90 seconds (-30 seconds, +0 seconds, and +30 seconds) to account for time drift (which should be very rare for mobile devices) and humans who are handicapped or otherwise struggle with quick fine motor skills (like my grandma).
It doesn't use native node crypto and there are open security issues which have been left unaddressed.