Skip to content

feat: 2fa backup codes#9

Open
akshayutture-augment wants to merge 1 commit into
enhance-two-factor-security-foundationfrom
improve-two-factor-authentication-features
Open

feat: 2fa backup codes#9
akshayutture-augment wants to merge 1 commit into
enhance-two-factor-security-foundationfrom
improve-two-factor-authentication-features

Conversation

@akshayutture-augment

@akshayutture-augment akshayutture-augment commented Nov 14, 2025

Copy link
Copy Markdown

Test 3

Summary by CodeRabbit

Release Notes

  • New Features

    • Backup codes now generated during two-factor authentication setup for account recovery
    • Users can download, copy, and use backup codes to regain access if they lose their authenticator
    • Added "Lost access" option to authenticate using backup codes
    • Users can disable 2FA using backup codes
  • Localization

    • Added backup code-related strings and error messages
  • Tests

    • Added end-to-end tests for backup code workflows

✏️ Tip: You can customize this high-level summary in your review settings.


Replicated from ai-code-review-evaluation/cal.com-coderabbit#3

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
@akshayutture-augment

Copy link
Copy Markdown
Author

@coderabbitai review

@akshayutture-augment

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Nov 18, 2025

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Nov 18, 2025

Copy link
Copy Markdown

Walkthrough

This pull request implements single-use backup codes as an alternative authentication mechanism for two-factor authentication. The feature includes UI components for backup code entry, API endpoints for generation and validation, database schema updates, and integrated error handling across login and settings flows.

Changes

Cohort / File(s) Summary
New Components
apps/web/components/auth/BackupCode.tsx
Introduces a new React component for backup code input in 2FA flows, with configurable center layout and form integration via react-hook-form.
Component Enhancements
apps/web/components/auth/TwoFactor.tsx, packages/ui/components/form/inputs/Input.tsx
Adds autoFocus prop to TwoFactor component to control initial focus behavior; adds tabIndex attribute to PasswordField toggle button.
Settings UI
apps/web/components/settings/DisableTwoFactorModal.tsx, apps/web/components/settings/EnableTwoFactorModal.tsx
Extends disable modal to support backup code validation and adds new DisplayBackupCodes step to enable modal with copy/download/display functionality for backup codes.
API Layer
apps/web/components/settings/TwoFactorAuthAPI.ts, apps/web/pages/api/auth/two-factor/totp/disable.ts, apps/web/pages/api/auth/two-factor/totp/setup.ts
Updates API methods to accept and process backup codes; generates 10 encrypted backup codes on setup and validates them during disable.
Authentication Flow
apps/web/pages/auth/login.tsx, packages/features/auth/lib/next-auth-options.ts
Extends login form with backup code field and UI toggle; adds backup code authentication branch to credentials provider with single-use code validation and consumption logic.
Error Handling & Types
packages/features/auth/lib/ErrorCode.ts
Adds two new error codes: IncorrectBackupCode and MissingBackupCodes.
Database Schema
packages/prisma/schema.prisma, packages/prisma/migrations/20230804153419_add_backup_codes/migration.sql
Adds optional backupCodes field to User model and creates migration to add TEXT column to users table.
Localization
apps/web/public/static/locales/en/common.json
Adds seven new localization keys for backup code messaging (backup_code, backup_codes, backup_code_instructions, backup_codes_copied, incorrect_backup_code, lost_access, missing_backup_codes).
Test Support
packages/lib/test/builder.ts, apps/web/playwright/login.2fa.e2e.ts
Extends test builder with backupCodes field; adds E2E test coverage for backup code download, copy, and close workflows in 2FA flows.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant LoginForm
    participant API
    participant DB

    User->>LoginForm: Enter credentials + 2FA required
    LoginForm->>API: POST sign-in
    API-->>LoginForm: SecondFactorRequired
    
    Note over User: User has two paths:
    
    rect rgb(200, 220, 255)
    Note over LoginForm,DB: Path 1: TOTP
    User->>LoginForm: Enter authenticator code
    LoginForm->>API: POST with TOTP code
    API->>DB: Verify TOTP
    API-->>LoginForm: Success
    end
    
    rect rgb(220, 255, 220)
    Note over LoginForm,DB: Path 2: Backup Code (Lost Access)
    User->>LoginForm: Click "Lost access"
    LoginForm->>User: Show backup code input
    User->>LoginForm: Enter backup code
    LoginForm->>API: POST with backup code
    API->>DB: Decrypt codes, verify match
    API->>DB: Nullify used code, re-encrypt
    API-->>LoginForm: Success
    end
    
    LoginForm-->>User: Authenticated
Loading
sequenceDiagram
    participant User
    participant SettingsUI
    participant API
    participant DB

    User->>SettingsUI: Enable 2FA
    SettingsUI->>API: POST setup with TOTP secret
    
    rect rgb(200, 220, 255)
    Note over API,DB: Generate & Store
    API->>API: Generate 10 random backup codes
    API->>DB: Encrypt & store codes
    API-->>SettingsUI: Return codes + secret
    end
    
    rect rgb(255, 240, 200)
    Note over SettingsUI,User: Display Codes
    SettingsUI->>User: Show backup codes in UI
    User->>SettingsUI: Copy codes
    SettingsUI->>User: Toast confirmation
    User->>SettingsUI: Download as file
    SettingsUI->>User: cal-backup-codes.txt
    User->>SettingsUI: Close dialog
    end
    
    SettingsUI-->>User: 2FA enabled
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Key areas requiring focused attention:

  • Authentication Flow Changes (packages/features/auth/lib/next-auth-options.ts): New backup code validation branch with encryption/decryption logic and single-use code consumption adds significant logic density to the credentials provider.
  • API Endpoint Updates (apps/web/pages/api/auth/two-factor/totp/disable.ts, setup.ts): Backup code generation, encryption, storage, and validation logic requires careful review of error cases and state management.
  • Modal State Management (apps/web/components/settings/EnableTwoFactorModal.tsx): Multi-step flow with DisplayBackupCodes state, backup code generation, and action handlers (copy/download) adds complexity.
  • Integration Across Login Flow (apps/web/pages/auth/login.tsx): New conditional rendering and state toggling between 2FA and backup code paths; error handling for new error codes.
  • Database Migration & Schema: Verify encryption key dependency and nullable field design.

Poem

🐰 Through encrypted tunnels, our codes now run,
When 2FA locks its door, backup codes save the day!
Copy, download, use them wisely—each one burns away,
Lost access, no longer, hop right back with these,
A safety net in binary, as clever as we please! 🔐

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: 2fa backup codes' clearly and concisely summarizes the main change across the changeset: introducing backup code functionality for two-factor authentication.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch improve-two-factor-authentication-features

Comment @coderabbitai help to get the list of available commands and usage tips.

@akshayutture-augment

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Nov 18, 2025

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@akshayutture-augment

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Nov 21, 2025

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (9)
apps/web/pages/api/auth/two-factor/totp/setup.ts (1)

60-61: Consider increasing backup code entropy for enhanced security.

Each backup code has 10 hex characters (40 bits of entropy). While acceptable for single-use codes behind authentication, consider:

  • Increasing to 12-16 characters for stronger security
  • Using alphanumeric (base32/base62) encoding for better entropy-per-character ratio
  • Following OWASP or NIST guidelines for backup code strength

For comparison:

  • GitHub uses 16-character backup codes
  • Google uses 8-character alphanumeric codes
  • Auth0 recommends 10-16 characters

If you decide to increase entropy, consider:

-  const backupCodes = Array.from(Array(10), () => crypto.randomBytes(5).toString("hex"));
+  // 8 bytes = 64 bits of entropy per code
+  const backupCodes = Array.from(Array(10), () => crypto.randomBytes(8).toString("hex"));
apps/web/playwright/login.2fa.e2e.ts (1)

110-126: Complete the TODOs for backup code file and clipboard validation.

The test verifies the download and copy actions occur but doesn't validate the actual content. This leaves gaps in test coverage:

  • Line 117: TODO to check file content
  • Line 122: TODO to check clipboard content

Consider completing these validations to ensure:

  • Downloaded file contains valid backup codes
  • Clipboard contains the expected backup codes
  • The codes match the format generated by the backend (10 hex characters each)

Example for file content validation:

// After download
const downloadPath = await download.path();
const fileContent = await fs.readFile(downloadPath, 'utf-8');
const codes = fileContent.trim().split('\n');
expect(codes).toHaveLength(10);
codes.forEach(code => {
  expect(code).toMatch(/^[0-9a-f]{10}$/);
});
apps/web/components/auth/BackupCode.tsx (1)

7-29: Rename component function to match file/usage for clarity

The default export is named TwoFactor in a file imported elsewhere as BackupCode. This works technically, but the mismatch can be confusing in error stacks and debugging. Consider renaming the function to BackupCode (or similar) so the symbol name, file name, and import name stay aligned; you can keep the default export the same.

apps/web/pages/api/auth/two-factor/totp/disable.ts (1)

46-68: Backup-code disable flow is correct; consider de-duplicating validation logic

The backup-code path here correctly:

  • Requires a password check first (for CAL users).
  • Validates presence of CALENDSO_ENCRYPTION_KEY and user.backupCodes.
  • Decrypts/parses backup codes and checks the normalized input (replaceAll("-","")) against them.
  • Returns specific error codes for missing/incorrect codes.
  • Finally clears both backupCodes and twoFactorSecret when disabling 2FA.

Semantics (any valid backup code can disable 2FA once, after which all codes are cleared) look sound. Since the decryption and matching logic is nearly identical to the login flow in next-auth-options.ts, you may want to extract a shared helper to avoid divergence if the format or matching rules change later.

Also applies to: 101-110

apps/web/components/settings/DisableTwoFactorModal.tsx (1)

23-27: Disable-2FA modal wiring for backup codes looks solid; consider resetting view on close

The modal cleanly adds backup-code support:

  • Form model and handleDisable correctly include backupCode and pass it to TwoFactorAuthAPI.disable.
  • Error handling now covers IncorrectBackupCode and MissingBackupCodes with appropriate localized messages.
  • The “Lost access” toggle swaps between <TwoFactor> and <BackupCode> while preserving the password and clearing codes/errors via resetForm(false), which is a nice touch.

One small UX refinement you might consider: reset twoFactorLostAccess (and possibly call resetForm()) when the modal is closed via onCancel/onOpenChange, so it always reopens in the default TOTP view instead of remembering the last mode.

Also applies to: 38-49, 50-86, 93-107, 112-121

apps/web/components/settings/EnableTwoFactorModal.tsx (3)

64-65: Tighten backup code state typing and lifecycle (cleanup + privacy).

The flow works, but a few small changes would make the backup-code state safer and clearer:

  • backupCodes is inferred as any[] and backupCodesUrl as string; they’re really string[] and a nullable URL.
  • resetState doesn’t clear backupCodes, backupCodesUrl, dataUri, or secret, so sensitive data stays in memory for the lifetime of the component.
  • The Blob URL is revoked only when regenerating codes, not when the modal is closed/unmounted.

Consider something like:

- const [backupCodes, setBackupCodes] = useState([]);
- const [backupCodesUrl, setBackupCodesUrl] = useState("");
+ const [backupCodes, setBackupCodes] = useState<string[]>([]);
+ const [backupCodesUrl, setBackupCodesUrl] = useState<string | null>(null);

- const resetState = () => {
-   setPassword("");
-   setErrorMessage(null);
-   setStep(SetupStep.ConfirmPassword);
- };
+ const resetState = () => {
+   setPassword("");
+   setErrorMessage(null);
+   setBackupCodes([]);
+   if (backupCodesUrl) URL.revokeObjectURL(backupCodesUrl);
+   setBackupCodesUrl(null);
+   setDataUri("");
+   setSecret("");
+   setStep(SetupStep.ConfirmPassword);
+ };

Optionally, you could also add a small useEffect cleanup that revokes backupCodesUrl on unmount for completeness.

Also applies to: 71-76, 92-99


31-31: Backup-codes step wiring looks sound; consider making formatBackupCode more defensive.

The new SetupStep.DisplayBackupCodes and its description, plus the transition in handleEnable to this step, create a clear final stage where users must acknowledge/save their backup codes, which is good UX.

formatBackupCode currently assumes a fixed 10-character code:

const formatBackupCode = (code: string) => `${code.slice(0, 5)}-${code.slice(5, 10)}`;

If the backend ever changes the code length, this will silently produce odd formatting. A slightly more defensive version would avoid that assumption:

- const formatBackupCode = (code: string) => `${code.slice(0, 5)}-${code.slice(5, 10)}`;
+ const formatBackupCode = (code: string) =>
+   code.length > 5 ? `${code.slice(0, 5)}-${code.slice(5)}` : code;

Functionally everything works today; this just future‑proofs the formatter a bit.

Also applies to: 58-58, 135-135, 163-163, 200-208


8-8: Finalize‑step actions are good; handle clipboard failures and confirm onEnable timing.

The close/copy/download actions in the backup-codes footer are a nice completion flow, but two small follow‑ups may help:

  • Clipboard errors: navigator.clipboard.writeText(...) returns a promise that can reject (permissions, unsupported environment). Right now that rejection would be unhandled. Wrapping it in a try/catch or .catch and showing an error toast would avoid noisy unhandled promise rejections and improve UX:
- onClick={(e) => {
-   e.preventDefault();
-   navigator.clipboard.writeText(backupCodes.map(formatBackupCode).join("\n"));
-   showToast(t("backup_codes_copied"), "success");
- }}
+ onClick={async (e) => {
+   e.preventDefault();
+   try {
+     await navigator.clipboard.writeText(backupCodes.map(formatBackupCode).join("\n"));
+     showToast(t("backup_codes_copied"), "success");
+   } catch {
+     showToast(t("something_went_wrong"), "error");
+   }
+}}
  • onEnable timing: Previously onEnable likely fired immediately after a successful enable call; now it fires only when the user clicks Close on the backup-codes step. If the parent UI depends on onEnable to reflect that 2FA is active, this is a behavioral change (enable becomes “effective” only after the user dismisses the backup-codes screen). If that’s intentional, all good—otherwise you may want to call onEnable when the API returns 200 and keep the Close button purely visual.

Also applies to: 222-231, 261-289

apps/web/pages/auth/login.tsx (1)

24-25: Backup-code toggle flow is solid; consider heading text and “go back” behavior.

The introduction of twoFactorLostAccess plus the new BackupCode component cleanly separates normal TOTP and backup-code flows, and the form wiring via LoginValues.backupCode looks correct.

A couple of small UX refinements you may want to consider:

  • Heading for backup-code mode: AuthContainer.heading is always t("2fa_code") when twoFactorRequired is true, even when twoFactorLostAccess is true and you’re actually showing <BackupCode />. For clarity, you could switch to t("backup_code") in that case, leveraging the new locale key:
- heading={twoFactorRequired ? t("2fa_code") : t("welcome_back")}
+ heading={
+   twoFactorRequired
+     ? twoFactorLostAccess
+       ? t("backup_code")
+       : t("2fa_code")
+     : t("welcome_back")
+ }
  • “Go back” semantics: In TwoFactorFooter, the Go back button:
    • From TOTP → sets twoFactorRequired(false) and clears totpCode (back to email/password screen).
    • From backup-code → only flips twoFactorLostAccess to false and clears backupCode (back to TOTP, still in 2FA flow).

If the intent is “back one step” this is fine; if the intent is “back to login” from both variants, you might instead always clear both codes and set twoFactorRequired(false) in both branches.

Nothing here is strictly wrong—just worth double-checking the intended UX.

Also applies to: 32-33, 40-45, 70-71, 103-132, 221-221

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between efa6d46 and a308075.

📒 Files selected for processing (16)
  • apps/web/components/auth/BackupCode.tsx (1 hunks)
  • apps/web/components/auth/TwoFactor.tsx (2 hunks)
  • apps/web/components/settings/DisableTwoFactorModal.tsx (4 hunks)
  • apps/web/components/settings/EnableTwoFactorModal.tsx (9 hunks)
  • apps/web/components/settings/TwoFactorAuthAPI.ts (1 hunks)
  • apps/web/pages/api/auth/two-factor/totp/disable.ts (2 hunks)
  • apps/web/pages/api/auth/two-factor/totp/setup.ts (3 hunks)
  • apps/web/pages/auth/login.tsx (6 hunks)
  • apps/web/playwright/login.2fa.e2e.ts (3 hunks)
  • apps/web/public/static/locales/en/common.json (1 hunks)
  • packages/features/auth/lib/ErrorCode.ts (1 hunks)
  • packages/features/auth/lib/next-auth-options.ts (4 hunks)
  • packages/lib/test/builder.ts (1 hunks)
  • packages/prisma/migrations/20230804153419_add_backup_codes/migration.sql (1 hunks)
  • packages/prisma/schema.prisma (1 hunks)
  • packages/ui/components/form/inputs/Input.tsx (1 hunks)
🔇 Additional comments (12)
packages/ui/components/form/inputs/Input.tsx (1)

47-51: Verify accessibility impact of removing password toggle from tab order.

Adding tabIndex={-1} removes the password visibility toggle button from keyboard navigation. While this can reduce tab stops for keyboard users, ensure that:

  • Keyboard-only users have an alternative way to toggle password visibility if needed
  • This aligns with your accessibility guidelines and WCAG standards

Some applications intentionally keep password toggles in the tab order for better keyboard accessibility, while others exclude them as supplementary controls. Verify that this change aligns with your project's accessibility requirements.

packages/prisma/schema.prisma (1)

205-205: LGTM!

The optional backupCodes field addition is appropriate for storing encrypted backup codes. The nullable String? type correctly reflects that backup codes are only present when 2FA is enabled.

packages/prisma/migrations/20230804153419_add_backup_codes/migration.sql (1)

1-2: LGTM!

The migration correctly adds a nullable TEXT column to store encrypted backup codes. The TEXT type is appropriate for storing the encrypted JSON string representation of backup codes.

packages/lib/test/builder.ts (1)

192-192: LGTM!

The addition of backupCodes: null correctly updates the test builder to match the updated User schema. The null default is appropriate for test users without 2FA enabled.

packages/features/auth/lib/ErrorCode.ts (1)

11-12: LGTM!

The new error codes for backup code validation are appropriately named and follow the existing naming conventions. Both IncorrectBackupCode and MissingBackupCodes serve clear, distinct purposes in the backup code authentication flow.

apps/web/components/auth/TwoFactor.tsx (2)

8-8: LGTM!

Adding the autoFocus prop with a default value of true provides flexibility while maintaining backward compatibility. This is particularly useful when the TwoFactor component is conditionally rendered alongside other input components (like the new BackupCode component).


43-43: LGTM!

The updated autofocus logic correctly gates the behavior on the new autoFocus prop, allowing parent components to control focus management. This improves accessibility and component reusability.

apps/web/pages/api/auth/two-factor/totp/setup.ts (1)

60-61: Single-use enforcement for backup codes is properly implemented.

The backup codes are correctly enforced as single-use in the primary authentication path (packages/features/auth/lib/next-auth-options.ts, lines 144–154):

  • Validation finds the code via indexOf()
  • Used code is replaced with null and the entire array is re-encrypted and saved
  • Subsequent attempts to reuse the same code will not find it in the array (returning -1), triggering an IncorrectBackupCode error
  • This design prevents replay attacks
apps/web/components/settings/TwoFactorAuthAPI.ts (1)

22-26: Backup-code parameter wiring to disable API looks consistent

disable(password, code, backupCode) correctly posts { password, code, backupCode } to the server, matching the backend’s expected shape and the modal’s call site (password, totpCode, backupCode). No issues spotted here.

apps/web/components/settings/EnableTwoFactorModal.tsx (1)

167-170: Dialog header and password-confirm step look consistent.

Using setupDescriptions[step] for the DialogContent description and switching the title to “Backup Codes” in the backup step is consistent with the new enum and locale keys. Replacing the raw password input with PasswordField keeps this step aligned with other password UIs in the app.

No issues spotted here.

Also applies to: 174-174

apps/web/public/static/locales/en/common.json (1)

2013-2019: Backup-code localization entries are consistent and complete.

The new strings cover all necessary backup-code flows (labels, instructions, success and error messages, and the “Lost access” action) and match surrounding tone and casing.

Looks good as-is.

apps/web/pages/auth/login.tsx (1)

145-161: New backup-code error handling aligns with locale additions.

Handling ErrorCode.IncorrectBackupCode and ErrorCode.MissingBackupCodes explicitly in onSubmit and surfacing t("incorrect_backup_code") / t("missing_backup_codes") keeps backup-code failures distinct from generic 2FA or credential errors.

Assuming the backend returns these two error codes as described, this looks correct.


test.describe.configure({ mode: "parallel" });

// TODO: add more backup code tests, e.g. login + disabling 2fa with backup

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Implement comprehensive backup code authentication tests.

The TODO indicates missing test coverage for critical backup code flows. Consider adding tests for:

  • Logging in with a backup code instead of TOTP
  • Disabling 2FA using a backup code
  • Verifying backup codes are single-use (attempting to reuse a code should fail)
  • Handling of the IncorrectBackupCode error

These tests are essential for validating the security properties of backup codes. Would you like me to help draft the test scenarios?

🤖 Prompt for AI Agents
In apps/web/playwright/login.2fa.e2e.ts around line 12, there's a TODO noting
missing backup-code tests; add end-to-end Playwright tests that (1) create/setup
a user with 2FA and generated backup codes, (2) perform login using one valid
backup code instead of TOTP and assert successful authentication and subsequent
UI/state, (3) attempt to reuse the same backup code and assert failure and the
IncorrectBackupCode error/state, (4) perform disabling 2FA using a valid backup
code and assert 2FA is removed and backup codes invalidated, and (5) cover
expected server error responses/messages for IncorrectBackupCode; implement test
helpers to generate/inspect backup codes, use unique test users, clean up state
between tests, and include explicit assertions for single-use behavior and
proper error handling.

Comment on lines +50 to +51
// FIXME: this passes even when switch is not checked, compare to test
// below which checks for data-state="checked" and works as expected

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Address the FIXME for switch state validation.

The FIXME indicates that the current assertion passes even when the switch is unchecked, suggesting a potential issue with the test reliability. The comparison with the working data-state="checked" approach (line 127) suggests switching to that pattern.

Apply this diff to use the more reliable state check:

-      // FIXME: this passes even when switch is not checked, compare to test
-      // below which checks for data-state="checked" and works as expected
-      await page.waitForSelector(`[data-testid=two-factor-switch]`);
-      await expect(page.locator(`[data-testid=two-factor-switch]`).isChecked()).toBeTruthy();
+      await expect(page.locator(`[data-testid=two-factor-switch][data-state="checked"]`)).toBeVisible();

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/web/playwright/login.2fa.e2e.ts around lines 50 to 51, the current
switch assertion is unreliable (passes when unchecked); replace the current
check with the more robust attribute-based check used later: assert the switch
element's data-state attribute equals "checked" (mirroring the pattern at line
127) so the test only passes when the switch is actually checked.

Comment on lines +14 to 15
import { symmetricDecrypt, symmetricEncrypt } from "@calcom/lib/crypto";
import { defaultCookies } from "@calcom/lib/default-cookies";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Backup-code login likely blocked for non‑CAL identity providers

The new backup-code branch looks correct in isolation (decrypt, normalize, single-use via backupCodes[index] = null and re-encrypt), but the surrounding guards still only treat totpCode as the “second factor”:

  • Line 113: if (user.identityProvider !== IdentityProvider.CAL && !credentials.totpCode) { throw ThirdPartyIdentityProviderEnabled; }
  • Line 117: if (!user.password && user.identityProvider !== IdentityProvider.CAL && !credentials.totpCode) { ... }

For an OAuth/SAML user on the 2FA screen who enters only a backupCode (no totpCode, no password), !credentials.totpCode is true, so these checks will throw before your backupCode branch (lines 131–157) runs. That means backup-code login works for CAL/password users but is probably broken for non‑CAL users even though the login UI exposes backup codes generically.

If backup codes are intended to work for all identity providers, consider relaxing these conditions to also treat backupCode as a valid second factor, e.g. by checking !credentials.totpCode && !credentials.backupCode instead.

Also, backup-code decryption/validation is now duplicated here and in apps/web/pages/api/auth/two-factor/totp/disable.ts; extracting a shared helper would reduce drift risk when changing the format or error handling.

Also applies to: 62-66, 89-90, 113-129, 131-157

🤖 Prompt for AI Agents
In packages/features/auth/lib/next-auth-options.ts around lines 62-66, 89-90,
113-129 and 131-157, the current guards only treat totpCode as the second factor
which blocks backupCode flows for non-CAL identity providers; update all
relevant conditionals to accept either credentials.totpCode OR
credentials.backupCode (i.e., replace checks like !credentials.totpCode with
!credentials.totpCode && !credentials.backupCode) so backup codes are allowed
the same way as TOTP, and refactor the duplicate backup-code
decryption/validation logic into a shared helper used here and in
apps/web/pages/api/auth/two-factor/totp/disable.ts to avoid divergence (move
decryption, normalization, single-use marking and re-encrypting into that helper
and call it from both places).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants