Skip to content

feat: 2fa backup codes#1

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

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

Conversation

@ShashankFC

@ShashankFC ShashankFC commented Feb 13, 2026

Copy link
Copy Markdown

User description

Test 3

Summary by CodeRabbit

Release Notes

  • New Features
    • Added backup codes as a secondary two-factor authentication method
    • Users can download, copy, and view backup codes when enabling 2FA
    • Added "Lost Access" recovery option allowing users to authenticate with backup codes if their authenticator app is unavailable
    • Users can disable 2FA using backup codes if they've lost access to their primary method

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


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


CodeAnt-AI Description

Add single-use backup codes for two-factor authentication and a "lost access" recovery flow

What Changed

  • When enabling 2FA, the system now generates 10 single-use backup codes, shows them after setup, and lets users copy or download them as a text file.
  • Users can use a backup code to sign in or to disable 2FA when they cannot access their authenticator app; backup codes are validated server-side and consumed when used.
  • Backup codes are stored encrypted with existing encryption keys and are removed when 2FA is disabled; the UI adds a "Lost access" option on login and in the disable-2FA dialog to switch between authenticator code and backup code entry.
  • Small UX fixes: optional autofocus for 2FA inputs, password field toggle button made non-focusable to avoid tab stops, and localized messages for backup-code flows.

Impact

✅ Fewer account lockouts when users lose their authenticator
✅ Clearer 2FA recovery with copy/download of backup codes
✅ Shorter recovery during 2FA disable and login

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

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

Copy link
Copy Markdown
Author

@CodeAnt-AI: review

@codeant-ai

codeant-ai Bot commented Feb 13, 2026

Copy link
Copy Markdown

CodeAnt AI is running the review.

@codeant-ai codeant-ai Bot added the size:L This PR changes 100-499 lines, ignoring generated files label Feb 13, 2026
// 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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The assertion uses expect(page.locator(...).isChecked()).toBeTruthy() without awaiting isChecked(), so expect receives a Promise (always truthy) and the test passes even when the switch is not actually checked; instead, you should assert directly on the locator being checked (or on the awaited boolean) so the test correctly fails when 2FA is not enabled. [logic error]

Severity Level: Major ⚠️
- ❌ 2FA enablement test passes even when toggle unchecked.
- ⚠️ Regressions in 2FA UI state may go undetected.
- ⚠️ Weakens reliability of 2FA end-to-end coverage.
Suggested change
await expect(page.locator(`[data-testid=two-factor-switch]`).isChecked()).toBeTruthy();
await expect(page.locator(`[data-testid=two-factor-switch]`)).toBeChecked();
Steps of Reproduction ✅
1. From the repo root `/workspace/codeant-cal_dot_com`, run the Playwright test file
`apps/web/playwright/login.2fa.e2e.ts` (e.g. via `npx playwright test
apps/web/playwright/login.2fa.e2e.ts`), which defines the "2FA Tests" suite at lines
16–162 in `apps/web/playwright/login.2fa.e2e.ts`.

2. The test `"should allow a user to enable 2FA and login using 2FA"` at lines 20–88
executes the `"Enable 2FA"` step (lines 22–55), navigating to
`/settings/security/two-factor-auth`, clicking the two-factor switch, and completing OTP
setup.

3. After submitting a valid OTP, the test reaches the assertion block at lines 50–53: it
waits for the selector `[data-testid=two-factor-switch]` and then runs `await
expect(page.locator(`[data-testid=two-factor-switch]`).isChecked()).toBeTruthy();`, where
`locator().isChecked()` returns a `Promise<boolean>`.

4. Because the Promise is not awaited, the `expect` call at line 53 receives a Promise
object (always truthy) instead of a boolean; thus, even if the two-factor switch is not
actually checked in the UI, the assertion passes and the test will not fail, which matches
the FIXME comment at lines 50–51 indicating the test incorrectly passes.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** apps/web/playwright/login.2fa.e2e.ts
**Line:** 53:53
**Comment:**
	*Logic Error: The assertion uses `expect(page.locator(...).isChecked()).toBeTruthy()` without awaiting `isChecked()`, so `expect` receives a Promise (always truthy) and the test passes even when the switch is not actually checked; instead, you should assert directly on the locator being checked (or on the awaited boolean) so the test correctly fails when 2FA is not enabled.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

<button className="text-emphasis h-9" type="button" onClick={() => toggleIsPasswordVisible()}>
<button
className="text-emphasis h-9"
tabIndex={-1}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: Setting the password visibility toggle button's tabIndex to -1 makes it unreachable via keyboard navigation, which breaks keyboard accessibility for this control and prevents non-mouse users from toggling password visibility. [logic error]

Severity Level: Major ⚠️
- ⚠️ Keyboard-only users cannot toggle password visibility.
- ⚠️ Affects login password field on `/auth/login`.
- ⚠️ Affects password change form at `settings/security/password`.
- ⚠️ Affects 2FA enable/disable password confirmation modals.
Suggested change
tabIndex={-1}
Steps of Reproduction ✅
1. Render the shared PasswordField component from
`packages/ui/components/form/inputs/Input.tsx:21-64`, which internally renders the toggle
button defined at lines 47-51 with `tabIndex={-1}`.

2. Use any screen that consumes PasswordField, for example the login page at
`apps/web/pages/auth/login.tsx:193-209`, where the password input is rendered via
`<PasswordField id="password" ... {...register("password")} />`.

3. In a browser, navigate to `/auth/login` so that the form from `login.tsx` is rendered,
then use only the keyboard (Tab/Shift+Tab) to move focus through the form fields (email,
password, forgot-password link).

4. Observe that the password visibility toggle button rendered by PasswordField (the
`<button>` at `Input.tsx:47-51`) never receives focus via Tab because `tabIndex={-1}`
removes it from the tab order, so keyboard-only users cannot activate it to toggle
password visibility.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** packages/ui/components/form/inputs/Input.tsx
**Line:** 49:49
**Comment:**
	*Logic Error: Setting the password visibility toggle button's `tabIndex` to `-1` makes it unreachable via keyboard navigation, which breaks keyboard accessibility for this control and prevents non-mouse users from toggling password visibility.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

@codeant-ai

codeant-ai Bot commented Feb 13, 2026

Copy link
Copy Markdown

Nitpicks 🔍

🔒 No security issues identified
⚡ Recommended areas for review

  • Backup Code Logic
    The server-side handler checks a provided backup code and then (later) clears all stored backup codes by setting backupCodes: null when disabling 2FA. Verify the intended semantics: if backup codes are supposed to be single-use, the handler should remove only the used code; if they are one-time-for-all, ensure the behavior is documented. Also consider using a more compatible normalization than replaceAll (or ensure Node target supports it).

  • Sensitive Backup Codes
    Backup codes are generated, stored encrypted in the DB and returned in the API response. Returning the plaintext backup codes in the JSON response increases the attack surface (accidental logs, browser history, MITM if TLS misconfigured). Consider returning only an ephemeral confirmation or require the client to fetch them immediately via a separate secure endpoint and avoid returning plaintext in the API response if possible. Also consider storing hashed (not just encrypted) backup codes server side and validating by hashing the supplied code.

  • UX / Validation Inconsistency
    The login form adds a backupCode field in the form values and toggles between TOTP and backup code UI, but the form schema doesn't validate backupCode explicitly (it uses .passthrough()). Ensure the BackupCode component registers the field and any validation/clearing logic is consistent with these toggles to avoid stale values being submitted. Also verify that server-side credential flows accept the backupCode field name used here.

  • Flaky Test Assertion
    The test asserts switch state by calling .isChecked() inside expect(...) which resolves a boolean. Playwright has dedicated matchers for locators (e.g., expect(locator).toBeChecked()), and using those will produce clearer, less flaky tests. Also new backup-code download/copy checks have TODOs (content/clipboard assertions) meaning the test is not fully verifying the backup code contents.

@codeant-ai

codeant-ai Bot commented Feb 13, 2026

Copy link
Copy Markdown

CodeAnt AI finished running the review.

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

Labels

size:L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants