How to Implement Two-Factor Authentication in a Node.js App with TOTP

How to Implement Two-Factor Authentication in a Node.js App with TOTP

by | Aug 24, 2026 | Uncategorized | 0 comments

Adding two factor authentication in Node.js is one of the most effective ways to protect user accounts from credential leaks, phishing, and brute-force attacks. In this practical tutorial, we will build a complete TOTP-based 2FA flow using Express, speakeasy, and qrcode. By the end, your users will be able to scan a QR code with Google Authenticator, Microsoft Authenticator, or Authy, and confirm their identity with a six-digit code.

Unlike solutions that rely on SMS or paid third-party APIs, TOTP is free, offline, and works with any authenticator app. Let’s get into it.

What Is TOTP and Why Use It for 2FA?

TOTP stands for Time-based One-Time Password (RFC 6238). It generates a short numeric code that changes every 30 seconds, derived from a shared secret and the current timestamp. Because the code is generated on the user’s device, no network call is needed to validate it.

TOTP vs Other 2FA Methods

Method Cost Security User Experience
TOTP (Speakeasy) Free High Excellent (offline)
SMS Codes Paid per SMS Vulnerable to SIM swap Good
Email OTP Low Medium Depends on inbox
Hardware Keys (FIDO2) Hardware cost Very high Requires device
two factor authentication phone code

Prerequisites

  • Node.js 20 or newer installed
  • Basic knowledge of Express and REST APIs
  • An authenticator app on your phone (Google Authenticator, Authy, 1Password, etc.)
  • A database or in-memory store for user data (we will simulate one)

Step 1: Project Setup

Create a new folder and initialize the project:

mkdir node-2fa-totp
cd node-2fa-totp
npm init -y
npm install express speakeasy qrcode
npm install --save-dev nodemon

Update package.json to include a start script:

"scripts": {
  "dev": "nodemon server.js"
}
two factor authentication phone code

Step 2: Build the Express Server Skeleton

Create a server.js file:

const express = require('express');
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');

const app = express();
app.use(express.json());

// Fake in-memory user store. Replace with your DB.
const users = {};

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});

Step 3: Generate a TOTP Secret and QR Code

When a user enables 2FA, we generate a unique secret and turn it into a QR code that the authenticator app can scan.

app.post('/2fa/setup', async (req, res) => {
  const { userId } = req.body;

  // Generate a base32 secret
  const secret = speakeasy.generateSecret({
    name: `BoxSoftware (${userId})`,
    issuer: 'BoxSoftware'
  });

  // Save the secret to your DB, but keep 2fa disabled until verified
  users[userId] = {
    tempSecret: secret.base32,
    twoFactorEnabled: false
  };

  try {
    const qrDataUrl = await QRCode.toDataURL(secret.otpauth_url);
    res.json({
      message: 'Scan this QR code with your authenticator app',
      qrCode: qrDataUrl,
      manualEntryKey: secret.base32
    });
  } catch (err) {
    res.status(500).json({ error: 'Could not generate QR code' });
  }
});

The otpauth_url follows the standard otpauth://totp/ format, so it works with any TOTP-compatible app. There’s a good explainer over at plainenglish.io.

Step 4: Verify the Token and Enable 2FA

After the user scans the QR code, they must enter the current code to confirm the setup worked. This prevents locking themselves out.

app.post('/2fa/verify', (req, res) => {
  const { userId, token } = req.body;
  const user = users[userId];

  if (!user || !user.tempSecret) {
    return res.status(400).json({ error: 'No 2FA setup in progress' });
  }

  const verified = speakeasy.totp.verify({
    secret: user.tempSecret,
    encoding: 'base32',
    token,
    window: 1
  });

  if (!verified) {
    return res.status(400).json({ error: 'Invalid token' });
  }

  // Promote temp secret to permanent
  user.secret = user.tempSecret;
  delete user.tempSecret;
  user.twoFactorEnabled = true;

  res.json({ message: '2FA enabled successfully' });
});

The window: 1 option allows a small clock drift between server and phone (one 30-second step before and after).

two factor authentication phone code

Step 5: Enforce 2FA During Login

Now integrate the verification into your login flow. After the password check, if 2FA is enabled, require a token:

app.post('/login', (req, res) => {
  const { userId, password, token } = req.body;

  // 1. Validate password (mocked here)
  const user = users[userId];
  if (!user) return res.status(401).json({ error: 'Invalid credentials' });

  // 2. If 2FA is enabled, verify token
  if (user.twoFactorEnabled) {
    if (!token) {
      return res.status(401).json({ error: '2FA token required' });
    }

    const valid = speakeasy.totp.verify({
      secret: user.secret,
      encoding: 'base32',
      token,
      window: 1
    });

    if (!valid) return res.status(401).json({ error: 'Invalid 2FA token' });
  }

  // 3. Issue JWT / session
  res.json({ message: 'Logged in', session: 'fake-jwt-token' });
});

Step 6: Test the Full Flow

  1. Start the server with npm run dev
  2. Call POST /2fa/setup with a userId
  3. Open the returned qrCode data URL in your browser and scan it with your authenticator
  4. Call POST /2fa/verify with the 6-digit code shown in the app
  5. Try POST /login with and without a token to see the enforcement
two factor authentication phone code

Production Best Practices

The code above works, but before shipping to production, harden it with these steps:

  • Encrypt TOTP secrets at rest. Use a KMS or a strong symmetric key. Never store secrets in plain text.
  • Generate backup codes. Give users 8 to 10 one-time recovery codes in case they lose their device. Hash them like passwords.
  • Rate-limit verification endpoints. TOTP has only 1,000,000 possible codes. Without throttling, brute force is realistic.
  • Log 2FA events. Track enable, disable, and failed attempts for security auditing.
  • Require re-authentication before allowing 2FA to be disabled.
  • Use HTTPS everywhere. TOTP secrets travel during setup and must never leak.

Common Mistakes to Avoid

  • Storing the temporary secret in the same column as the confirmed one, which can activate 2FA without verification
  • Forgetting to set the window option, causing false negatives due to clock drift
  • Returning the raw base32 secret in login responses (only return it during initial setup)
  • Not offering a fallback recovery method, resulting in permanent account lockouts

FAQ

Is Speakeasy still maintained in 2026?

Speakeasy remains one of the most downloaded TOTP libraries for Node.js. If you prefer a more actively updated alternative, otplib and node-2fa follow the same RFC 6238 standard and are drop-in compatible with any authenticator app.

Can I use this with Google Authenticator and Microsoft Authenticator?

Yes. Any app that supports the otpauth:// URI standard will work, including Google Authenticator, Microsoft Authenticator, Authy, 1Password, Bitwarden, and Duo. See stytch.com for their take.

What happens if the user loses their phone?

Without a recovery method, they lose access. Always issue backup codes during setup, or allow admin-assisted reset with strong identity proof.

How is TOTP different from HOTP?

HOTP is counter-based, meaning it increments each time a code is used. TOTP is time-based and rotates every 30 seconds. TOTP is the modern standard for user-facing 2FA. We break it down further here.

Should I use SMS-based 2FA instead?

SMS 2FA is better than nothing, but it is vulnerable to SIM swap attacks and adds recurring cost. TOTP is more secure, free, and offline.

Conclusion

You now have a working two factor authentication system in Node.js using TOTP, Speakeasy, and QR codes. The full flow covers secret generation, QR display, verification, and enforcement during login. Add encrypted storage, backup codes, and rate limiting, and you have a production-grade 2FA layer that dramatically reduces account takeover risk.

At BoxSoftware, we help teams design and implement secure authentication systems for Node.js, including passwordless login, WebAuthn, and enterprise SSO. Reach out if you would like a security review of your current auth stack.