Introduction
Passwords alone aren't enough — implement 2FA with TOTP (Time-based One-Time Password). Generate RFC 6238-compliant implementations compatible with Google Authenticator and Authy using Claude Code.
CLAUDE.md 2FA Rules
## Two-Factor Authentication Design Rules
### TOTP Implementation
- RFC 6238 compliant (TOTP)
- Store secrets encrypted in DB (AES-256-GCM)
- Correctly set issuer and account name in QR code
- Allow ±1 step time drift (30 seconds)
### Backup Codes
- Generate 10 codes of 8 characters each
- Store BCrypt hashed
- Immediately invalidate used codes
- Force regeneration after recovery use
### Security
- Rate limit TOTP verification (lock for 15 min after 5 failures)
- Don't enable 2FA until setup is confirmed
- Require current password + 2FA code to disable 2FA
Generated 2FA Implementation
// src/auth/twoFactor.ts
const ENCRYPTION_KEY = Buffer.from(process.env.TOTP_ENCRYPTION_KEY!, 'hex');
function encryptSecret(secret: string): string {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-gcm', ENCRYPTION_KEY, iv);
const encrypted = Buffer.concat([cipher.update(secret, 'utf8'), cipher.final()]);
const authTag = cipher.getAuthTag();
return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted.toString('hex')}`;
}
export async function setup2FA(userId: string, userEmail: string) {
const secret = authenticator.generateSecret(20);
await prisma.twoFactorSetup.deleteMany({ where: { userId } });
await prisma.twoFactorSetup.create({
data: { userId, encryptedSecret: encryptSecret(secret), expiresAt: new Date(Date.now() + 600_000) },
});
const otpauthUrl = authenticator.keyuri(userEmail, process.env.APP_NAME ?? 'MyApp', secret);
const qrCodeDataUrl = await QRCode.toDataURL(otpauthUrl);
return { qrCodeDataUrl, manualEntryKey: secret };
}
export async function confirm2FA(userId: string, totpCode: string) {
const setup = await prisma.twoFactorSetup.findUnique({ where: { userId } });
if (!setup || setup.expiresAt < new Date()) throw new ValidationError('2FA setup expired');
const secret = decryptSecret(setup.encryptedSecret);
if (!authenticator.check(totpCode, secret)) throw new ValidationError('Invalid TOTP code');
const plainBackupCodes = Array.from({ length: 10 }, () =>
crypto.randomBytes(4).toString('hex').toUpperCase()
);
const hashedBackupCodes = await Promise.all(plainBackupCodes.map(code => bcrypt.hash(code, 10)));
await prisma.$transaction(async (tx) => {
await tx.twoFactorSetup.delete({ where: { userId } });
await tx.twoFactor.upsert({ where: { userId }, create: { userId, encryptedSecret: setup.encryptedSecret, enabledAt: new Date() }, update: { encryptedSecret: setup.encryptedSecret, enabledAt: new Date() } });
await tx.backupCode.deleteMany({ where: { userId } });
await tx.backupCode.createMany({ data: hashedBackupCodes.map(hash => ({ userId, codeHash: hash, usedAt: null })) });
await tx.user.update({ where: { id: userId }, data: { twoFactorEnabled: true } });
});
return { backupCodes: plainBackupCodes };
}
export async function verify2FA(userId: string, code: string): Promise<void> {
const rateKey = `2fa:attempts:${userId}`;
const attempts = await redis.incr(rateKey);
await redis.expire(rateKey, 900);
if (attempts > 5) throw new TooManyRequestsError('Too many failed 2FA attempts. Try again in 15 minutes.');
const twoFactor = await prisma.twoFactor.findUnique({ where: { userId } });
if (!twoFactor) throw new ValidationError('2FA not enabled');
const secret = decryptSecret(twoFactor.encryptedSecret);
if (authenticator.check(code, secret)) { await redis.del(rateKey); return; }
const backupCodes = await prisma.backupCode.findMany({ where: { userId, usedAt: null } });
for (const bc of backupCodes) {
if (await bcrypt.compare(code, bc.codeHash)) {
await prisma.backupCode.update({ where: { id: bc.id }, data: { usedAt: new Date() } });
await redis.del(rateKey);
return;
}
}
throw new ValidationError('Invalid 2FA code');
}
Login Flow Integration
router.post('/login', async (req, res) => {
const user = await verifyPassword(req.body.email, req.body.password);
if (user.twoFactorEnabled) {
const tempToken = await createTempToken(user.id, '2fa_pending');
return res.json({ requiresTwoFactor: true, tempToken });
}
res.json(await generateTokenPair(user.id));
});
router.post('/login/2fa', async (req, res) => {
const userId = await verifyTempToken(req.body.tempToken, '2fa_pending');
if (!userId) return res.status(401).json({ error: 'Invalid token' });
await verify2FA(userId, req.body.code);
res.json(await generateTokenPair(userId));
});
Summary
Design 2FA (TOTP) with Claude Code:
- CLAUDE.md — RFC 6238, encrypt secrets, 10 backup codes, 5-attempt rate limit
- AES-256-GCM for TOTP secret encryption in DB (plaintext forbidden)
- Confirm first TOTP code before activating 2FA
- BCrypt backup codes — immediately invalidate used codes
Review 2FA security designs with **Security Pack (¥1,480)* using /security-check at prompt-works.jp*
myouga (@myougatheaxo) — Axolotl VTuber.
Top comments (0)