Multi-Factor Auth
intermediateMulti-factor authentication with TOTP, QR code generation, and backup codes.
mfa2fatotpsecurityauthentication
Tested on⬢20▲16⚛19TS5.9
$ bunx sinew add security/mfaInteractive demo coming soon
1The Problem
Password-only authentication is vulnerable to:
- Credential stuffing attacks
- Phishing
- Password reuse across sites
- Brute force attempts
2The Solution
Add TOTP (Time-based One-Time Passwords) as a second factor using the otpauth library. Includes QR code generation for authenticator apps and backup codes for recovery.
3Files
lib/mfa/totp.ts
lib/mfa/totp.tsTypeScript
import { TOTP, Secret } from "otpauth";
import crypto from "crypto";
const MFA_ISSUER = process.env.NEXT_PUBLIC_APP_NAME || "MyApp";
// Generate a new TOTP secret
export function generateSecret(): string {
const secret = new Secret({ size: 20 });
return secret.base32;
}
// Generate TOTP URI for QR code
export function generateTOTPUri(secret: string, accountName: string): string {
const totp = new TOTP({
issuer: MFA_ISSUER,
label: accountName,
algorithm: "SHA1",
digits: 6,
period: 30,
secret: Secret.fromBase32(secret),
});
return totp.toString();
}
// Verify a TOTP code
export function verifyTOTPCode(secret: string, code: string): boolean {
const totp = new TOTP({
algorithm: "SHA1",
digits: 6,
period: 30,
secret: Secret.fromBase32(secret),
});
// window: 1 accepts +/-1 period (90s total acceptance window).
return totp.validate({ token: code, window: 1 }) !== null;
}
// Server-side key used to HMAC backup codes (set MFA_BACKUP_SECRET, >= 32 chars).
function getBackupCodeKey(): Buffer {
const key = process.env.MFA_BACKUP_SECRET;
if (!key || key.length < 32) {
throw new Error("MFA_BACKUP_SECRET is required (>= 32 chars)");
}
return Buffer.from(key);
}
// Generate backup codes: 8 random bytes (64 bits) each.
export function generateBackupCodes(count = 10): string[] {
const codes: string[] = [];
for (let i = 0; i < count; i++) {
const code = crypto.randomBytes(8).toString("hex").toUpperCase();
codes.push(`${code.slice(0, 4)}-${code.slice(4, 8)}-${code.slice(8, 12)}-${code.slice(12)}`);
}
return codes;
}
// Hash a backup code with a keyed HMAC (not bare SHA-256), so a DB leak alone
// cannot be rainbow-tabled back to the codes.
export function hashBackupCode(code: string): string {
return crypto
.createHmac("sha256", getBackupCodeKey())
.update(code.replace(/-/g, "").toUpperCase())
.digest("hex");
}lib/mfa/qrcode.ts
lib/mfa/qrcode.tsTypeScript
import QRCode from "qrcode";
export async function generateQRCodeDataURL(uri: string): Promise<string> {
return QRCode.toDataURL(uri, {
width: 256,
margin: 2,
color: { dark: "#000000", light: "#FFFFFF" },
});
}app/api/mfa/setup/route.ts
app/api/mfa/setup/route.tsTypeScript
import { NextRequest, NextResponse } from "next/server";
import {
generateSecret,
generateTOTPUri,
generateBackupCodes,
hashBackupCode,
} from "@/lib/mfa/totp";
import { generateQRCodeDataURL } from "@/lib/mfa/qrcode";
export async function POST(req: NextRequest) {
// SECURITY: identify the user from your verified session, not from request
// headers. The "x-user-id"/"x-user-email" headers below are client-controlled
// and let a caller set up MFA for any account, so they MUST be populated (and
// any inbound copy stripped) by your auth layer before this handler runs.
// Replace with your own session lookup, e.g.:
// const session = await auth();
// const userId = session?.user?.id;
// const userEmail = session?.user?.email;
const userId = req.headers.get("x-user-id");
const userEmail = req.headers.get("x-user-email");
if (!userId || !userEmail) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const secret = generateSecret();
const uri = generateTOTPUri(secret, userEmail);
const qrCode = await generateQRCodeDataURL(uri);
const backupCodes = generateBackupCodes(10);
// TODO: Store secret and hashed backup codes in database
// const hashedCodes = backupCodes.map(hashBackupCode);
return NextResponse.json({
secret,
qrCode,
backupCodes, // Show to user once
});
}4Dependencies
$ bun add otpauth qrcode5Configuration
Environment Variables
| Variable | Description | Required |
| ---------------------- | ------------------------------------------- | -------- |
| NEXT_PUBLIC_APP_NAME | App name shown in authenticator | No |
| MFA_BACKUP_SECRET | Key (>= 32 chars) used to HMAC backup codes | Yes |
Database Schema
6Usage
Setup Flow
- User initiates MFA setup
- Generate secret and QR code
- User scans QR code with authenticator app
- User enters code to verify setup
- Store the secret in the database, encrypting it at rest (see the data-encryption pattern); store backup codes as the keyed HMAC hashes from
hashBackupCode
Verify During Login
import { verifyTOTPCode } from "@/lib/mfa/totp";
// After password verification
const mfaSecret = await getUserMFASecret(userId);
if (mfaSecret) {
const isValid = verifyTOTPCode(mfaSecret, userInputCode);
if (!isValid) {
return { error: "Invalid MFA code" };
}
}TypeScript