A complete guide to integrating Google Authenticator using AuthenticatorAPI.com
This guide covers adding Google Authenticator TOTP two-factor authentication to Node.js applications — from a minimal Express server to a full Next.js or NestJS project. No npm packages required beyond Node's built-in crypto module and the standard fetch API (Node 18+).
fetch). For older Node, use the node-fetch package. A database (PostgreSQL, MongoDB, etc.) to store per-user secrets.// lib/totp.js 'use strict'; const { randomBytes } = require('crypto'); const BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; const API_BASE = 'https://www.authenticatorApi.com'; /** * Generates a cryptographically random 32-character Base32 secret. */ function generateSecret(length = 32) { const bytes = randomBytes(length); return Array.from(bytes, b => BASE32_ALPHABET[b % 32]).join(''); } /** * Returns the HTML img tag for the QR code setup page. */ async function getQrCodeHtml(appName, userInfo, secret) { const params = new URLSearchParams({ AppName: appName, AppInfo: userInfo, SecretCode: secret }); const res = await fetch(`${API_BASE}/pair.aspx?${params}`); if (!res.ok) throw new Error(`Pair API error: ${res.status}`); return res.text(); } /** * Returns true if the 6-digit pin is valid for the given secret. */ async function validatePin(pin, secret) { if (!/^\d{6}$/.test(pin)) return false; const params = new URLSearchParams({ Pin: pin, SecretCode: secret }); const res = await fetch(`${API_BASE}/Validate.aspx?${params}`); if (!res.ok) throw new Error(`Validate API error: ${res.status}`); const text = await res.text(); return text.trim().toLowerCase() === 'true'; } module.exports = { generateSecret, getQrCodeHtml, validatePin };
// routes/2fa.js const express = require('express'); const router = express.Router(); const totp = require('../lib/totp'); const db = require('../lib/db'); // GET /2fa/setup — show the QR code router.get('/setup', async (req, res) => { const secret = totp.generateSecret(); req.session.pending2FASecret = secret; // store until confirmed const qrHtml = await totp.getQrCodeHtml('MyApp', req.user.email, secret); res.render('2fa-setup', { qrHtml, secret }); }); // POST /2fa/setup — confirm the user has scanned correctly router.post('/setup', async (req, res) => { const { pin } = req.body; const secret = req.session.pending2FASecret; if (await totp.validatePin(pin, secret)) { await db.saveEncryptedSecret(req.user.id, secret); delete req.session.pending2FASecret; res.redirect('/dashboard'); } else { res.render('2fa-setup', { error: 'Invalid code, please try again.', qrHtml: await totp.getQrCodeHtml('MyApp', req.user.email, secret), secret }); } }); // POST /2fa/verify — called at login time router.post('/verify', async (req, res) => { const { pin } = req.body; const secret = await db.getDecryptedSecret(req.user.id); if (await totp.validatePin(pin, secret)) { req.session.twoFactorVerified = true; res.redirect('/dashboard'); } else { res.render('2fa-verify', { error: 'Invalid code, please try again.' }); } }); module.exports = router;
// middleware/require2fa.js module.exports = async function require2fa(req, res, next) { if (!req.user) return res.redirect('/login'); const has2fa = await db.userHas2FA(req.user.id); if (has2fa && !req.session.twoFactorVerified) { return res.redirect('/2fa/verify'); } next(); }; // In your router: const require2fa = require('./middleware/require2fa'); app.use('/dashboard', require2fa, dashboardRouter); app.use('/settings', require2fa, settingsRouter);
// app/actions/totp.ts 'use server'; import { randomBytes } from 'crypto'; import { cookies } from 'next/headers'; const BASE32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; export function generateSecret(length = 32): string { const bytes = randomBytes(length); return Array.from(bytes, b => BASE32[b % 32]).join(''); } export async function fetchQrHtml(appName: string, email: string, secret: string) { const params = new URLSearchParams({ AppName: appName, AppInfo: email, SecretCode: secret }); const res = await fetch(`https://www.authenticatorApi.com/pair.aspx?${params}`, { cache: 'no-store' }); return res.text(); } export async function verifyPin(pin: string, secret: string): Promise<boolean> { if (!/^\d{6}$/.test(pin)) return false; const params = new URLSearchParams({ Pin: pin, SecretCode: secret }); const res = await fetch(`https://www.authenticatorApi.com/Validate.aspx?${params}`, { cache: 'no-store' }); return (await res.text()).trim().toLowerCase() === 'true'; }
// app/settings/2fa/page.tsx import { generateSecret, fetchQrHtml } from '@/app/actions/totp'; import TwoFaSetupForm from '@/components/TwoFaSetupForm'; export default async function TwoFaSetupPage() { const secret = generateSecret(); const qrHtml = await fetchQrHtml('MyApp', 'user@example.com', secret); return ( <div> <h1>Set Up Two-Factor Authentication</h1> <div dangerouslySetInnerHTML={{ __html: qrHtml }} /> <p>Manual key: <code>{secret}</code></p> <TwoFaSetupForm secret={secret} /> </div> ); }
crypto.randomBytes() — never Math.random()/2fa/verify route with express-rate-limitreq.session.regenerate())helmet and HTTPS in production