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 typeMyApp:[email protected]— label (shown in the app)secret=...— the base32-encoded shared secretissuer=MyApp— the app name (shown in the app)algorithm=SHA1— the HMAC algorithmdigits=6— code lengthperiod=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:
- Generate: Create a secret, show the QR code and URI to the user
- 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?