patterntypescriptMajor
TOTP Multi-Factor Authentication Integration
Viewed 0 times
otplib v12
totpmfa2faotplibauthenticatorshared secretqr coderecovery codes
Problem
Adding TOTP-based MFA (Google Authenticator, Authy) requires generating a shared secret, presenting a QR code for enrollment, validating time-based codes, and protecting the MFA flow itself from being bypassed.
Solution
Use the otplib library to generate a secret, create a TOTP URI for the QR code, and verify submitted codes. Gate the MFA verification step as a distinct session state between credential verification and full session establishment.
Why
TOTP codes are time-synchronized one-time passwords derived from a shared secret and the current 30-second time window. Verification must happen before issuing a full authenticated session to prevent credential-only bypass.
Gotchas
- Store the TOTP secret encrypted in the database — it is a long-lived credential equivalent to a password
- Allow a window of ±1 time step (30s) to tolerate clock skew between user device and server
- Provide recovery codes (single-use) at enrollment time in case the user loses their authenticator
- Never reveal whether an account has MFA enabled to unauthenticated users — it leaks account existence
Code Snippets
TOTP setup and verification with otplib
import { authenticator } from 'otplib';
import qrcode from 'qrcode';
// Enrollment: generate secret and QR code URI
export async function setupMfa(userId: string, userEmail: string) {
const secret = authenticator.generateSecret();
const otpUri = authenticator.keyuri(userEmail, 'MyApp', secret);
const qrCodeDataUrl = await qrcode.toDataURL(otpUri);
// Store secret encrypted; mark MFA as pending until first verification
await db.user.update({
where: { id: userId },
data: { totpSecret: encrypt(secret), mfaEnabled: false },
});
return { qrCodeDataUrl, secret };
}
// Verification
export function verifyTotp(secret: string, code: string): boolean {
authenticator.options = { window: 1 }; // ±1 step tolerance
return authenticator.verify({ token: code, secret });
}Revisions (0)
No revisions yet.