authy vs otplib vs speakeasy
Implementing Two-Factor Authentication in Node.js Applications
authyotplibspeakeasySimilar Packages:

Implementing Two-Factor Authentication in Node.js Applications

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.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
authy09622.5 kB5-MIT
otplib02,290612 kB1019 days agoMIT
speakeasy02,755-6611 years agoMIT

Implementing 2FA: Authy vs OTPlib vs Speakeasy

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.

🏗️ Architecture: Managed Service vs. Self-Hosted Library

authy acts as a client for Twilio's cloud infrastructure.

  • You do not generate or store the secret keys yourself; Authy does it.
  • Your server talks to Authy's API to register users and verify codes.
  • This shifts the security burden of key storage to Twilio but introduces a network dependency.
// 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.

  • You generate the secret, store it in your own database, and verify codes locally.
  • No external API calls are needed during login, making it faster and cheaper at scale.
  • It gives you full ownership of the security model.
// 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.

  • It handles secret generation and verification entirely on your server.
  • It has been around longer and uses a slightly older coding style but functions similarly.
  • Like 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' });

🔑 Secret Generation and Storage

How you handle the "shared secret" is the biggest difference between these tools.

authy hides the secret from you.

  • When you register a user, Authy generates the key and stores it on their servers.
  • You only get a User ID.
  • This is safer if you are worried about leaking your database, but you cannot easily migrate away from Authy later.
// 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.

  • It generates a random Base32 string.
  • You must encrypt this column in your database because anyone with this string can generate valid 2FA codes.
  • This allows you to switch libraries or providers later if needed.
// 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.

  • It returns the secret in Base32, Hex, and ASCII.
  • Most apps use the Base32 version for compatibility with Google Authenticator.
  • Like 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) });

📱 QR Code Generation for Setup

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.

  • You ask the API for the QR code, and it returns an image URL or barcode string.
  • This ensures the QR code format is always correct for the Authy app.
// 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.

  • It provides a helper to create the otpauth:// URL string.
  • You must use a separate library (like qrcode) to turn that string into an image.
  • This adds a step but gives you full control over the image styling.
// 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.

  • It includes a google_auth_qr property in the generated secret object for convenience.
  • Like 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);

✅ Verifying Tokens During Login

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.

  • Your server sends the token and User ID to Twilio.
  • Twilio checks it and tells you yes or no.
  • This adds network latency to every login attempt.
// 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.

  • Your server takes the stored secret and the user's input.
  • It calculates the expected code and compares them instantly.
  • This is extremely fast and works even if your server has no internet access.
// otplib: Local verification
const isValid = authenticator.verify({ token: '123456', secret: storedSecret });
if (isValid) {
  // Login successful
}

speakeasy also verifies locally.

  • It uses the same TOTP algorithm as otplib.
  • It allows you to adjust the "window" of time (e.g., accept codes from 30 seconds ago) to handle clock skew.
// 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
}

🌐 Real-World Scenarios

Scenario 1: SaaS Startup Needing SMS Fallback

You want users to have an app, but also need to send SMS codes if they lose their phone.

  • Best choice: authy
  • Why? It handles both App tokens and SMS delivery in one API. Building SMS logic yourself is complex and expensive.
// authy: Request SMS fallback easily
authy.users.requestSMS(authyId, (err, res) => {
  // Twilio sends the code via SMS automatically
});

Scenario 2: High-Security Internal Tool

You are building a dashboard for a bank. Data never leaves the private network.

  • Best choice: otplib
  • Why? It requires no external API calls. You keep the secrets in your encrypted DB, ensuring zero data leakage to third parties.
// otplib: Fully offline verification
const valid = authenticator.verify({ token, secret });
// No network request made

Scenario 3: Quick Prototype or Legacy Maintenance

You need to add 2FA to an old Node.js app quickly with minimal config.

  • Best choice: speakeasy
  • Why? It has simple documentation and works out of the box with many existing tutorials. Good for getting something running fast.
// speakeasy: Simple setup
const token = speakeasy.totp({ secret: secret.base32, encoding: 'base32' });

📌 Summary Table

Featureauthyotplibspeakeasy
TypeManaged Service SDKSelf-Hosted LibrarySelf-Hosted Library
Secret StorageTwilio CloudYour DatabaseYour Database
VerificationAPI Call (Network)Local (Math)Local (Math)
SMS Support✅ Built-in❌ None❌ None
DependenciesHigh (Network reliant)ZeroLow
Best ForMulti-channel 2FAModern, secure appsLegacy or simple apps

💡 Final Recommendation

Think about control versus convenience.

  • Need SMS, Push, and easy setup? → Choose authy. You trade control for a feature-rich service that handles the hard parts of delivery.
  • Building a modern, secure app from scratch? → Choose otplib. It is modular, actively maintained, and keeps your data in your hands. It is the standard for new Node.js projects today.
  • Maintaining older code or need a quick fix?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.

How to Choose: authy vs otplib vs speakeasy

  • authy:

    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.

  • otplib:

    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.

  • speakeasy:

    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.

README for authy

node-authy Dependency Status

Authy and Verify API Client for Node.js written by Adam Baldwin.

Installation

npm install authy

When in doubt check out the official Authy and Verify docs.

Usage

Requiring node-authy

var authy = require('authy')('APIKEY');

Send OneTouch

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}
});
  • id is the Authy id.
  • user_payload: { 'message': 'user message here', ['details': {...}] }
  • hidden_details: optional
  • logos: optional

Check Approval Status

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 New User

User API Information

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 Token

verify(id, token, [force], callback);

authy.verify('1337', '0000000', function (err, res) {

});

Request SMS

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 Registered User

delete_user(id, callback);

authy.delete_user('1337', function (err, res) {

});

Get Registered User Status

user_status(id, callback);

authy.user_status('1337', function (err, res) {

});

Start Phone Verification

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.

Check Phone Verification

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) {

});

Status of Phone Verification

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) {

});
Contributors