hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses Two-Factor and Passwordless Auth with @hectoday/http

Why Passwords Are Not Enough

  • The Problem with Passwords
  • Project Setup

TOTP (Time-Based One-Time Passwords)

  • How TOTP Works
  • Generating Secrets and QR Codes
  • Enabling 2FA on an Account
  • Verifying TOTP on Login
  • Time Windows and Clock Drift

Recovery

  • Recovery Codes
  • Disabling 2FA
  • Account Recovery When Everything Is Lost

Magic Links

  • How Magic Links Work
  • Building Magic Link Login
  • Security Considerations

WebAuthn and Passkeys

  • What Are Passkeys?
  • Registration Flow
  • Authentication Flow
  • Passkeys as Second Factor or Primary

Putting It All Together

  • Multi-Method Auth
  • Auth Method Checklist and Capstone

Generating Secrets and QR Codes

The otpauth:// URI

Authenticator apps understand a standard URI format for importing TOTP secrets:

otpauth://totp/MyApp:[email protected]?secret=JBSWY3DPEHPK3PXP&issuer=MyApp&algorithm=SHA1&digits=6&period=30

The parts:

  • otpauth://totp/ — protocol and type
  • MyApp:[email protected] — label (shown in the app)
  • secret=... — the base32-encoded shared secret
  • issuer=MyApp — the app name (shown in the app)
  • algorithm=SHA1 — the HMAC algorithm
  • digits=6 — code length
  • period=30 — seconds per code

When this URI is encoded as a QR code, the user scans it with their authenticator app, and the secret is imported automatically.

Generating a secret

Using the otpauth library:

// src/totp.ts
import { TOTP } from "otpauth";
import QRCode from "qrcode";

const ISSUER = "HectodayCourse";

export function createTOTP(email: string, secret?: string): TOTP {
  return new TOTP({
    issuer: ISSUER,
    label: email,
    algorithm: "SHA1",
    digits: 6,
    period: 30,
    secret: secret ?? undefined,
    // If no secret provided, the library generates a random one
  });
}

export function generateSecret(email: string): { secret: string; uri: string } {
  const totp = createTOTP(email);
  return {
    secret: totp.secret.base32,
    uri: totp.toString(),
  };
}

export async function generateQRCode(uri: string): Promise<string> {
  return QRCode.toDataURL(uri);
}

generateSecret creates a new TOTP instance with a random secret and returns the base32-encoded secret and the otpauth:// URI. generateQRCode converts the URI into a QR code data URL (a base64-encoded PNG image that can be embedded in HTML or returned as JSON).

Verifying a code

export function verifyTOTPCode(secret: string, code: string): boolean {
  const totp = createTOTP("", secret);
  const delta = totp.validate({ token: code, window: 1 });
  return delta !== null;
}

validate returns the time step delta (0 for current window, -1 for previous, +1 for next) or null if the code is invalid. Setting window: 1 accepts codes from the current window and one window on each side (we cover why in the time windows lesson).

The full setup flow

The setup flow has two steps:

  1. Generate: Create a secret, show the QR code and URI to the user
  2. Confirm: The user scans the QR code and enters a code from their authenticator. If the code is valid, the secret is stored and 2FA is enabled.

This two-step process ensures the user has a working authenticator before 2FA is enabled. Without the confirmation step, a user could enable 2FA and immediately be locked out because they never set up their authenticator.

Exercises

Exercise 1: Call generateSecret("[email protected]") and print the result. You should see a base32 secret and an otpauth:// URI.

Exercise 2: Paste the otpauth:// URI into a QR code generator (or use the generateQRCode function) and scan it with Google Authenticator. The app should show a 6-digit code.

Exercise 3: Use verifyTOTPCode with the secret and the code from your authenticator. It should return true.

Why do we return the secret and the QR code instead of just the QR code?

← How TOTP Works Enabling 2FA on an Account →

© 2026 hectoday. All rights reserved.