Skip to content

Conversation

@hussam789
Copy link

@hussam789 hussam789 commented Oct 30, 2025

User description

PR #2


PR Type

Enhancement


Description

  • Add backup codes feature for two-factor authentication recovery

  • Generate and display 10 backup codes during 2FA setup with download/copy options

  • Allow users to disable 2FA or login using backup codes when authenticator unavailable

  • Store encrypted backup codes in database and validate during authentication

  • Add UI components and error handling for backup code workflows


Diagram Walkthrough

flowchart LR
  A["2FA Setup"] -->|Generate| B["10 Backup Codes"]
  B -->|Display & Download| C["User Stores Codes"]
  D["Login/Disable 2FA"] -->|Lost Access| E["Use Backup Code"]
  E -->|Validate & Consume| F["Grant Access"]
  B -->|Encrypt & Store| G["Database"]
Loading

File Walkthrough

Relevant files
Enhancement
10 files
BackupCode.tsx
New backup code input component                                                   
+29/-0   
TwoFactor.tsx
Add autoFocus prop to TwoFactor component                               
+2/-2     
DisableTwoFactorModal.tsx
Add backup code support to disable 2FA modal                         
+35/-7   
EnableTwoFactorModal.tsx
Add backup codes display and download functionality           
+76/-7   
TwoFactorAuthAPI.ts
Update disable API to accept backup code parameter             
+2/-2     
disable.ts
Add backup code validation for disabling 2FA                         
+25/-2   
setup.ts
Generate and return backup codes during 2FA setup               
+6/-1     
login.tsx
Add backup code login option with lost access flow             
+36/-12 
ErrorCode.ts
Add backup code error codes                                                           
+2/-0     
next-auth-options.ts
Add backup code authentication logic to credentials provider
+30/-2   
Tests
2 files
login.2fa.e2e.ts
Add backup code download and copy tests                                   
+21/-0   
builder.ts
Add backupCodes field to test user builder                             
+1/-0     
Documentation
1 files
common.json
Add backup code related translation strings                           
+7/-0     
Configuration changes
2 files
migration.sql
Add backupCodes column to users table                                       
+2/-0     
schema.prisma
Add backupCodes field to User model                                           
+1/-0     
Bug fix
1 files
Input.tsx
Add tabIndex to password visibility toggle button               
+5/-1     

Co-authored-by: Peer Richelsen <[email protected]>
@qodo-code-review
Copy link

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Backup code handling

Description: Backup codes are only replaced with null upon use and the entire array is re-encrypted,
potentially leaving unused plaintext-like null placeholders and not rotating the
encryption key or re-randomizing structure, which could aid traffic analysis; consider
removing consumed entries entirely and enforcing rate-limiting on backup code attempts.
next-auth-options.ts [123-156]

Referred Code
    throw new Error(ErrorCode.IncorrectEmailPassword);
  }
  const isCorrectPassword = await verifyPassword(credentials.password, user.password);
  if (!isCorrectPassword) {
    throw new Error(ErrorCode.IncorrectEmailPassword);
  }
}

if (user.twoFactorEnabled && credentials.backupCode) {
  if (!process.env.CALENDSO_ENCRYPTION_KEY) {
    console.error("Missing encryption key; cannot proceed with backup code login.");
    throw new Error(ErrorCode.InternalServerError);
  }

  if (!user.backupCodes) throw new Error(ErrorCode.MissingBackupCodes);

  const backupCodes = JSON.parse(
    symmetricDecrypt(user.backupCodes, process.env.CALENDSO_ENCRYPTION_KEY)
  );

  // check if user-supplied code matches one


 ... (clipped 13 lines)
Backup code reuse risk

Description: Disabling 2FA via backup code does not consume a single-use code and instead clears all
codes after disabling, enabling unlimited retry attempts on the same code during this
flow; add rate limiting and consume the code upon successful verification.
disable.ts [47-66]

Referred Code
// if user has 2fa and using backup code
if (user.twoFactorEnabled && req.body.backupCode) {
  if (!process.env.CALENDSO_ENCRYPTION_KEY) {
    console.error("Missing encryption key; cannot proceed with backup code login.");
    throw new Error(ErrorCode.InternalServerError);
  }

  if (!user.backupCodes) {
    return res.status(400).json({ error: ErrorCode.MissingBackupCodes });
  }

  const backupCodes = JSON.parse(symmetricDecrypt(user.backupCodes, process.env.CALENDSO_ENCRYPTION_KEY));

  // check if user-supplied code matches one
  const index = backupCodes.indexOf(req.body.backupCode.replaceAll("-", ""));
  if (index === -1) {
    return res.status(400).json({ error: ErrorCode.IncorrectBackupCode });
  }

  // we delete all stored backup codes at the end, no need to do this here
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Missing audit log: New critical actions around disabling 2FA and using backup codes lack explicit audit
logging in the added code.

Referred Code
// if user has 2fa and using backup code
if (user.twoFactorEnabled && req.body.backupCode) {
  if (!process.env.CALENDSO_ENCRYPTION_KEY) {
    console.error("Missing encryption key; cannot proceed with backup code login.");
    throw new Error(ErrorCode.InternalServerError);
  }

  if (!user.backupCodes) {
    return res.status(400).json({ error: ErrorCode.MissingBackupCodes });
  }

  const backupCodes = JSON.parse(symmetricDecrypt(user.backupCodes, process.env.CALENDSO_ENCRYPTION_KEY));

  // check if user-supplied code matches one
  const index = backupCodes.indexOf(req.body.backupCode.replaceAll("-", ""));
  if (index === -1) {
    return res.status(400).json({ error: ErrorCode.IncorrectBackupCode });
  }

  // we delete all stored backup codes at the end, no need to do this here



 ... (clipped 41 lines)
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Env key assumption: The setup handler uses encryption without validating the presence of the required
encryption key before encrypting backup codes, which may fail at runtime if the key is
unset.

Referred Code
  // generate backup codes with 10 character length
  const backupCodes = Array.from(Array(10), () => crypto.randomBytes(5).toString("hex"));

  await prisma.user.update({
    where: {
      id: session.user.id,
    },
    data: {
      backupCodes: symmetricEncrypt(JSON.stringify(backupCodes), process.env.CALENDSO_ENCRYPTION_KEY),
      twoFactorEnabled: false,
      twoFactorSecret: symmetricEncrypt(secret, process.env.CALENDSO_ENCRYPTION_KEY),
    },
  });

  const name = user.email || user.username || user.id.toString();
  const keyUri = authenticator.keyuri(name, "Cal", secret);
  const dataUri = await qrcode.toDataURL(keyUri);

  return res.json({ secret, keyUri, dataUri, backupCodes });
}
Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Generic console error: A console.error message is added for missing encryption key without structured logging and
may bypass centralized secure logging practices.

Referred Code
if (!process.env.CALENDSO_ENCRYPTION_KEY) {
  console.error("Missing encryption key; cannot proceed with backup code login.");
  throw new Error(ErrorCode.InternalServerError);
}
Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Backup code handling: Backup codes are decrypted and processed from credentials without explicit input
validation beyond simple formatting and rely on env key presence, requiring verification
of rate limiting and sanitization at this layer.

Referred Code
if (user.twoFactorEnabled && credentials.backupCode) {
  if (!process.env.CALENDSO_ENCRYPTION_KEY) {
    console.error("Missing encryption key; cannot proceed with backup code login.");
    throw new Error(ErrorCode.InternalServerError);
  }

  if (!user.backupCodes) throw new Error(ErrorCode.MissingBackupCodes);

  const backupCodes = JSON.parse(
    symmetricDecrypt(user.backupCodes, process.env.CALENDSO_ENCRYPTION_KEY)
  );

  // check if user-supplied code matches one
  const index = backupCodes.indexOf(credentials.backupCode.replaceAll("-", ""));
  if (index === -1) throw new Error(ErrorCode.IncorrectBackupCode);

  // delete verified backup code and re-encrypt remaining
  backupCodes[index] = null;
  await prisma.user.update({
    where: {
      id: user.id,


 ... (clipped 6 lines)
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review
Copy link

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Security
Use constant-time comparison for backup codes

Replace the indexOf method for backup code validation with a constant-time
comparison algorithm to mitigate potential timing attack vulnerabilities.

apps/web/pages/api/auth/two-factor/totp/disable.ts [58-66]

 const backupCodes = JSON.parse(symmetricDecrypt(user.backupCodes, process.env.CALENDSO_ENCRYPTION_KEY));
 
-// check if user-supplied code matches one
-const index = backupCodes.indexOf(req.body.backupCode.replaceAll("-", ""));
-if (index === -1) {
+// check if user-supplied code matches one in constant time to prevent timing attacks
+const userCode = req.body.backupCode.replaceAll("-", "");
+const match = backupCodes.some((code: string | null) => {
+  if (!code || code.length !== userCode.length) {
+    return false;
+  }
+  let result = 0;
+  for (let i = 0; i < code.length; i++) {
+    result |= code.charCodeAt(i) ^ userCode.charCodeAt(i);
+  }
+  return result === 0;
+});
+
+if (!match) {
   return res.status(400).json({ error: ErrorCode.IncorrectBackupCode });
 }
 
 // we delete all stored backup codes at the end, no need to do this here
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a potential timing attack vulnerability and proposes a standard mitigation using a constant-time comparison, which is a security best practice.

Medium
  • More

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants