feat: 2fa backup codes - #1
Conversation
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
There was a problem hiding this comment.
Walkthrough
This PR implements backup code functionality for two-factor authentication (2FA) across the Cal.com application. Users can now generate 10 single-use backup codes during 2FA setup, which can be downloaded or copied to clipboard. These backup codes serve as an alternative authentication method when the primary TOTP authenticator is unavailable. The implementation includes UI components for backup code input, API endpoints for generation and validation, database schema changes to store encrypted backup codes, and comprehensive integration across login, 2FA setup, and 2FA disable flows. End-to-end tests verify the download and copy functionality, and localization strings support the new user-facing features.
Changes
| File(s) | Summary |
|---|---|
packages/prisma/schema.prismapackages/prisma/migrations/20230804153419_add_backup_codes/migration.sql |
Added backupCodes field to the User model as an optional TEXT column to store encrypted backup authentication codes. |
packages/features/auth/lib/ErrorCode.ts |
Added two new error codes: IncorrectBackupCode and MissingBackupCodes for backup code validation. |
apps/web/components/auth/BackupCode.tsx |
Created new React component for backup code input with form validation (10-11 characters) and optional centering layout. |
apps/web/components/auth/TwoFactor.tsx |
Added optional autoFocus prop (default true) to control auto-focus behavior on the first input field. |
apps/web/components/settings/EnableTwoFactorModal.tsx |
Added backup codes display step after 2FA setup with copy-to-clipboard and download functionality, replaced TextField with PasswordField for password input. |
apps/web/components/settings/DisableTwoFactorModal.tsx |
Implemented backup code support as alternative authentication method with "Lost Access" toggle between TOTP and backup code modes. |
apps/web/components/settings/TwoFactorAuthAPI.ts |
Modified disable method to accept backupCode parameter and include it in the API request payload. |
apps/web/pages/api/auth/two-factor/totp/setup.ts |
Added generation of 10 cryptographically secure backup codes, encrypted storage in database, and inclusion in API response. |
apps/web/pages/api/auth/two-factor/totp/disable.ts |
Enhanced disable endpoint to validate backup codes as alternative to TOTP codes, with decryption and matching logic, and cleanup of backup codes on disable. |
apps/web/pages/auth/login.tsx |
Implemented backup code authentication flow with lost access toggle, new backupCode field, conditional component rendering, and error handling for backup code errors. |
packages/features/auth/lib/next-auth-options.ts |
Added backup code credential field, validation logic with decryption, and removal of used backup codes from encrypted list upon successful authentication. |
apps/web/public/static/locales/en/common.json |
Added seven i18n keys for backup code labels, instructions, success/error messages, and warnings. |
apps/web/playwright/login.2fa.e2e.ts |
Added E2E tests for backup code download, copy-to-clipboard functionality, and dialog closure with TODO for additional scenarios. |
packages/lib/test/builder.ts |
Added backupCodes property initialized to null in buildUser test helper function. |
packages/ui/components/form/inputs/Input.tsx |
Added tabIndex={-1} to password visibility toggle button to exclude it from keyboard tab navigation. |
Sequence Diagram
This diagram shows the interactions between components:
sequenceDiagram
actor User
participant TwoFactor as TwoFactor Component
participant FormContext as React Hook Form Context
participant LocaleHook as useLocale Hook
participant TextField as TextField Component
User->>TwoFactor: Render component (center prop)
activate TwoFactor
TwoFactor->>LocaleHook: useLocale()
activate LocaleHook
LocaleHook-->>TwoFactor: Return translation function (t)
deactivate LocaleHook
TwoFactor->>FormContext: useFormContext()
activate FormContext
FormContext-->>TwoFactor: Return form methods (register, etc.)
deactivate FormContext
Note over TwoFactor: Render UI with translations:<br/>- backup_code label<br/>- backup_code_instructions text
TwoFactor->>TextField: Render with props
activate TextField
Note over TextField: Props: id="backup-code"<br/>placeholder="XXXXX-XXXXX"<br/>minLength=10, maxLength=11<br/>required=true
TwoFactor->>FormContext: methods.register("backupCode")
activate FormContext
FormContext-->>TextField: Register field with form
Note over FormContext,TextField: Field "backupCode" now<br/>controlled by form context
deactivate FormContext
TextField-->>TwoFactor: Rendered input field
deactivate TextField
TwoFactor-->>User: Display backup code input form
deactivate TwoFactor
User->>TextField: Enter backup code
TextField->>FormContext: Update form state
FormContext-->>TextField: Acknowledge update
🔗 Cross-Repository Impact Analysis
Enable automatic detection of breaking changes across your dependent repositories. → Set up now
Learn more about Cross-Repository Analysis
What It Does
- Automatically identifies repositories that depend on this code
- Analyzes potential breaking changes across your entire codebase
- Provides risk assessment before merging to prevent cross-repo issues
How to Enable
- Visit Settings → Code Management
- Configure repository dependencies
- Future PRs will automatically include cross-repo impact analysis!
Benefits
- 🛡️ Prevent breaking changes across repositories
- 🔍 Catch integration issues before they reach production
- 📊 Better visibility into your multi-repo architecture
Install the extension
Note for Windsurf
Please change the default marketplace provider to the following in the windsurf settings:Marketplace Extension Gallery Service URL: https://marketplace.visualstudio.com/_apis/public/gallery
Marketplace Gallery Item URL: https://marketplace.visualstudio.com/items
Entelligence.ai can learn from your feedback. Simply add 👍 / 👎 emojis to teach it your preferences. More shortcuts below
Emoji Descriptions:
⚠️ Potential Issue - May require further investigation.- 🔒 Security Vulnerability - Fix to ensure system safety.
- 💻 Code Improvement - Suggestions to enhance code quality.
- 🔨 Refactor Suggestion - Recommendations for restructuring code.
- ℹ️ Others - General comments and information.
Interact with the Bot:
- Send a message or request using the format:
@entelligenceai + *your message*
Example: @entelligenceai Can you suggest improvements for this code?
- Help the Bot learn by providing feedback on its responses.
@entelligenceai + *feedback*
Example: @entelligenceai Do not comment on `save_auth` function !
Also you can trigger various commands with the bot by doing
@entelligenceai command
The current supported commands are
config- shows the current configretrigger_review- retriggers the review
More commands to be added soon.
|
|
||
| if (response.status === 200) { | ||
| onEnable(); | ||
| setStep(SetupStep.DisplayBackupCodes); |
There was a problem hiding this comment.
Correctness: In handleEnable, removing onEnable() causes a state desync. If the user dismisses the modal via ESC or backdrop after 2FA is enabled but before clicking the 'Close' button, the parent component is not notified and the UI remains stale. onEnable() must be invoked upon a successful API response to synchronize the parent state.
🤖 AI Agent Prompt for Cursor/Windsurf
📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue
In `apps/web/components/settings/EnableTwoFactorModal.tsx` at the success branch inside `handleEnable`, restore the `onEnable()` callback before advancing to `DisplayBackupCodes` so the parent is notified immediately after enabling 2FA. Ensure this callback fires regardless of how the modal is closed.
| 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.
Correctness: The backup code invalidation is not atomic. Concurrent login requests can fetch the same user.backupCodes state, verify the same code, and both succeed before prisma.user.update persists the invalidation. This allows a single-use backup code to be reused. Wrap the verification and update in a database transaction with a row-level lock for atomicity.
🤖 AI Agent Prompt for Cursor/Windsurf
📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue
File: packages/features/auth/lib/next-auth-options.ts. At lines ~149-156, the backup code is validated then updated without atomic protection. Update the logic so consumption of a backup code is atomic (e.g., transaction with row lock or updateMany with a guard on the original encrypted backupCodes and fail if no row is updated). Ensure concurrent logins cannot reuse the same backup code.
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
EntelligenceAI PR Summary
This PR adds backup code support for two-factor authentication, allowing users to generate and use single-use backup codes as an alternative authentication method.
backupCodesTEXT column in User model