feat: 2fa backup codes#1
Conversation
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
There was a problem hiding this comment.
Pull request overview
This PR implements backup codes as a secondary two-factor authentication method, providing users with a recovery option if they lose access to their authenticator app.
Changes:
- Added backup code generation and storage in the database with encryption
- Implemented backup code authentication flow for login and 2FA disabling
- Added UI components for displaying, copying, and downloading backup codes
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/prisma/schema.prisma | Added backupCodes field to User model |
| packages/prisma/migrations/20230804153419_add_backup_codes/migration.sql | Database migration to add backup codes column |
| packages/lib/test/builder.ts | Updated test user builder with backupCodes field |
| packages/features/auth/lib/next-auth-options.ts | Implemented backup code verification logic in login flow |
| packages/features/auth/lib/ErrorCode.ts | Added error codes for backup code validation |
| apps/web/public/static/locales/en/common.json | Added translations for backup code UI elements |
| apps/web/pages/auth/login.tsx | Added "Lost Access" UI and backup code input handling |
| apps/web/pages/api/auth/two-factor/totp/setup.ts | Generate backup codes during 2FA setup |
| apps/web/pages/api/auth/two-factor/totp/disable.ts | Allow 2FA disabling with backup codes |
| apps/web/components/settings/TwoFactorAuthAPI.ts | Updated API to accept backup codes |
| apps/web/components/settings/EnableTwoFactorModal.tsx | Added backup code display step in setup flow |
| apps/web/components/settings/DisableTwoFactorModal.tsx | Added backup code option for disabling 2FA |
| apps/web/components/auth/TwoFactor.tsx | Added autoFocus prop for better UX |
| apps/web/components/auth/BackupCode.tsx | New component for backup code input |
| packages/ui/components/form/inputs/Input.tsx | Added tabIndex={-1} to password visibility toggle |
| apps/web/playwright/login.2fa.e2e.ts | Added tests for backup code functionality |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import { useLocale } from "@calcom/lib/hooks/useLocale"; | ||
| import { Label, TextField } from "@calcom/ui"; | ||
|
|
||
| export default function TwoFactor({ center = true }) { |
There was a problem hiding this comment.
The function name 'TwoFactor' does not match the component's purpose. It should be named 'BackupCode' to reflect that it handles backup code input.
| export default function TwoFactor({ center = true }) { | |
| export default function BackupCode({ center = true }) { |
|
|
||
| test.describe.configure({ mode: "parallel" }); | ||
|
|
||
| // TODO: add more backup code tests, e.g. login + disabling 2fa with backup |
There was a problem hiding this comment.
Missing test coverage for critical backup code scenarios including login with backup codes and disabling 2FA using backup codes.
| const textBlob = new Blob([body.backupCodes.map(formatBackupCode).join("\n")], { | ||
| type: "text/plain", | ||
| }); | ||
| if (backupCodesUrl) URL.revokeObjectURL(backupCodesUrl); |
There was a problem hiding this comment.
Memory leak: The backup codes URL is created but never revoked when the component unmounts. Add cleanup in a useEffect to revoke the URL on unmount.
| // delete verified backup code and re-encrypt remaining | ||
| backupCodes[index] = null; | ||
| await prisma.user.update({ | ||
| where: { | ||
| id: user.id, | ||
| }, | ||
| data: { | ||
| backupCodes: symmetricEncrypt(JSON.stringify(backupCodes), process.env.CALENDSO_ENCRYPTION_KEY), |
There was a problem hiding this comment.
Setting used backup codes to null creates sparse arrays. Consider filtering out used codes or using a different data structure to maintain a clean array of valid codes.
| // delete verified backup code and re-encrypt remaining | |
| backupCodes[index] = null; | |
| await prisma.user.update({ | |
| where: { | |
| id: user.id, | |
| }, | |
| data: { | |
| backupCodes: symmetricEncrypt(JSON.stringify(backupCodes), process.env.CALENDSO_ENCRYPTION_KEY), | |
| // remove verified backup code and re-encrypt remaining valid codes | |
| const updatedBackupCodes = backupCodes.filter((_, i) => i !== index); | |
| await prisma.user.update({ | |
| where: { | |
| id: user.id, | |
| }, | |
| data: { | |
| backupCodes: symmetricEncrypt( | |
| JSON.stringify(updatedBackupCodes), | |
| process.env.CALENDSO_ENCRYPTION_KEY | |
| ), |
Test 3
Summary by CodeRabbit
Release Notes
✏️ Tip: You can customize this high-level summary in your review settings.
Replicated from ai-code-review-evaluation/cal.com-coderabbit#3