Loading...
Loading...
> Add an extra layer of security with authenticator apps and backup codes.
Two-factor authentication (2FA) adds an extra step to logging in. After entering your password, you also need to provide a code from your phone. Even if someone steals your password, they can't access your account without your phone. Think of it like a bank vault with two locks - you need both keys to get in.
DESC: Customize the issuer name (shown in authenticator apps) in src/config.js
1// src/config.js23export const config = {4 auth: {5 // This name appears in authenticator apps like:6 // "YourApp (user@example.com)"7 mfaIssuer: "YourApp",8 },9};
The MFA functions are available in src/lib/auth/mfa.ts
1import {2 generateTOTPSecret,3 verifyTOTP,4 generateBackupCodes,5 verifyBackupCode,6 hashBackupCode7} from "@/lib/auth/mfa";89// Generate a new TOTP secret for user setup10const { secret, qrCodeUrl, otpAuthUrl } = await generateTOTPSecret(11 "user@example.com",12 "YourApp" // issuer name13);1415// Verify a 6-digit code from authenticator app16const isValid = verifyTOTP(userCode, secret);1718// Generate 10 backup codes (XXXX-XXXX format)19const backupCodes = generateBackupCodes(10);2021// Hash backup codes before storing in database22const hashedCodes = await Promise.all(23 backupCodes.map(code => hashBackupCode(code))24);2526// Verify a backup code (also handles marking it as used)27const { valid, remainingCodes } = await verifyBackupCode(28 submittedCode,29 user.mfaBackupCodes30);
In your API routes
1// Check if user has 2FA enabled2const user = await prisma.user.findUnique({3 where: { id: session.user.id },4 select: {5 mfaEnabled: true,6 mfaSecret: true,7 },8});910if (user.mfaEnabled) {11 // User has 2FA enabled12 // Require verification before sensitive actions13}1415// For sensitive operations, you might want to require16// re-verification even for logged-in users:17export async function deleteAccount(userId: string, mfaCode: string) {18 const user = await prisma.user.findUnique({19 where: { id: userId },20 });2122 if (user.mfaEnabled) {23 const isValid = verifyTOTP(mfaCode, decrypt(user.mfaSecret));24 if (!isValid) {25 throw new Error("Invalid 2FA code");26 }27 }2829 // Proceed with deletion30}
Backup codes are essential for account recovery. They let users log in even if they lose access to their authenticator app (lost phone, new device, etc.).
Any app that supports TOTP (RFC 6238) works. Popular options include:
They can use one of their 10 backup codes to log in. After logging in, they should:
This is a worst-case scenario. You have a few options:
This is why it's important to tell users to save their backup codes securely!
2FA is opt-in by default. To require it, you'd add middleware that checks if user.mfaEnabled is true and redirects users to set it up if not. This is common for enterprise/admin users.
TOTP secrets are encrypted before storage using your NEXTAUTH SECRET. Backup codes are hashed with SHA-256. Even if your database is compromised, attackers can't use the raw values.
The current implementation covers TOTP (authenticator apps). Future versions may include:
Text message codes as an alternative (requires Twilio integration).
Hardware security keys and biometric authentication (Touch ID, Face ID).