8000 2FA by devkiran · Pull Request #2505 · dubinc/dub · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

2FA #2505

8000
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 16 commits into
base: main
Choose a base branch
from
Draft

2FA #2505

wants to merge 16 commits into from

Conversation

devkiran
Copy link
Collaborator
@devkiran devkiran commented Jun 9, 2025

Summary by CodeRabbit

  • New Features

    • Introduced two-factor authentication (2FA) support, allowing users to enable, confirm, and disable 2FA for their accounts.
    • Added dedicated UI components and modals for managing 2FA setup, code entry, and recovery.
    • Implemented a two-factor challenge page during login for users with 2FA enabled.
    • Users receive email notifications when 2FA is enabled or disabled.
  • Enhancements

    • Updated account security settings to include 2FA management.
    • Improved error handling and user feedback for 2FA-related login scenarios.
    • Added seamless 2FA cookie handling and authentication flow integration.
    • Enhanced user data fetching with revalidation support.
  • Bug Fixes

    • Ensured proper redirect and access control for the two-factor challenge flow.
  • Chores

    • Added the "otpauth" dependency for TOTP-based authentication.

Copy link
vercel bot commented Jun 9, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Updated (UTC)
dub ✅ Ready (Inspect) Visit Preview Jun 10, 2025 10:57am

Copy link
Contributor
coderabbitai bot commented Jun 9, 2025

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

This update introduces two-factor authentication (2FA) throughout the web application. It extends the user model and authentication logic to support enabling, confirming, and disabling 2FA, integrates TOTP-based code verification, and adds new UI components and modals for 2FA setup and management. Email notifications and error handling for 2FA events are also implemented.

Changes

File(s) Change Summary
packages/prisma/schema/schema.prisma Added twoFactorSecret, twoFactorConfirmedAt, and twoFactorRecoveryCodes to the User model.
apps/web/lib/types.ts Extended UserProps with twoFactorConfirmedAt and twoFactorRecoveryCodes.
apps/web/app/api/user/route.ts Included 2FA fields in user selection and response.
apps/web/lib/auth/constants.ts Added TWO_FA_COOKIE_NAME constant.
apps/web/lib/auth/totp.ts Introduced TOTP secret generation and instance creation utilities.
apps/web/lib/auth/options.ts Added 2FA cookie handling, 2FA challenge provider, and 2FA checks to credentials provider.
apps/web/lib/actions/auth/enable-two-factor-auth.ts New action to initiate enabling 2FA, generating secret and QR code.
apps/web/lib/actions/auth/confirm-two-factor-auth.ts New action to confirm 2FA setup with OTP validation and email notification.
apps/web/lib/actions/auth/disable-two-factor-auth.ts New action to disable 2FA, clear secrets, and send notification email.
apps/web/lib/swr/use-user.ts Switched to useSWR, exposing mutate for user data refresh.
apps/web/lib/middleware/app.ts Allowed unauthenticated access to /two-factor-challenge route.
apps/web/app/app.dub.co/(dashboard)/account/settings/security/page-client.tsx Always renders main content; adds TwoFactorAuth component.
apps/web/app/app.dub.co/(dashboard)/account/settings/security/two-factor-auth.tsx New TwoFactorAuth component for managing 2FA settings and modals.
apps/web/app/app. 8000 dub.co/(auth)/two-factor-challenge/form.tsx New TwoFactorChallengeForm for submitting 2FA codes.
apps/web/app/app.dub.co/(auth)/two-factor-challenge/page.tsx New challenge page; redirects if 2FA cookie missing.
apps/web/ui/auth/login/email-sign-in.tsx Redirects to /two-factor-challenge on 2FA-required error.
apps/web/ui/auth/login/login-form.tsx Added 2FA-related error codes to error message mapping.
apps/web/ui/modals/enable-two-factor-auth-modal.tsx New modal and hook for enabling 2FA, including QR code and OTP input.
apps/web/ui/modals/disable-two-factor-auth-modal.tsx New modal and hook for disabling 2FA with confirmation.
packages/email/src/templates/two-factor-enabled.tsx New email template for 2FA enabled notification.
packages/email/src/templates/two-factor-disabled.tsx New email template for 2FA disabled notification.
apps/web/package.json Added otpauth dependency.

Sequence Diagram(s)

Two-Factor Authentication Login Flow

sequenceDiagram
    participant User
    participant LoginForm
    participant Server (Credentials Provider)
    participant Cookie
    participant 2FA Challenge Page
    participant Server (2FA Challenge Provider)

    User->>LoginForm: Enter email & password
    LoginForm->>Server (Credentials Provider): Submit credentials
    Server (Credentials Provider)->>Server (Credentials Provider): Validate credentials
    alt 2FA enabled
        Server (Credentials Provider)->>Cookie: Set 2FA cookie
        Server (Credentials Provider)->>LoginForm: Return "two-factor-required" error
        LoginForm->>2FA Challenge Page: Redirect
        User->>2FA Challenge Page: Enter 2FA code
        2FA Challenge Page->>Server (2FA Challenge Provider): Submit code + 2FA cookie
        Server (2FA Challenge Provider)->>Server (2FA Challenge Provider): Validate code and cookie
        alt Code valid
            Server (2FA Challenge Provider)->>2FA Challenge Page: Success (user authenticated)
        else Code invalid
            Server (2FA Challenge Provider)->>2FA Challenge Page: Error
        end
    else 2FA not enabled
        Server (Credentials Provider)->>LoginForm: Success (user authenticated)
    end
Loading

Enabling Two-Factor Authentication

sequenceDiagram
    participant User
    participant SecurityPage
    participant Server (Enable 2FA Action)
    participant Enable2FAModal
    participant Server (Confirm 2FA Action)
    participant Email

    User->>SecurityPage: Click "Enable 2FA"
    SecurityPage->>Server (Enable 2FA Action): Request secret & QR code
    Server (Enable 2FA Action)->>SecurityPage: Return secret & QR code URL
    SecurityPage->>Enable2FAModal: Open modal with QR code & secret
    User->>Enable2FAModal: Enter 6-digit code
    Enable2FAModal->>Server (Confirm 2FA Action): Submit code
    Server (Confirm 2FA Action)->>Server (Confirm 2FA Action): Validate code
    alt Code valid
        Server (Confirm 2FA Action)->>Email: Send 2FA enabled notification
        Server (Confirm 2FA Action)->>Enable2FAModal: Success
        Enable2FAModal->>SecurityPage: Close modal, refresh user
    else Code invalid
        Server (Confirm 2FA Action)->>Enable2FAModal: Error
    end
Loading

Disabling Two-Factor Authentication

sequenceDiagram
    participant User
    participant SecurityPage
    participant Disable2FAModal
    participant Server (Disable 2FA Action)
    participant Email

    User->>SecurityPage: Click "Disable 2FA"
    SecurityPage->>Disable2FAModal: Open modal
    User->>Disable2FAModal: Confirm disable
    Disable2FAModal->>Server (Disable 2FA Action): Request disable
    Server (Disable 2FA Action)->>Server (Disable 2FA Action): Clear 2FA fields
    Server (Disable 2FA Action)->>Email: Send 2FA disabled notification
    Server (Disable 2FA Action)->>Disable2FAModal: Success
    Disable2FAModal->>SecurityPage: Close modal, refresh user
Loading

Poem

Two fluffy ears, a hop, a leap—
Now your logins are twice as deep!
With secret codes and emails bright,
Our carrots guard you day and night.
Enable, confirm, or turn it off—
Two-factor’s here, so hackers scoff!
🥕🔒


🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
socket-security bot commented Jun 9, 2025

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedotpauth@​9.4.010010010081100

View full report

Copy link
bug0-qa-agent bot commented Jun 9, 2025

🤖 Bug0 QA Agent

Here are the results of the automated tests for PR #2505:

To re-run the tests, please comment /bug0 run or push a new commit to this PR.

Copy link
bug0-qa-agent bot commented Jun 9, 2025

🤖 Bug0 QA Agent

Here are the results of the automated tests for PR #2505:

To re-run the tests, please comment /bug0 run or push a new commit to this PR.

Copy link
bug0-qa-agent bot commented Jun 9, 2025

🤖 Bug0 QA Agent

Here are the results of the automated tests for PR #2505:

To re-run the tests, please comment /bug0 run or push a new commit to this PR.

Copy link
bug0-qa-agent bot commented Jun 9, 2025

🤖 Bug0 QA Agent

Here are the results of the automated tests for PR #2505:

To re-run the tests, please comment /bug0 run or push a new commit to this PR.

- Updated user API to include twoFactorConfirmedAt and twoFactorRecoveryCodes.
- Enhanced TwoFactorAuth component to display loading state and toggle button text based on user's 2FA status.
- Introduced disableTwoFactorAuthAction to handle disabling of 2FA for users.
- Updated UserProps interface to reflect new 2FA properties.
Copy link
bug0-qa-agent bot commented Jun 9, 2025

🤖 Bug0 QA Agent

Here are the results of the automated tests for PR #2505:

To re-run the tests, please comment /bug0 run or push a new commit to this PR.

Copy link
bug0-qa-agent bot commented Jun 9, 2025

🤖 Bug0 QA Agent

Here are the results of the automated tests for PR #2505:

To re-run the tests, please comment /bug0 run or push a new commit to this PR.

Copy link
bug0-qa-agent bot commented Jun 9, 2025

🤖 Bug0 QA Agent

Here are the results of the automated tests for PR #2505:

To re-run the tests, please comment /bug0 run or push a new commit to this PR.

Copy link
bug0-qa-agent bot commented Jun 10, 2025

🤖 Bug0 QA Agent

Here are the results of the automated tests for PR #2505:

To re-run the tests, please comment /bug0 run or push a new commit to this PR.

Copy link
bug0-qa-agent bot commented Jun 10, 2025

🤖 Bug0 QA Agent

Here are the results of the automated tests for PR #2505:

To re-run the tests, please comment /bug0 run or push a new commit to this PR.

@devkiran
Copy link
Collaborator Author

@coderabbitai review

Copy link
Contributor
coderabbitai bot commented Jun 10, 2025
✅ 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.

@devkiran
Copy link
Collaborator Author

@coderabbitai full review

Copy link
Contributor
coderabbitai bot commented Jun 10, 2025
✅ Actions performed

Full review triggered.

Copy link
Contributor
@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (7)
packages/prisma/schema/schema.prisma (1)

31-34: Consider adding database index for 2FA queries.

The twoFactorConfirmedAt field will likely be queried frequently to check if users have 2FA enabled. Consider adding an index for better query performance.

+ @@index(twoFactorConfirmedAt)

Also, storing recovery codes as a comma-separated string in twoFactorRecoveryCodes requires parsing. Consider if this approach aligns with your data access patterns or if a separate table would be more appropriate for better normalization.

packages/email/src/templates/two-factor-disabled.tsx (1)

18-18: Consider updating the default email placeholder.

The default email "panic@thedis.co" appears to be a placeholder. Consider using a more generic placeholder like "user@example.com" for consistency.

-  email = "panic@thedis.co",
+  email = "user@example.com",
packages/email/src/templates/two-factor-enabled.tsx (1)

18-18: Consider updating the default email placeholder for consistency.

Same as the disabled template, consider using "user@example.com" instead of "panic@thedis.co".

-  email = "panic@thedis.co",
+  email = "user@example.com",
apps/web/ui/modals/enable-two-factor-auth-modal.tsx (2)

84-91: Accessibility: OTP cells need an explicit label

div elements that visually represent input fields lack aria-label / role="textbox" which breaks screen-reader navigation.
Wrap the OTPInput in a single visually hidden <label> (or add aria-label to each cell) so assistive tech can announce what the user is expected to enter.


128-139: Memoised modal callback still re-creates on every showModal toggle

Because showModal is part of ModalCallback’s dependency list, the component returned from useEnableTwoFactorAuthModal changes on every open/close which defeats memoisation at the parent level.

If only the visibility should change, exclude showModal from the dependencies and pass it as prop instead:

-  const ModalCallback = useCallback(
-    () => (
+  const ModalCallback = useCallback(
+    ({ visible }: { visible: boolean }) => (
       <EnableTwoFactorAuthModal
-        showModal={showModal}
+        showModal={visible}
         …
       />
     ),
-    [showModal, onSuccess, secret, qrCodeUrl],
+    [onSuccess, secret, qrCodeUrl],
   );

Callers would render <EnableTwoFactorAuthModal visible={showModal} …/>.
Keeps a stable component reference and prevents unnecessary remounts.

apps/web/lib/auth/options.ts (2)

337-344: Cookie is deleted before challenge outcome is known

cookies().delete(TWO_FA_COOKIE_NAME) executes immediately, so any network failure or invalid code forces the user to start the login flow from scratch.
Delete the cookie only after successful verification, or re-issue a fresh token on failure to maintain UX while still preventing replay attacks.


395-400: Verbose logging leaks potentially sensitive identifiers

console.log / console.error include sub and email. In a serverless environment these land in shared logs and could violate privacy policies.

Replace with structured, redacted logging or remove entirely.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bc41d19 and 3e1972a.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (22)
  • apps/web/app/api/user/route.ts (1 hunks)
  • apps/web/app/app.dub.co/(auth)/two-factor-challenge/form.tsx (1 hunks)
  • apps/web/app/app.dub.co/(auth)/two-factor-challenge/page.tsx (1 hunks)
  • apps/web/app/app.dub.co/(dashboard)/account/settings/security/page-client.tsx (1 hunks)
  • apps/web/app/app.dub.co/(dashboard)/account/settings/security/two-factor-auth.tsx (1 hunks)
  • apps/web/lib/actions/auth/confirm-two-factor-auth.ts (1 hunks)
  • apps/web/lib/actions/auth/disable-two-factor-auth.ts (1 hunks)
  • apps/web/lib/actions/auth/enable-two-factor-auth.ts (1 hunks)
  • apps/web/lib/auth/constants.ts (1 hunks)
  • apps/web/lib/auth/options.ts (3 hunks)
  • apps/web/lib/auth/totp.ts (1 hunks)
  • apps/web/lib/middleware/app.ts (1 hunks)
  • apps/web/lib/swr/use-user.ts (1 hunks)
  • apps/web/lib/types.ts (1 hunks)
  • apps/web/package.json (1 hunks)
  • apps/web/ui/auth/login/email-sign-in.tsx (1 hunks)
  • apps/web/ui/auth/login/login-form.tsx (1 hunks)
  • apps/web/ui/modals/disable-two-factor-auth-modal.tsx (1 hunks)
  • apps/web/ui/modals/enable-two-factor-auth-modal.tsx (1 hunks)
  • packages/email/src/templates/two-factor-disabled.tsx (1 hunks)
  • packages/email/src/templates/two-factor-enabled.tsx (1 hunks)
  • packages/prisma/schema/schema.prisma (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (7)
apps/web/app/app.dub.co/(dashboard)/account/settings/security/page-client.tsx (2)
apps/web/lib/swr/use-user.ts (1)
  • useUser (5-13)
apps/web/app/app.dub.co/(dashboard)/account/settings/security/two-factor-auth.tsx (1)
  • TwoFactorAuth (12-101)
apps/web/lib/swr/use-user.ts (1)
apps/web/lib/types.ts (1)
  • UserProps (178-192)
apps/web/lib/actions/auth/disable-two-factor-auth.ts (3)
apps/web/lib/actions/safe-action.ts (1)
  • authUserActionClient (19-31)
packages/email/src/index.ts (1)
  • sendEmail (6-29)
packages/email/src/templates/two-factor-disabled.tsx (1)
  • TwoFactorDisabled (17-49)
apps/web/lib/actions/auth/confirm-two-factor-auth.ts (4)
apps/web/lib/actions/safe-action.ts (1)
  • authUserActionClient (19-31)
apps/web/lib/auth/totp.ts (1)
  • getTOTPInstance (14-26)
packages/email/src/index.ts (1)
  • sendEmail (6-29)
packages/email/src/templates/two-factor-enabled.tsx (1)
  • TwoFactorEnabled (17-49)
packages/email/src/templates/two-factor-enabled.tsx (2)
packages/utils/src/constants/main.ts (1)
  • DUB_WORDMARK (68-68)
packages/ui/src/footer.tsx (1)
  • Footer (85-310)
apps/web/app/app.dub.co/(dashboard)/account/settings/security/two-factor-auth.tsx (4)
apps/web/lib/swr/use-user.ts (1)
  • useUser (5-13)
apps/web/ui/modals/enable-two-factor-auth-modal.tsx (1)
  • useEnableTwoFactorAuthModal (117-148)
apps/web/ui/modals/disable-two-factor-auth-modal.tsx (1)
  • useDisableTwoFactorAuthModal (72-92)
apps/web/lib/actions/auth/enable-two-factor-auth.ts (1)
  • enableTwoFactorAuthAction (8-55)
apps/web/ui/modals/disable-two-factor-auth-modal.tsx (2)
apps/web/lib/swr/use-user.ts (1)
  • useUser (5-13)
apps/web/lib/actions/auth/disable-two-factor-auth.ts (1)
  • disableTwoFactorAuthAction (10-44)
🔇 Additional comments (26)
apps/web/lib/auth/constants.ts (1)

12-12: LGTM! Clean constant addition for 2FA cookie.

The constant follows good naming conventions with a descriptive name and appropriate prefix.

apps/web/package.json (1)

99-99:

✅ Verification successful

Verify the otpauth package version and security status.

The otpauth library is a good choice for TOTP implementation, but please ensure version 9.4.0 is current and free from vulnerabilities.


🌐 Web query:

What is the latest version of the otpauth npm package and are there any known security vulnerabilities in version 9.4.0?

💡 Result:

The latest version of the otpauth npm package is 9.4.0, released on March 29, 2025, approximately 2.5 months ago from the current date[1][2]. This package is a One Time Password (HOTP/TOTP) library designed for Node.js, Deno, Bun, and browsers, supporting the generation and validation of time-based and HMAC-based one-time passwords commonly used in multi-factor authentication systems[1].

Regarding security vulnerabilities in version 9.4.0, according to the Snyk Vulnerability Database, there are no known direct vulnerabilities in this latest version[2]. When examining the vulnerability table from Snyk, version 9.4.0 shows no critical, high, medium, or low severity vulnerabilities[2].

It's worth noting that earlier versions of otpauth (specifically versions below 3.2.8) were vulnerable to an Authentication Bypass issue[5], but this vulnerability has been addressed in more recent versions, including 9.4.0.

The otpauth package is maintained by Héctor Molinero Fernández under the MIT License[7]. The package supports various algorithms for the HMAC function, including SHA1, SHA224, SHA256, SHA384, SHA512, and several SHA3 variants, making it a versatile option for implementing two-factor authentication in applications[1].

Citations:


otpauth@9.4.0 version and security status verified

Version 9.4.0 is the latest release (Mar 29, 2025) and has no known vulnerabilities according to the Snyk database. No further action required.

apps/web/ui/auth/login/email-sign-in.tsx (1)

95-98: LGTM! Proper 2FA redirect handling.

The logic correctly handles the "two-factor-required" error by redirecting to the challenge page and returning early to bypass normal error handling.

apps/web/app/api/user/route.ts (1)

42-43: LGTM! Appropriate addition of 2FA fields to user API.

The inclusion of twoFactorConfirmedAt and twoFactorRecoveryCodes fields is necessary for the frontend to manage 2FA state and recovery codes.

apps/web/lib/middleware/app.ts (1)

30-30: LGTM! Correct middleware exclusion for 2FA flow.

The exclusion of /two-factor-challenge from authentication requirements is necessary to allow users in the partial authentication state to complete their 2FA verification.

apps/web/lib/swr/use-user.ts (2)

2-2: Good change to enable data revalidation for 2FA operations.

Switching from useSWRImmutable to useSWR is appropriate since user data can now change (e.g., 2FA status updates).


6-6: Adding mutate function enables proper state synchronization.

The mutate function allows components to trigger revalidation after 2FA operations, ensuring the UI stays in sync with the backend state.

Also applies to: 11-11

apps/web/ui/auth/login/login-form.tsx (1)

47-53: Well-designed 2FA error messages.

The new error codes provide clear, user-friendly feedback for 2FA scenarios:

  • Appropriate prompting for 2FA code entry
  • Clear messaging for expired authentication sessions
  • Helpful feedback for incorrect codes

The messages guide users toward resolution without exposing sensitive implementation details.

apps/web/app/app.dub.co/(dashboard)/account/settings/security/page-client.tsx (2)

5-5: Clean integration of 2FA management.

The addition of the TwoFactorAuth component properly integrates 2FA settings into the security page alongside password management.

Also applies to: 37-37


32-39:

✅ Verification successful

Verify child component loading states.

Since the parent loading state was removed, ensure that both UpdatePassword/RequestSetPassword and TwoFactorAuth components handle their own loading states appropriately to prevent UI issues.


🏁 Script executed:

#!/bin/bash
# Description: Check if child components handle loading states properly

# Check TwoFactorAuth component for loading state handling
ast-grep --pattern $'const { user, loading } = useUser()' 

# Check UpdatePassword component for loading state handling  
rg -A 10 "useUser|loading" apps/web/app/app.dub.co/\(dashboard\)/account/settings/security/update-password.tsx

# Check RequestSetPassword component for loading state handling
rg -A 10 "useUser|loading" apps/web/app/app.dub.co/\(dashboard\)/account/settings/security/request-set-password.tsx

Length of output: 1162


🏁 Script executed:

#!/bin/bash
# Description: Verify TwoFactorAuth component for its own loading state handling

# Inspect useUser usage
rg -A 10 "useUser" apps/web/app/app.dub.co/\(dashboard\)/account/settings/security/two-factor-auth.tsx

# Inspect any loading or internal submission state
rg -A 10 "loading" apps/web/app/app.dub.co/\(dashboard\)/account/settings/security/two-factor-auth.tsx

Length of output: 2025


No action needed: child components each manage their own loading states

All three components independently handle loading:

  • UpdatePassword uses isSubmitting (via form) for its loading prop.
  • RequestSetPassword tracks a sending state for loading and disabled.
  • TwoFactorAuth uses loading from useUser (disables the button and shows “Loading…”) plus its own isEnabling for the loading prop when toggling 2FA.
packages/email/src/templates/two-factor-disabled.tsx (1)

17-21: LGTM! Clean email template implementation.

The template structure and security messaging are appropriate for notifying users about 2FA being disabled.

apps/web/lib/actions/auth/confirm-two-factor-auth.ts (4)

11-13: Excellent input validation with appropriate constraints.

The 6-digit length validation ensures TOTP code format compliance and provides clear error messaging.


32-38: Good security checks for preventing misuse.

The validation logic properly handles edge cases:

  • Prevents confirming already-enabled 2FA
  • Ensures secret exists before validation
  • Provides informative error messages without exposing sensitive details

44-53: Secure TOTP validation with appropriate tolerance window.

The window of 1 step (±30 seconds) provides good user experience while maintaining security. The null check for invalid tokens is correctly implemented.


65-72: Proper async email handling for better performance.

Using waitUntil for email sending ensures the user action completes quickly while the notification is sent asynchronously.

apps/web/lib/auth/totp.ts (2)

3-8: Standard TOTP configuration looks good.

The options follow RFC 6238 recommendations with appropriate defaults for issuer, digits, and period.


14-26: Function implementation is correct for TOTP instance creation.

The getTOTPInstance function properly spreads options and accepts the necessary parameters.

packages/email/src/templates/two-factor-enabled.tsx (1)

17-21: LGTM! Consistent with the disabled template.

The template structure and security messaging appropriately notify users about 2FA being enabled.

apps/web/app/app.dub.co/(auth)/two-factor-challenge/page.tsx (3)

8-10: Good use of dynamic metadata with environment variables.

The metadata construction properly includes the app name from environment variables for better branding consistency.


12-17: Proper authentication guard with cookie validation.

The cookie check and redirect logic correctly prevents unauthorized access to the 2FA challenge page while allowing legitimate users to proceed.


19-31: Clean component structure with consistent layout.

The use of AuthLayout and proper semantic structure (heading + form) creates a good user experience for the 2FA challenge flow.

apps/web/lib/actions/auth/disable-two-factor-auth.ts (1)

1-44: Well-implemented 2FA disable action with proper security checks.

The server action correctly:

  • Ensures user authentication via authUserActionClient
  • Validates that 2FA is currently enabled before disabling
  • Clears all 2FA-related fields atomically
  • Sends notification email asynchronously using waitUntil

The implementation follows security best practices and maintains data consistency.

apps/web/lib/actions/auth/enable-two-factor-auth.ts (1)

1-55: Secure 2FA enablement flow with proper two-step verification.

The implementation correctly:

  • Validates that 2FA isn't already enabled
  • Generates and stores the TOTP secret without immediately enabling 2FA
  • Creates a QR code URL for authenticator app setup
  • Requires separate confirmation step for security

This two-step approach (setup + confirmation) is a security best practice that prevents accidental lockouts.

apps/web/app/app.dub.co/(auth)/two-factor-challenge/form.tsx (1)

1-67: Well-designed 2FA challenge form with good UX considerations.

The form implementation includes:

  • Proper input validation (6-digit numeric pattern)
  • Mobile-responsive autofocus behavior
  • Button state management (disabled until valid input)
  • Comprehensive error handling with user-friendly messages
  • Correct integration with NextAuth's "two-factor-challenge" provider

The user experience is well thought out for both mobile and desktop users.

apps/web/app/app.dub.co/(dashboard)/account/settings/security/two-factor-auth.tsx (1)

1-101: Comprehensive 2FA management component with excellent state management.

The component provides:

  • Clean integration between enable/disable modals
  • Proper state management for secrets and QR codes
  • Automatic user data refresh after 2FA state changes
  • Conditional UI that clearly shows current 2FA status
  • Appropriate loading states and error feedback
  • Good separation of concerns with dedicated modal hooks

The user experience flows smoothly between enabling and disabling 2FA.

apps/web/ui/modals/disable-two-factor-auth-modal.tsx (1)

1-92: Well-structured modal with excellent state management patterns.

The modal implementation provides:

  • Clear warning message about consequences of disabling 2FA
  • Proper success/error handling with user feedback
  • Clean custom hook pattern for state management
  • Good separation between modal logic and presentation
  • Appropriate button states and loading indicators

The useDisableTwoFactorAuthModal hook follows React best practices with proper memoization.

Comment on lines 189 to 192
provider: string | null;
twoFactorConfirmedAt: Date | null;
twoFactorRecoveryCodes: string[] | null;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Type inconsistency between Prisma schema and TypeScript interface.

There's a mismatch in how twoFactorRecoveryCodes is typed:

  • In the Prisma schema (packages/prisma/schema/schema.prisma line 33): String? (single string for comma-separated values)
  • In this TypeScript interface: string[] | null (array of strings)

This inconsistency could lead to runtime errors when the API returns a string but the frontend expects an array.

Ensure consistent typing by either:

  1. Changing the TypeScript type to string | null to match the schema
  2. Or updating the API layer to parse the comma-separated string into an array
- twoFactorRecoveryCodes: string[] | null;
+ twoFactorRecoveryCodes: string | null;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
provider: string | null;
twoFactorConfirmedAt: Date | null;
twoFactorRecoveryCodes: string[] | null;
}
provider: string | null;
twoFactorConfirmedAt: Date | null;
twoFactorRecoveryCodes: string | null;
}
🤖 Prompt for AI Agents
In apps/web/lib/types.ts around lines 189 to 192, the twoFactorRecoveryCodes
field is typed as string[] | null, but the Prisma schema defines it as a single
nullable string representing comma-separated values. To fix this inconsistency,
update the TypeScript type to string | null to match the Prisma schema, or
alternatively, modify the API layer to parse the comma-separated string into an
array so the frontend can correctly handle string[]. Choose one approach and
ensure both schema and TypeScript types align to prevent runtime errors.

Comment on lines +13 to +29
// if (loading) {
// return (
// <div className="rounded-lg border border-neutral-200 bg-white">
// <div className="flex flex-col gap-3 border-b border-neutral-200 p-5 sm:p-10">
// <h2 className="text-xl font-medium">Password</h2>
// <div className="h-3 w-56 rounded-full bg-neutral-100"></div>
// </div>
// <div className="p-5 sm:p-10">
// <div className="flex justify-between gap-2">
// <div className="h-3 w-56 rounded-full bg-neutral-100"></div>
// <div className="h-3 w-56 rounded-full bg-neutral-100"></div>
// </div>
// <div className="mt-5 h-3 rounded-full bg-neutral-100"></div>
// </div>
// </div>
// );
// }
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider the implications of removing loading state.

Commenting out the loading state might cause the page to render before user data is available, potentially leading to:

  • UI flicker when components transition from loading to loaded states
  • Brief display of incorrect states (e.g., showing "Enable 2FA" before checking if it's already enabled)

Consider keeping a minimal loading state or ensure child components handle their own loading states properly.

🤖 Prompt for AI Agents
In apps/web/app/app.dub.co/(dashboard)/account/settings/security/page-client.tsx
around lines 13 to 29, the loading state rendering is commented out, which can
cause UI flicker and incorrect display before user data is ready. To fix this,
restore a minimal loading state UI that shows placeholders or skeletons while
data is loading, or ensure all child components independently handle their
loading states to prevent premature rendering of incomplete or incorrect
information.

Comment on lines 10 to 12
export const totpSecret = new OTPAuth.Secret({
size: 20, // 160 bits = 32 characters
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

CRITICAL: Shared secret creates major security vulnerability.

Exporting a single totpSecret instance means all users would share the same 2FA secret, completely undermining the security of the 2FA system. Each user must have a unique secret.

Replace the exported constant with a function that generates a new secret per user:

-export const totpSecret = new OTPAuth.Secret({
-  size: 20, // 160 bits = 32 characters
-});
+export const generateTotpSecret = () => {
+  return new OTPAuth.Secret({
+    size: 20, // 160 bits = 32 characters
+  });
+};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const totpSecret = new OTPAuth.Secret({
size: 20, // 160 bits = 32 characters
});
export const generateTotpSecret = () => {
return new OTPAuth.Secret({
size: 20, // 160 bits = 32 characters
});
};
🤖 Prompt for AI Agents
In apps/web/lib/auth/totp.ts around lines 10 to 12, the current code exports a
single totpSecret instance, causing all users to share the same 2FA secret,
which is a major security flaw. Replace the exported constant with a function
that generates and returns a new OTPAuth.Secret instance each time it is called,
ensuring each user gets a unique secret for their 2FA.

</div>

<div className="bg-neutral-50">
<form => disable2FA()}>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix form submission to prevent default behavior.

The form's onSubmit handler should prevent the default form submission to avoid potential page refresh or navigation issues.

Apply this fix:

-        <form  => disable2FA()}>
+        <form  => { e.preventDefault(); disable2FA(); }}>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<form onSubmit={() => disable2FA()}>
<form onSubmit={(e) => { e.preventDefault(); disable2FA(); }}>
🤖 Prompt for AI Agents
In apps/web/ui/modals/disable-two-factor-auth-modal.tsx at line 47, the form's
onSubmit handler currently calls disable2FA() directly without preventing the
default form submission behavior. Modify the onSubmit handler to accept the
event parameter and call event.preventDefault() before invoking disable2FA() to
prevent page refresh or navigation issues.

Comment on lines +24 to +27
const [token, setToken] = useState("");
const [touched, setTouched] = useState(false);
const [error, setError] = useState<string | undefined>(undefined);

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

State isn’t reset when the modal re-opens

token, touched, and error keep their previous values if the modal is closed and re-opened in the same session, which produces confusing UI (old errors, red borders, pre-filled digits).
Reset local state whenever the modal becomes visible.

+  useEffect(() => {
+    if (showModal) {
+      setToken("");
+      setTouched(false);
+      setError(undefined);
+    }
+  }, [showModal]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [token, setToken] = useState("");
const [touched, setTouched] = useState(false);
const [error, setError] = useState<string | undefined>(undefined);
const [token, setToken] = useState("");
const [touched, setTouched] = useState(false);
const [error, setError] = useState<string | undefined>(undefined);
useEffect(() => {
if (showModal) {
setToken("");
setTouched(false);
setError(undefined);
}
}, [showModal]);
🤖 Prompt for AI Agents
In apps/web/ui/modals/enable-two-factor-auth-modal.tsx around lines 24 to 27,
the state variables token, touched, and error are not reset when the modal
re-opens, causing stale UI data to persist. Add an effect hook that listens for
the modal's visibility change and resets these state variables to their initial
values whenever the modal becomes visible.

Comment on lines 315 to 335
// Two-factor challenge
CredentialsProvider({
id: "two-factor-challenge",
name: "Two-factor challenge",
type: "credentials",
credentials: {
code: { type: "text" },
},
async authorize(credentials, req) {
if (!credentials) {
throw new Error("no-credentials");
}

const { code } = credentials;

if (!code) {
throw new Error("no-credentials");
}

// TODO:
// Rate limit the request
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Brute-force window: rate-limiting is still a TODO

two-factor-challenge provider lacks rate limiting, making it trivial to brute-force 6-digit TOTP codes within the two-minute window, especially for high-value accounts.

Implement the same ratelimit logic used in the credentials provider, keyed by user id or the JWT sub. Do this before TOTP validation to avoid unnecessary DB hits.

🤖 Prompt for AI Agents
In apps/web/lib/auth/options.ts around lines 315 to 335, the
two-factor-challenge credentials provider lacks rate limiting, allowing
brute-force attacks on TOTP codes. Implement the existing ratelimit logic used
in the credentials provider by applying it at the start of the authorize
function, keyed by the user id or JWT sub from the request. This should be done
before any TOTP validation or database access to prevent unnecessary load and
improve security.

Comment on lines +301 to +304
if (user.twoFactorConfirmedAt) {
await setTwoFactorAuthCookie(user);
throw new Error("two-factor-required");
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Race-condition risk when throwing after async cookie write

await setTwoFactorAuthCookie(user) is followed by throw new Error("two-factor-required").
If setTwoFactorAuthCookie ever fails or is significantly delayed (e.g. edge runtime latency), the error is thrown without the cookie, forcing an impossible challenge flow.

Add try/catch and ensure the cookie write succeeds before signalling 2FA:

-        if (user.twoFactorConfirmedAt) {
-          await setTwoFactorAuthCookie(user);
-          throw new Error("two-factor-required");
-        }
+        if (user.twoFactorConfirmedAt) {
+          try {
+            await setTwoFactorAuthCookie(user);
+          } catch (e) {
+            console.error("Unable to set 2FA cookie", e);
+            throw new Error("internal-error");
+          }
+          throw new Error("two-factor-required");
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (user.twoFactorConfirmedAt) {
await setTwoFactorAuthCookie(user);
throw new Error("two-factor-required");
}
if (user.twoFactorConfirmedAt) {
try {
await setTwoFactorAuthCookie(user);
} catch (e) {
console.error("Unable to set 2FA cookie", e);
throw new Error("internal-error");
}
throw new Error("two-factor-required");
}
🤖 Prompt for AI Agents
In apps/web/lib/auth/options.ts around lines 301 to 304, the code throws an
error immediately after awaiting setTwoFactorAuthCookie(user), which risks
throwing before the cookie is successfully set if the async call fails or is
delayed. To fix this, wrap the await setTwoFactorAuthCookie(user) call in a
try/catch block to handle any errors during cookie setting, ensuring the cookie
write completes successfully before throwing the "two-factor-required" error.

Comment on lines +35 to +56
const setTwoFactorAuthCookie = async (user: Pick<User, "id" | "email">) => {
const token = await encode({
secret: process.env.NEXTAUTH_SECRET as string,
maxAge: 2 * 60,
token: {
sub: user.id,
email: user.email,
purpose: "2fa",
iat: Math.floor(Date.now() / 1000),
},
});

cookies().set({
name: TWO_FA_COOKIE_NAME,
value: token,
path: "/",
httpOnly: true,
secure: VERCEL_DEPLOYMENT,
expires: new Date(Date.now() + 2 * 60 * 1000),
sameSite: "lax",
});
};
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

❓ Verification inconclusive

Cookie security: consider SameSite="strict"

The 2-minute 2FA cookie is sensitive – it should never be sent in a cross-site context. sameSite: "lax" allows it on top-level GET navigations, which an attacker could exploit if they can trick the user into following a link before the cookie expires.

-    sameSite: "lax",
+    sameSite: "strict",

Also consider adding priority: "high" for modern browsers.


Cookie security: enforce SameSite="strict" for the 2FA cookie

The 2-minute 2FA cookie carries highly sensitive data and must never be sent in a cross-site context. sameSite: "lax" still allows it on top-level GET navigations, which an attacker could exploit.

• File: apps/web/lib/auth/options.ts
Function: setTwoFactorAuthCookie
Update the cookie options as follows:

-    sameSite: "lax",
+    sameSite: "strict",
+    priority: "high",    // ensure this cookie is sent ahead of lower-priority cookies

This change prevents the cookie from being sent on any cross-site navigation.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
47C5
const setTwoFactorAuthCookie = async (user: Pick<User, "id" | "email">) => {
const token = await encode({
secret: process.env.NEXTAUTH_SECRET as string,
maxAge: 2 * 60,
token: {
sub: user.id,
email: user.email,
purpose: "2fa",
iat: Math.floor(Date.now() / 1000),
},
});
cookies().set({
name: TWO_FA_COOKIE_NAME,
value: token,
path: "/",
httpOnly: true,
secure: VERCEL_DEPLOYMENT,
expires: new Date(Date.now() + 2 * 60 * 1000),
sameSite: "lax",
});
};
const setTwoFactorAuthCookie = async (user: Pick<User, "id" | "email">) => {
const token = await encode({
secret: process.env.NEXTAUTH_SECRET as string,
maxAge: 2 * 60,
token: {
sub: user.id,
email: user.email,
purpose: "2fa",
iat: Math.floor(Date.now() / 1000),
},
});
cookies().set({
name: TWO_FA_COOKIE_NAME,
value: token,
path: "/",
httpOnly: true,
secure: VERCEL_DEPLOYMENT,
expires: new Date(Date.now() + 2 * 60 * 1000),
sameSite: "strict",
priority: "high",
});
};
🤖 Prompt for AI Agents
In apps/web/lib/auth/options.ts around lines 35 to 56, update the
setTwoFactorAuthCookie function to set the cookie's sameSite attribute to
"strict" instead of "lax" to prevent the 2FA cookie from being sent in any
cross-site context. Additionally, add the priority: "high" option to the cookie
settings to ensure modern browsers treat this cookie with higher precedence.
This enhances the security of the sensitive 2FA cookie.

Copy link
Contributor
@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (8)
apps/web/lib/types.ts (1)

190-191: Type inconsistency between Prisma schema and TypeScript interface.

There's a mismatch in how twoFactorRecoveryCodes is typed:

  • In the Prisma schema: String? (single string for comma-separated values)
  • In this TypeScript interface: string[] | null (array of strings)

This inconsistency could lead to runtime errors when the API returns a string but the frontend expects an array.

Ensure consistent typing by either:

  1. Changing the TypeScript type to string | null to match the schema
  2. Or updating the API layer to parse the comma-separated string into an array
- twoFactorRecoveryCodes: string[] | null;
+ twoFactorRecoveryCodes: string | null;
apps/web/app/app.dub.co/(dashboard)/account/settings/security/page-client.tsx (1)

13-29: Consider the implications of removing loading state.

The commented-out loading state could cause UI flicker and incorrect display before user data is available. While the TwoFactorAuth component handles its own loading state, the password section might still render prematurely.

apps/web/lib/auth/totp.ts (1)

10-12: ⚠️ Potential issue

CRITICAL: Shared secret creates major security vulnerability.

This is the same critical security issue identified in the previous review. The exported totpSecret constant means all users would share the same 2FA secret, completely undermining the security of the 2FA system.

apps/web/ui/modals/disable-two-factor-auth-modal.tsx (1)

47-47: Fix form submission to prevent default behavior.

The form's onSubmit handler should prevent the default form submission to avoid potential page refresh or navigation issues.

Apply this fix:

-        <form  => disable2FA()}>
+        <form  => { e.preventDefault(); disable2FA(); }}>
apps/web/ui/modals/enable-two-factor-auth-modal.tsx (1)

24-27: State isn't reset when the modal re-opens

token, touched, and error keep their previous values if the modal is closed and re-opened in the same session, which produces confusing UI (old errors, red borders, pre-filled digits).
Reset local state whenever the modal becomes visible.

Add an effect hook to reset state when modal opens:

+import { useCallback, useEffect, useMemo, useState } from "react";
-import { useCallback, useMemo, useState } from "react";

After line 27, add:

+  useEffect(() => {
+    if (showModal) {
+      setToken("");
+      setTouched(false);
+      setError(undefined);
+    }
+  }, [showModal]);
apps/web/lib/auth/options.ts (3)

35-56: Cookie security: enforce SameSite="strict" for the 2FA cookie

The 2-minute 2FA cookie carries highly sensitive data and must never be sent in a cross-site context. sameSite: "lax" still allows it on top-level GET navigations, which an attacker could exploit.

-    sameSite: "lax",
+    sameSite: "strict",
+    priority: "high",

301-304: Race-condition risk when throwing after async cookie write

await setTwoFactorAuthCookie(user) is followed by throw new Error("two-factor-required").
If setTwoFactorAuthCookie ever fails or is significantly delayed (e.g. edge runtime latency), the error is thrown without the cookie, forcing an impossible challenge flow.

         if (user.twoFactorConfirmedAt) {
-          await setTwoFactorAuthCookie(user);
-          throw new Error("two-factor-required");
+          try {
+            await setTwoFactorAuthCookie(user);
+          } catch (e) {
+            console.error("Unable to set 2FA cookie", e);
+            throw new Error("internal-error");
+          }
+          throw new Error("two-factor-required");
         }

334-335: ⚠️ Potential issue

Brute-force window: rate-limiting is still a TODO

two-factor-challenge provider lacks rate limiting, making it trivial to brute-force 6-digit TOTP codes within the two-minute window, especially for high-value accounts.

Replace the TODO with actual rate limiting implementation:

-        // TODO:
-        // Rate limit the request
+        const { sub, email } = decoded as any;
+        
+        const { success } = await ratelimit(5, "1 m").limit(
+          `2fa-attempts:${sub || email}`,
+        );
+
+        if (!success) {
+          throw new Error("too-many-2fa-attempts");
+        }

Move this rate limiting check before the cookie operations to avoid unnecessary processing.

🧹 Nitpick comments (5)
apps/web/app/app.dub.co/(auth)/two-factor-challenge/form.tsx (1)

47-56: Enhance input validation for better UX and security.

The current implementation allows any characters in the 6-digit code field. Consider these improvements:

            <Input
              type="text"
              autoFocus={!isMobile}
              value={code}
              placeholder="012345"
              pattern="[0-9]*"
+             inputMode="numeric"
               => setCode(e.target.value)}
              maxLength={6}
+              => {
+               e.currentTarget.value = e.currentTarget.value.replace(/\D/g, '');
+               setCode(e.currentTarget.value);
+             }}
            />

This ensures only numeric input is accepted and provides better mobile keyboard experience.

apps/web/app/app.dub.co/(dashboard)/account/settings/security/two-factor-auth.tsx (1)

34-38: Improve error handling specificity.

The current error handling is generic. Consider providing more specific error messages based on the type of failure:

      onSuccess: async ({ data }) => {
        if (!data) {
-         toast.error("Failed to enable 2FA. Please try again.");
+         toast.error("Failed to generate 2FA setup data. Please try again.");
          return;
        }
+       if (!data.secret || !data.qrCodeUrl) {
+         toast.error("Incomplete 2FA setup data received. Please try again.");
+         return;
+       }

        setSecret(data.secret);
        setQrCodeUrl(data.qrCodeUrl);
        setShowEnableTwoFactorAuthModal(true);
      },
apps/web/ui/modals/enable-two-factor-auth-modal.tsx (2)

41-49: Prevent duplicate form submissions

While the button is disabled when isPending, the form could still be submitted via Enter key or if there's a race condition. Consider adding a guard to prevent multiple concurrent requests.

   const confirmTwoFactorAuth = async (e: React.FormEvent) => {
     e.preventDefault();
+    if (isPending) return;
     setTouched(true);
     setError(undefined);

     await executeAsync({
       token,
     });
   };

84-84: Simplify complex conditional styling

The error styling logic is complex and hard to read. Consider extracting it into a separate variable for better maintainability.

-                    className={`relative flex h-14 w-12 items-center justify-center rounded-lg border bg-white text-xl transition-all ${isActive ? "z-10 border-neutral-800 ring-2 ring-neutral-200" : "border-neutral-200"} ${(touched && token.length < 6) || error ? "border-red-500 ring-red-200" : ""}`}
+                    className={`relative flex h-14 w-12 items-center justify-center rounded-lg border bg-white text-xl transition-all ${
+                      isActive 
+                        ? "z-10 border-neutral-800 ring-2 ring-neutral-200" 
+                        : "border-neutral-200"
+                    } ${
+                      (touched && token.length < 6) || error 
+                        ? "border-red-500 ring-red-200" 
+                        : ""
+                    }`}
apps/web/lib/auth/options.ts (1)

372-398: Sensitive information in error logs

Multiple console.error statements include user email and ID which could leak sensitive information in production logs. Consider using structured logging with appropriate log levels or removing these in production.

         if (!user) {
-          console.error("User not found", { sub, email });
+          if (process.env.NODE_ENV === "development") {
+            console.error("User not found", { sub, email });
+          }
           throw new Error("invalid-credentials");
         }

         if (!user.twoFactorConfirmedAt) {
-          console.error("Two-factor not confirmed", { sub, email });
+          if (process.env.NODE_ENV === "development") {
+            console.error("Two-factor not confirmed", { sub, email });
+          }
           throw new Error("invalid-credentials");
         }

         if (!user.twoFactorSecret) {
-          console.error("Two-factor secret not found", { sub, email });
+          if (process.env.NODE_ENV === "development") {
+            console.error("Two-factor secret not found", { sub, email });
+          }
           throw new Error("invalid-credentials");
         }

And after line 398:

         if (delta === null) {
-          console.error("Invalid 2FA code entered", { sub, email });
+          if (process.env.NODE_ENV === "development") {
+            console.error("Invalid 2FA code entered", { sub, email });
+          }
           throw new Error("invalid-2fa-code");
         }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bc41d19 and 3e1972a.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (22)
  • apps/web/app/api/user/route.ts (1 hunks)
  • apps/web/app/app.dub.co/(auth)/two-factor-challenge/form.tsx (1 hunks)
  • apps/web/app/app.dub.co/(auth)/two-factor-challenge/page.tsx (1 hunks)
  • apps/web/app/app.dub.co/(dashboard)/account/settings/security/page-client.tsx (1 hunks)
  • apps/web/app/app.dub.co/(dashboard)/account/settings/security/two-factor-auth.tsx (1 hunks)
  • apps/web/lib/actions/auth/confirm-two-factor-auth.ts (1 hunks)
  • apps/web/lib/actions/auth/disable-two-factor-auth.ts (1 hunks)
  • apps/web/lib/actions/auth/enable-two-factor-auth.ts (1 hunks)
  • apps/web/lib/auth/constants.ts (1 hunks)
  • apps/web/lib/auth/options.ts (3 hunks)
  • apps/web/lib/auth/totp.ts (1 hunks)
  • apps/web/lib/middleware/app.ts (1 hunks)
  • apps/web/lib/swr/use-user.ts (1 hunks)
  • apps/web/lib/types.ts (1 hunks)
  • apps/web/package.json (1 hunks)
  • apps/web/ui/auth/login/email-sign-in.tsx (1 hunks)
  • apps/web/ui/auth/login/login-form.tsx (1 hunks)
  • apps/web/ui/modals/disable-two-factor-auth-modal.tsx (1 hunks)
  • apps/web/ui/modals/enable-two-factor-auth-modal.tsx (1 hunks)
  • packages/email/src/templates/two-factor-disabled.tsx (1 hunks)
  • packages/email/src/templates/two-factor-enabled.tsx (1 hunks)
  • packages/prisma/schema/schema.prisma (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (8)
apps/web/lib/swr/use-user.ts (1)
apps/web/lib/types.ts (1)
  • UserProps (178-192)
apps/web/app/app.dub.co/(dashboard)/account/settings/security/page-client.tsx (2)
apps/web/lib/swr/use-user.ts (1)
  • useUser (5-13)
apps/web/app/app.dub.co/(dashboard)/account/settings/security/two-factor-auth.tsx (1)
  • TwoFactorAuth (12-101)
apps/web/app/app.dub.co/(auth)/two-factor-challenge/page.tsx (3)
packages/utils/src/functions/construct-metadata.ts (1)
  • constructMetadata (3-89)
apps/web/lib/auth/constants.ts (1)
  • TWO_FA_COOKIE_NAME (12-12)
apps/web/app/app.dub.co/(auth)/two-factor-challenge/form.tsx (1)
  • TwoFactorChallengeForm (9-67)
apps/web/lib/actions/auth/disable-two-factor-auth.ts (3)
apps/web/lib/actions/safe-action.ts (1)
  • authUserActionClient (19-31)
packages/email/src/index.ts (1)
  • sendEmail (6-29)
packages/email/src/templates/two-factor-disabled.tsx (1)
  • TwoFactorDisabled (17-49)
apps/web/lib/actions/auth/confirm-two-factor-auth.ts (4)
apps/web/lib/actions/safe-action.ts (1)
  • authUserActionClient (19-31)
apps/web/lib/auth/totp.ts (1)
  • getTOTPInstance (14-26)
packages/email/src/index.ts (1)
  • sendEmail (6-29)
packages/email/src/templates/two-factor-enabled.tsx (1)
  • TwoFactorEnabled (17-49)
packages/email/src/templates/two-factor-enabled.tsx (2)
packages/utils/src/constants/main.ts (1)
  • DUB_WORDMARK (68-68)
packages/ui/src/footer.tsx (1)
  • Footer (85-310)
apps/web/ui/modals/disable-two-factor-auth-modal.tsx (2)
apps/web/lib/swr/use-user.ts (1)
  • useUser (5-13)
apps/web/lib/actions/auth/disable-two-factor-auth.ts (1)
  • disableTwoFactorAuthAction (10-44)
apps/web/app/app.dub.co/(dashboard)/account/settings/security/two-factor-auth.tsx (4)
apps/web/lib/swr/use-user.ts (1)
  • useUser (5-13)
apps/web/ui/modals/enable-two-factor-auth-modal.tsx (1)
  • useEnableTwoFactorAuthModal (117-148)
apps/web/ui/modals/disable-two-factor-auth-modal.tsx (1)
  • useDisableTwoFactorAuthModal (72-92)
apps/web/lib/actions/auth/enable-two-factor-auth.ts (1)
  • enableTwoFactorAuthAction (8-55)
🔇 Additional comments (27)
apps/web/package.json (1)

99-99:

✅ Verification successful

Verify the otpauth package version for security and currency.

Ensure that version 9.4.0 is the latest stable release and check for any known security vulnerabilities in the otpauth package.


🌐 Web query:

What is the latest version of the otpauth npm package and are there any known security vulnerabilities in version 9.4.0?

💡 Result:

The latest version of the otpauth npm package is 9.4.0, published approximately a month ago. (npmjs.com)

Regarding security vulnerabilities, there are no known issues in version 9.4.0. The most recent version with a reported vulnerability is 9.3.6, which has a high-severity authentication bypass vulnerability affecting versions prior to 3.2.8. (security.snyk.io) Since version 9.4.0 is newer than 3.2.8, it does not contain this vulnerability.

For more details on the latest releases and their changes, you can refer to the GitHub repository's release notes. (github.com)

Citations:


otpauth Dependency Update Verified

The otpauth dependency added in apps/web/package.json (line 99) is at the latest stable release (v9.4.0) and has no reported security vulnerabilities. No further action is required.

apps/web/lib/auth/constants.ts (1)

12-12: LGTM! Well-structured 2FA cookie constant.

The constant follows good naming conventions and includes the app identifier to prevent cookie conflicts.

apps/web/app/api/user/route.ts (1)

42-43: LGTM! Proper extension of user data for 2FA support.

The addition of twoFactorConfirmedAt and twoFactorRecoveryCodes fields appropriately extends the user API response to support 2FA functionality and UI state management.

apps/web/ui/auth/login/email-sign-in.tsx (1)

95-98: LGTM! Proper 2FA flow handling in authentication.

The conditional check correctly identifies when 2FA is required and redirects users to the challenge page, providing a smooth authentication experience.

apps/web/lib/middleware/app.ts (1)

30-30: LGTM: Correctly allows unauthenticated access to 2FA challenge page.

This change properly integrates with the 2FA authentication flow by allowing users to access the two-factor challenge page without being fully authenticated, consistent with other authentication-related paths.

packages/prisma/schema/schema.prisma (1)

31-33: LGTM: Well-designed schema additions for 2FA support.

The three new optional fields appropriately support the 2FA implementation:

  • twoFactorSecret for storing TOTP secrets
  • twoFactorConfirmedAt for tracking 2FA enablement status
  • twoFactorRecoveryCodes for backup recovery options

The optional nature ensures backward compatibility with existing users.

apps/web/lib/swr/use-user.ts (2)

2-2: LGTM: Appropriate change to support data revalidation.

Switching from useSWRImmutable to useSWR is correct for the 2FA implementation, as user data can now change when 2FA is enabled/disabled.


6-11: LGTM: Exposing mutate function enables proper state management.

Returning the mutate function allows 2FA components to refresh user data after state changes, ensuring the UI stays synchronized with the backend state.

apps/web/ui/auth/login/login-form.tsx (1)

47-52: LGTM: Comprehensive error handling for 2FA scenarios.

The error codes appropriately cover the main 2FA authentication scenarios with clear, user-friendly messages. The duplicate messages for "no-2fa-token" and "invalid-2fa-token" are likely intentional for security reasons.

apps/web/app/app.dub.co/(dashboard)/account/settings/security/page-client.tsx (2)

5-5: LGTM: Clean integration of TwoFactorAuth component.

The import and usage of the TwoFactorAuth component properly integrates 2FA management into the security settings page.

Also applies to: 37-37


31-39: Well-structured layout for security settings.

The new layout cleanly separates password management and 2FA settings, providing a logical organization for security-related features.

packages/email/src/templates/two-factor-disabled.tsx (1)

17-49: LGTM! Well-structured email template for security notifications.

The email template follows best practices with clear messaging, proper branding, and includes a support contact link for users who didn't initiate the change. The component structure and styling are appropriate for email rendering.

apps/web/lib/actions/auth/confirm-two-factor-auth.ts (4)

11-13: Good input validation with appropriate constraints.

The Zod schema correctly validates the 6-digit token format, ensuring proper input before processing.


32-38: Proper state validation and error handling.

The checks for existing 2FA confirmation and missing secret prevent invalid state transitions and provide clear error messages.


44-53: Correct TOTP validation with appropriate time window.

The window: 1 parameter provides a ±30-second tolerance for clock skew, which is a security best practice for TOTP validation. The delta check properly handles both valid and invalid tokens.


65-72: Proper async email notification with waitUntil.

Using waitUntil for the email notification ensures the user response isn't delayed while still guaranteeing the email is sent. The email content and variant are appropriate for this security event.

apps/web/lib/auth/totp.ts (1)

14-26: LGTM! Proper TOTP instance factory function.

The getTOTPInstance function correctly accepts individual secrets and optional labels, allowing each user to have their own unique TOTP configuration.

packages/email/src/templates/two-factor-enabled.tsx (1)

17-49: LGTM! Consistent email template for 2FA enabled notifications.

The template maintains consistency with the disabled notification template while providing appropriate messaging for the enabled state. The security advisory to contact support if the change wasn't initiated by the user is a good security practice.

apps/web/app/app.dub.co/(auth)/two-factor-challenge/page.tsx (3)

8-10: Good metadata configuration with dynamic app name.

The metadata construction properly includes the application name from environment variables, providing proper SEO and browser tab context.


13-17: Correct 2FA flow enforcement with cookie validation.

The cookie check ensures users can only access this page as part of the proper 2FA challenge flow, redirecting unauthorized access back to login.


19-31: Clean UI structure with proper layout and form integration.

The component uses consistent auth layout patterns and provides clear context with the heading before rendering the challenge form.

apps/web/lib/actions/auth/disable-two-factor-auth.ts (1)

10-44: LGTM! Well-implemented 2FA disable action.

The implementation correctly:

  • Validates user authentication via authUserActionClient
  • Checks if 2FA is currently enabled before attempting to disable
  • Atomically clears all 2FA-related fields
  • Sends notification email asynchronously without blocking the response

Minor suggestion: Consider making the error message on line 21 more user-friendly, e.g., "Two-factor authentication is not currently enabled for your account."

apps/web/lib/actions/auth/enable-two-factor-auth.ts (1)

28-37: LGTM! Proper storage of unconfirmed 2FA secret.

The comment correctly explains that this step only stores the secret without enabling 2FA, requiring user confirmation via TOTP code. This is the correct security approach for 2FA setup.

apps/web/app/app.dub.co/(auth)/two-factor-challenge/form.tsx (1)

14-37: LGTM! Proper 2FA challenge submission handling.

The form correctly:

  • Prevents default form submission
  • Uses the appropriate NextAuth provider
  • Handles loading states
  • Provides user-friendly error messages
  • Disables redirect to handle errors manually
apps/web/app/app.dub.co/(dashboard)/account/settings/security/two-factor-auth.tsx (1)

75-95: LGTM! Well-implemented button logic and state management.

The button correctly:

  • Shows appropriate text based on loading and 2FA status
  • Uses proper variants (danger for disable, primary for enable)
  • Handles loading states from both user fetch and enable action
  • Triggers the correct modal or action based on current 2FA status
apps/web/ui/modals/disable-two-factor-auth-modal.tsx (2)

19-31: LGTM! Proper server action integration with error handling.

The modal correctly:

  • Uses the authenticated server action
  • Provides success feedback with toast
  • Closes modal and refreshes user data on success
  • Shows server error messages on failure
  • Handles loading states appropriately

72-92: LGTM! Well-structured custom hook with proper memoization.

The hook correctly:

  • Manages modal state
  • Provides memoized modal component to prevent unnecessary re-renders
  • Returns both setter and component for easy integration

Comment on lines 22 to 26
const secret = totpSecret.base32;

if (!secret) {
throw new Error("Failed to generate 2FA secret.");
}
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify that TOTP secret generation is unique per request.

The code accesses totpSecret.base32 directly, which raises a concern about whether a fresh secret is generated for each enable request. Reusing secrets across different users or requests would be a critical security vulnerability.

Please verify that totpSecret generates a new unique secret for each invocation:


🏁 Script executed:

#!/bin/bash
# Description: Check the implementation of totpSecret to ensure it generates unique secrets per request
# Expected: totpSecret should be a function call or generate fresh secrets, not a static value

ast-grep --pattern 'export const totpSecret = $_'
ast-grep --pattern 'export function totpSecret($_) { $$$ }'
ast-grep --pattern 'const totpSecret = $_'

Length of output: 514


Ensure TOTP secret is generated per request

Our verification shows that in apps/web/lib/auth/totp.ts you export a single OTPAuth.Secret instance at module load time:

  • apps/web/lib/auth/totp.ts:10
    export const totpSecret = new OTPAuth.Secret({
      size: 20,
    });

This means the same base32 secret is reused across all users/requests—a critical security risk. You must generate a fresh secret each time.

Locations to update:

  • apps/web/lib/auth/totp.ts
  • apps/web/lib/actions/auth/enable-two-factor-auth.ts

Suggested changes:

--- a/apps/web/lib/auth/totp.ts
- export const totpSecret = new OTPAuth.Secret({
-   size: 20,
- });
+ export function createTotpSecret(): OTPAuth.Secret {
+   return new OTPAuth.Secret({
+     size: 20,
+   });
+ }
--- a/apps/web/lib/actions/auth/enable-two-factor-auth.ts
- import { totpSecret } from 'apps/web/lib/auth/totp';
+ import { createTotpSecret } from 'apps/web/lib/auth/totp';- const secret = totpSecret.base32;
+ const secretInstance = createTotpSecret();
+ const secret = secretInstance.base32;

This ensures each enable-2FA action uses a unique TOTP secret.

🤖 Prompt for AI Agents
In apps/web/lib/actions/auth/enable-two-factor-auth.ts around lines 22 to 26,
the code uses a static totpSecret instance imported from
apps/web/lib/auth/totp.ts, causing the same TOTP secret to be reused for all
users, which is a security risk. To fix this, refactor apps/web/lib/auth/totp.ts
to export a function that generates and returns a new OTPAuth.Secret instance on
each call instead of a single static instance. Then, update
enable-two-factor-auth.ts to call this function to obtain a fresh secret for
every enable-2FA request, ensuring unique secrets per user.

window: 1,
});

console.log("totp delta", delta);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Remove debug logging in production

Debug console.log statement should be removed as it may leak information in production logs.

-        console.log("totp delta", delta);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
console.log("totp delta", delta);
🤖 Prompt for AI Agents
In apps/web/lib/auth/options.ts at line 395, remove the debug console.log
statement that outputs "totp delta" and its value to prevent sensitive
information from being logged in production environments.

throw new Error("no-2fa-token");
}

cookies().delete(TWO_FA_COOKIE_NAME);
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify cookie deletion behavior across different scenarios

The 2FA cookie is deleted immediately after reading, but if the subsequent validation fails, the user would need to login again from scratch. Consider deleting the cookie only after successful validation.


🏁 Script executed:

#!/bin/bash
# Search for how the two-factor-required error is handled in the UI
rg -A 5 "two-factor-required" --type tsx --type ts

# Check if there's any retry mechanism for 2FA
ast-grep --pattern 'error === "two-factor-required"'

Length of output: 133


🏁 Script executed:

#!/bin/bash
# Search for any occurrences of the two-factor-required error code
rg -n "two-factor-required"

echo "-----"

# Show surrounding lines where the 2FA cookie is deleted to inspect subsequent validation/error handling
rg -n -A10 "cookies().delete(TWO_FA_COOKIE_NAME)" apps/web/lib/auth/options.ts

Length of output: 418


🏁 Script executed:

#!/bin/bash
# Find the definition of the 2FA cookie constant
rg -n "TWO_FA_COOKIE_NAME" -A3 -B3 .

# Inspect all cookie-related calls in the 2FA handling file
rg -n "cookies" -A5 -B5 apps/web/lib/auth/options.ts

# Show context around the two-factor-required error throw
rg -n -A5 -B5 "two-factor-required" apps/web/lib/auth/options.ts

Length of output: 5077


Delay deleting 2FA cookie until after successful decoding and validation

Ensure that TWO_FA_COOKIE_NAME is only removed once the token is successfully decoded and the one-time code has been verified. Deleting it before decode() (and any OTP checks) forces users back to login on any decode/validation failure.

• File: apps/web/lib/auth/options.ts
– Remove or move the call at line 343 (cookies().delete(TWO_FA_COOKIE_NAME))
– Place it after the decode() call and after your 2FA code has been confirmed

Suggested diff:

- const cookie = cookies().get(TWO_FA_COOKIE_NAME);
- if (!cookie) {
-   throw new Error("no-2fa-token");
- }
- cookies().delete(TWO_FA_COOKIE_NAME);
- const decoded = await decode({ token: cookie.value, secret: process.env.NEXTAUTH_SECRET });
+ const cookie = cookies().get(TWO_FA_COOKIE_NAME);
+ if (!cookie) {
+   throw new Error("no-2fa-token");
+ }
+ const decoded = await decode({ token: cookie.value, secret: process.env.NEXTAUTH_SECRET });
+ // Only delete the cookie after decode + 2FA code validation succeeds
+ cookies().delete(TWO_FA_COOKIE_NAME);

This change preserves the cookie on decode/validation errors, allowing users to retry entering their 2FA code instead of restarting the login flow from scratch.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cookies().delete(TWO_FA_COOKIE_NAME);
const cookie = cookies().get(TWO_FA_COOKIE_NAME);
if (!cookie) {
throw new Error("no-2fa-token");
}
const decoded = await decode({ token: cookie.value, secret: process.env.NEXTAUTH_SECRET });
// Only delete the cookie after decode + 2FA code validation succeeds
cookies().delete(TWO_FA_COOKIE_NAME);
🤖 Prompt for AI Agents
In apps/web/lib/auth/options.ts at line 343, the TWO_FA_COOKIE_NAME cookie is
deleted immediately after reading, which causes users to restart login if
validation fails. To fix this, move the cookies().delete(TWO_FA_COOKIE_NAME)
call so it occurs only after the token is successfully decoded and the one-time
2FA code is verified. This ensures the cookie remains available for retries on
decode or validation errors, improving user experience.

devkiran added 5 commits June 10, 2025 10:23
- Updated UserProps interface to change twoFactorRecoveryCodes type from string[] to string.
- Implemented rate limiting for 2FA attempts in authOptions.
- Modified DisableTwoFactorAuthModal to prevent default form submission behavior.
- Added useEffect in EnableTwoFactorAuthModal to reset state when modal is shown.
Copy link
bug0-qa-agent bot commented Jun 10, 2025

🤖 Bug0 QA Agent

Here are the results of the automated tests for PR #2505:

To re-run the tests, please comment /bug0 run or push a new commit to this PR.

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.

1 participant
0