authy, otplib, and speakeasy are Node.js libraries used to implement Two-Factor Authentication (2FA) and Time-based One-Time Passwords (TOTP). authy is the official SDK for Twilio's Authy service, providing a managed solution for SMS and push-based 2FA. otplib is a modern, modular, and zero-dependency library focused strictly on generating and verifying TOTP/HOTP codes according to RFC standards. speakeasy is a long-standing, general-purpose library that supports TOTP, HOTP, and counter-based algorithms, historically popular for its simplicity and Google Authenticator compatibility.
Adding Two-Factor Authentication (2FA) to your application usually means deciding between building it yourself using open standards or buying a managed service. authy, otplib, and speakeasy represent these two paths. Let's look at how they handle the core tasks of generating secrets, creating QR codes, and verifying tokens.
authy acts as a client for Twilio's cloud infrastructure.
// authy: Registering a user via API
const authy = require('authy')(API_KEY);
authy.users.new({ email: 'user@example.com', cell: '1234567890' }, (err, res) => {
if (err) throw err;
// Authy returns an ID; they store the secret internally
console.log(`User ID: ${res.user.id}`);
});
otplib is a pure, self-contained library.
// otplib: Generating a secret locally
import { authenticator } from 'otplib';
const secret = authenticator.generateSecret();
// You must save this 'secret' to your database associated with the user
const token = authenticator.generate(secret);
speakeasy is also a self-contained library, similar to otplib.
otplib, you are responsible for database storage of the shared secret.// speakeasy: Generating a secret locally
const speakeasy = require('speakeasy');
const secret = speakeasy.generateSecret({ length: 20 });
// You must save 'secret.base32' to your database
const token = speakeasy.totp({ secret: secret.base32, encoding: 'base32' });
How you handle the "shared secret" is the biggest difference between these tools.
authy hides the secret from you.
// authy: No secret exposed
// The 'res.user.id' is your reference; the actual cryptographic secret is hidden
authy.users.new({ email: '...' }, (err, res) => {
const authyId = res.user.id;
// Save authyId to your DB, NOT a secret key
});
otplib gives you a raw string to store.
// otplib: You manage the secret
const secret = authenticator.generateSecret();
// Save 'secret' to DB. Ensure this column is encrypted at rest.
await db.users.update({ id: 1, twoFactorSecret: encrypt(secret) });
speakeasy provides an object with multiple formats.
otplib, you bear the responsibility of securing this data.// speakeasy: You manage the secret
const secret = speakeasy.generateSecret({ name: 'MyApp' });
// Save 'secret.base32' to DB
await db.users.update({ id: 1, twoFactorSecret: encrypt(secret.base32) });
Users need to scan a QR code to add your app to their authenticator (like Google Authenticator or Authy App).
authy generates the QR code URL for you.
// authy: Fetch QR code from API
authy.users.qrCode(authyId, (err, res) => {
if (err) throw err;
// Returns a URL to the QR code image hosted by Authy
const imageUrl = res.qr_code;
});
otplib does not generate images directly.
otpauth:// URL string.qrcode) to turn that string into an image.// otplib: Generate URL string, then use a separate library
import { authenticator } from 'otplib';
import QRCode from 'qrcode';
const secret = authenticator.generateSecret();
const otpauth = authenticator.keyuri('user@example.com', 'MyApp', secret);
// Generate the actual image data separately
const qrImageData = await QRCode.toDataURL(otpauth);
speakeasy also provides the URL string, not the image.
google_auth_qr property in the generated secret object for convenience.otplib, you need a separate package to render the final image.// speakeasy: Generate URL string, then use a separate library
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
const secret = speakeasy.generateSecret({ name: 'MyApp:user@example.com' });
// secret.google_auth_qr contains the otpauth:// URL
const qrImageData = await QRCode.toDataURL(secret.google_auth_qr);
When a user types in their 6-digit code, your server must check if it matches.
authy sends the code to the cloud for verification.
// authy: Verify via API call
authy.users.verify({ token: '123456', id: authyId }, (err, res) => {
if (res.token === 'is valid') {
// Login successful
}
});
otplib verifies locally using math.
// otplib: Local verification
const isValid = authenticator.verify({ token: '123456', secret: storedSecret });
if (isValid) {
// Login successful
}
speakeasy also verifies locally.
otplib.// speakeasy: Local verification
const isValid = speakeasy.totp.verify({
secret: storedSecret,
encoding: 'base32',
token: '123456',
window: 1 // Accepts previous/next token for clock drift
});
if (isValid) {
// Login successful
}
You want users to have an app, but also need to send SMS codes if they lose their phone.
authy// authy: Request SMS fallback easily
authy.users.requestSMS(authyId, (err, res) => {
// Twilio sends the code via SMS automatically
});
You are building a dashboard for a bank. Data never leaves the private network.
otplib// otplib: Fully offline verification
const valid = authenticator.verify({ token, secret });
// No network request made
You need to add 2FA to an old Node.js app quickly with minimal config.
speakeasy// speakeasy: Simple setup
const token = speakeasy.totp({ secret: secret.base32, encoding: 'base32' });
| Feature | authy | otplib | speakeasy |
|---|---|---|---|
| Type | Managed Service SDK | Self-Hosted Library | Self-Hosted Library |
| Secret Storage | Twilio Cloud | Your Database | Your Database |
| Verification | API Call (Network) | Local (Math) | Local (Math) |
| SMS Support | ✅ Built-in | ❌ None | ❌ None |
| Dependencies | High (Network reliant) | Zero | Low |
| Best For | Multi-channel 2FA | Modern, secure apps | Legacy or simple apps |
Think about control versus convenience.
authy. You trade control for a feature-rich service that handles the hard parts of delivery.otplib. It is modular, actively maintained, and keeps your data in your hands. It is the standard for new Node.js projects today.speakeasy will get the job done, but for any new major project, otplib is the stronger technical choice due to its modern architecture and active development.Choose authy if you need a managed service that handles SMS delivery, push notifications, and device management without building the infrastructure yourself. It is ideal for applications requiring multi-channel verification (SMS + App) and where you want to offload the complexity of secret storage and delivery to Twilio. Avoid this if you prefer a self-hosted, stateless solution or want to avoid vendor lock-in.
Choose otplib if you need a lightweight, modern, and strictly standards-compliant library for generating and verifying TOTP codes in a stateless manner. It is the best fit for new projects that require high modularity, TypeScript support, and zero external dependencies. Select this when you want full control over the secret generation and storage process without relying on a third-party API.
Choose speakeasy if you are maintaining a legacy system that already relies on it or need a simple, battle-tested library for basic TOTP/HOTP generation. It is suitable for straightforward implementations where you need to generate QR codes and verify tokens without complex modular configuration. However, for new greenfield projects, consider otplib due to speakeasy's slower update cadence and lack of modern modular architecture.

Authy and Verify API Client for Node.js written by Adam Baldwin.
npm install authy
When in doubt check out the official Authy and Verify docs.
var authy = require('authy')('APIKEY');
OneTouch API docs are the source of truth. send_approval_request(id,user_payload,hidden_details,logos,callback)
authy.send_approval_request('1337', user_payload, [hidden_details], [logos], function (err, res) {
// res = {"approval_request":{"uuid":"########-####-####-####-############"},"success":true}
});
check_approval_status (uuid,callback)
authy.check_approval_status(uuid, function(err, res) {
res = {
"approval_request": {
"_app_name": YOUR_APP_NAME,
"_app_serial_id": APP_SERIAL_ID,
"_authy_id": AUTHY_ID,
"_id": INTERNAL_ID,
"_user_email": EMAIL_ID,
"app_id": APP_ID,
"created_at": TIME_STAMP,
"notified": false,
"processed_at": null,
"seconds_to_expire": 600,
"status": 'pending',
"updated_at": TIME_STAMP,
"user_id": USER_ID,
"uuid": UUID
},
"success": true
}
});
register_user(email, cellphone, [country_code], [send_install_link_via_sms], callback);
authy.register_user('baldwin@andyet.net', '509-555-1212', function (err, res) {
// res = {user: {id: 1337}} where 1337 = ID given to use, store this someplace
});
If not given, country_code defaults to "1" and send_install_link_via_sms defaults to true.
verify(id, token, [force], callback);
authy.verify('1337', '0000000', function (err, res) {
});
request_sms(id, [force], callback);
authy.request_sms('1337', function (err, res) {
});
=======
request_call(id, [force], callback);
authy.request_call('1337', function (err, res) {
});
delete_user(id, callback);
authy.delete_user('1337', function (err, res) {
});
user_status(id, callback);
authy.user_status('1337', function (err, res) {
});
Browse the API docs for all available params.
phones().verification_start(phone_number, country_code, params, callback);
authy.phones().verification_start('111-111-1111', '1', { via: 'sms', locale: 'en', code_length: '6' }, function(err, res) {
});
The params argument is optional and sets 'sms' as the default via, leaving the other two options blank.
Browse the API docs for all available params.
phones().verification_check(phone_number, country_code, verification_code, callback);
authy.phones().verification_check('111-111-1111', '1', '0000', function (err, res) {
});
Browse the API docs for all available params.
phones().verification_status(phone_number, country_code, callback);
authy.phones().verification_status('111-111-1111', '1', function (err, res) {
});