Tech 3 min read

Implement TOTP authentication such as Google Authenticator in your own service

IkesanContents

Google Authenticator and Microsoft Authenticator are not apps for Google or Microsoft only. They are apps that support the TOTP (Time-based One-Time Password) standard (RFC 6238), and you can use them in your own service too.

How TOTP Works

HMAC-SHA1(secret key, floor(current time / 30)) -> 6-digit number
  • The server and the app share the same secret key
  • The current time divided into 30-second intervals is used as the counter
  • The same input always produces the same output, so both sides generate the same code

In short, if the server and the authenticator app share the same “secret” and the same time, they can calculate the same code without any network exchange.

Enrollment Flow

  1. The user requests that 2FA be enabled
  2. The server generates a secret key (Base32 encoded)
  3. A QR code is displayed so the user can scan it with an authenticator app
  4. The user enters the verification code
  5. If verification succeeds, the secret is stored in the database

The QR code contains a URI in this format:

otpauth://totp/service-name:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=service-name

Authentication Flow

  1. The username/password check succeeds
  2. Users with 2FA enabled are prompted for a 6-digit code
  3. The server calculates the current code from the secret stored in the database
  4. If the code matches, the login succeeds

Example Implementation in TypeScript

Use otplib.

npm install otplib qrcode

Secret Generation and QR Code Display

import { authenticator } from 'otplib';
import * as QRCode from 'qrcode';

// Generate a secret
const secret = authenticator.generateSecret();
// => something like "KVKFKRCPNZQUYMLXOVYDSQKJKZDTSRLD"

// Generate the URI used for the QR code
const otpauth = authenticator.keyuri('user@example.com', 'MyService', secret);
// => "otpauth://totp/MyService:user@example.com?secret=KVKF...&issuer=MyService"

// Generate the QR code as a Data URL
const qrDataUrl = await QRCode.toDataURL(otpauth);
// => display it in the frontend as <img src={qrDataUrl} />

Code Verification

import { authenticator } from 'otplib';

function verifyTOTP(secret: string, token: string): boolean {
  return authenticator.verify({ token, secret });
}

// Example
const isValid = verifyTOTP(userSecretFromDB, '123456');

otplib allows a tolerance of one step before and after the current time by default (±30 seconds). That helps with clock drift.

Recovery Codes

These are a backup in case the authenticator app can no longer be used. Show them only once during enrollment and ask the user to save them.

import { randomBytes } from 'crypto';

function generateRecoveryCodes(count = 10): string[] {
  return Array.from({ length: count }, () =>
    randomBytes(4).toString('hex').toUpperCase()
  );
}
// => ["A1B2C3D4", "E5F6G7H8", ...] - ten 8-character codes
  • Store them in the database as hashes
  • Invalidate codes after use so they can be used only once
  • Prompt the user to regenerate them once they are all used up

Security Notes

  • Encrypt the secret key before storing it in the database
  • Show the QR code only temporarily and do not cache it
  • Add brute-force protection with retry limits
  • Always hash recovery codes

Summary

  • TOTP is a standard, so it works with any authenticator app
  • otplib makes the implementation simple
  • Do not forget to provide recovery codes