Auth & Security/
Lesson

A user enables 2FAWhat is 2fa?Two-factor authentication - requiring a second verification step (like a phone code) on top of your password to prove it's really you. on Monday. On Wednesday, their phone falls in a lake. They cannot generate TOTPWhat is totp?Time-based One-Time Password - a 6-digit code generated from a shared secret and the current time, changing every 30 seconds. Powers authenticator apps. codes. They cannot receive push notifications. Their authenticator app, along with every secret stored in it, is gone. If your application has no recovery path, that user is permanently locked out of their account. This is not an edge case. Lost phones, factory resets, broken screens, and stolen devices happen constantly.

Recovery codes exist to solve this problem. They are the last-resort authenticationWhat is authentication?Verifying who a user is, typically through credentials like a password or token. method, a set of one-time codes generated during 2FA enrollment that work without any device at all.

How recovery codes work

Recovery codes are randomly generated strings, typically 8-12 codes of 8-10 characters each. They are displayed to the user once during 2FAWhat is 2fa?Two-factor authentication - requiring a second verification step (like a phone code) on top of your password to prove it's really you. enrollment and never shown again. The user is responsible for storing them securely, printed on paper, saved in a password manager, or locked in a safe.

// Recovery code generation
function generateRecoveryCodes(count = 10) {
  const codes = [];
  for (let i = 0; i < count; i++) {
    // Generate a random 10-character hex code
    const code = crypto.randomBytes(5).toString('hex');
    // Format as XXXXX-XXXXX for readability
    codes.push(`${code.slice(0, 5)}-${code.slice(5)}`);
  }
  return codes;
}

// Store hashed codes (never store plain text)
async function storeRecoveryCodes(userId, codes) {
  const hashedCodes = await Promise.all(
    codes.map(async (code) => ({
      hash: await bcrypt.hash(code, 10),
      used: false,
    }))
  );
  await db.saveRecoveryCodes(userId, hashedCodes);
}

The enrollment flow with recovery codes

StepActionWho
1User enables 2FA (scans QR code, verifies first TOTP code)User
2Server generates 10 recovery codesServer
3Server displays codes to the user with a "Download" or "Copy" buttonServer to User
4User confirms they have saved the codes (checkbox or re-entry of one code)User
5Server stores hashed codes, marks 2FA as fully enrolledServer
6Recovery codes are never displayed again,

The confirmation step (step 4) is important. Without it, users click through the enrollment flow without saving their codes and discover the omission only when they need them. Some applications require the user to type back one of the codes to prove they wrote them down.

AI pitfall
AI-generated 2FA enrollment generates the QR code and verification but skips recovery codes entirely. When you prompt "add 2FA to my Node.js app," the result is an enrollment flow that works perfectly, until a user loses their phone. Always check that the enrollment flow includes recovery code generation, display, and storage.
02

Using a recovery code to log in

When a user cannot provide their normal 2FAWhat is 2fa?Two-factor authentication - requiring a second verification step (like a phone code) on top of your password to prove it's really you. code, they use a recovery code instead. The login flow branches:

// Login with recovery code
async function verifyRecoveryCode(userId, inputCode) {
  const storedCodes = await db.getRecoveryCodes(userId);
  for (const stored of storedCodes) {
    if (stored.used) continue; // Skip already-used codes
    const match = await bcrypt.compare(inputCode, stored.hash);
    if (match) {
      // Mark code as used (single-use)
      await db.markRecoveryCodeUsed(userId, stored.id);
      // Count remaining codes
      const remaining = storedCodes.filter(c => !c.used).length - 1;
      return { valid: true, remaining };
    }
  }
  return { valid: false };
}

Critical implementation details

Single-use enforcement. Each recovery code works exactly once. After use, it is marked as consumed and cannot be used again. This prevents an attacker who intercepts one code from using it repeatedly.

Remaining code warning. After a recovery code is used, tell the user how many codes remain. When the count drops below 3, display a prominent warning. When it reaches 0, force the user to set up a new 2FA method or generate new codes.

Rate limitingWhat is rate limiting?Restricting how many requests a client can make within a time window. Prevents brute-force attacks and protects your API from being overwhelmed.. Recovery code verification must be rate-limited just like password verification. Without rate limiting, an attacker can brute-force recovery codes. Ten codes of 10 hex characters is a large space, but rate limiting adds a critical layer of defense.

// Rate limit recovery code attempts
async function verifyWithRateLimit(userId, inputCode) {
  const attempts = await db.getRecentAttempts(userId, '2fa_recovery');
  if (attempts > 5) {
    // Lock account after 5 failed recovery attempts in 15 minutes
    await db.lockAccount(userId, 15 * 60 * 1000);
    throw new Error('Too many attempts. Account temporarily locked.');
  }
  await db.recordAttempt(userId, '2fa_recovery');
  return verifyRecoveryCode(userId, inputCode);
}
03

Regenerating recovery codes

Users who have used several codes (or lost their saved copy) need to generate new ones. The regeneration flow:

  1. User navigates to security settings
  2. User re-authenticates (password + current 2FAWhat is 2fa?Two-factor authentication - requiring a second verification step (like a phone code) on top of your password to prove it's really you. code)
  3. Server generates a new set of recovery codes
  4. All previous codes are invalidated: old codes stop working immediately
  5. New codes are displayed exactly once
  6. User confirms they saved the new codes

Invalidating old codes on regeneration is critical. If old codes remain valid alongside new ones, a compromised set of old codes still works. The regeneration must be a clean replacement, not an addition.

04

Account recovery when all else fails

What happens when a user loses their phone and never saved their recovery codes? This is the hardest problem in 2FAWhat is 2fa?Two-factor authentication - requiring a second verification step (like a phone code) on top of your password to prove it's really you.. The options are limited, and none are perfect.

Manual identity verification

The user contacts support and proves their identity through alternative means: government ID, selfie verification, answering account-specific questions, or providing transactionWhat is transaction?A group of database operations that either all succeed together or all fail together, preventing partial updates. history. This is expensive (human review), slow (24-72 hours), and imperfect (social engineering risk). But for high-value accounts, it may be the only option.

Trusted contact recovery

Some platforms allow users to designate trusted contacts who can vouch for them. Facebook uses this approach. The user asks their trusted contacts for recovery codes, and a threshold of responses (e.g., 3 out of 5) unlocks the account.

Time-delayed recovery

The user requests account recovery. The system waits 7-14 days before disabling 2FA. During this period, the real account owner receives notifications and can cancel the request if it was not them. This balances security (attackers cannot get instant access) with usability (legitimate users recover eventually).

Recovery methodSpeedSecurityUser frictionCost
Recovery codesInstantHigh (if codes are stored securely)LowNone
Manual identity verification24-72 hoursMedium (social engineering risk)HighHigh (human review)
Trusted contactHours to daysMedium (depends on contacts' security)MediumLow
Time-delayed removal7-14 daysHigh (real owner can cancel)HighLow
No recovery pathNeverN/A (user locked out forever)MaximumNone
AI pitfall
AI generates 2FA enrollment and verification but completely ignores the "what if they lose access" scenario. When you review AI-generated authentication code, the first question should be: "What happens when the user cannot provide the second factor?" If the answer is "they are locked out forever," the implementation is incomplete.
05

Account lockout protection

Failed 2FAWhat is 2fa?Two-factor authentication - requiring a second verification step (like a phone code) on top of your password to prove it's really you. attempts should trigger progressive lockout, just like failed password attempts:

// Progressive lockout for 2FA failures
const LOCKOUT_THRESHOLDS = [
  { attempts: 3, lockoutMinutes: 1 },
  { attempts: 5, lockoutMinutes: 5 },
  { attempts: 8, lockoutMinutes: 15 },
  { attempts: 10, lockoutMinutes: 60 },
];

async function checkLockout(userId) {
  const failedAttempts = await db.getFailedTotpAttempts(userId);
  for (let i = LOCKOUT_THRESHOLDS.length - 1; i >= 0; i--) {
    const threshold = LOCKOUT_THRESHOLDS[i];
    if (failedAttempts >= threshold.attempts) {
      const lastAttempt = await db.getLastFailedAttempt(userId);
      const lockoutEnds = lastAttempt + threshold.lockoutMinutes * 60 * 1000;
      if (Date.now() < lockoutEnds) {
        const minutesLeft = Math.ceil((lockoutEnds - Date.now()) / 60000);
        throw new Error(
          `Account locked. Try again in ${minutesLeft} minutes.`
        );
      }
      break;
    }
  }
}

Alert on suspicious activity

When an account experiences multiple failed 2FA attempts, notify the account owner via email. This serves two purposes: it warns the real user that someone may have their password, and it creates an audit trail. A well-designed alert includes the time of the attempts, the IP addressWhat is ip address?A numerical label (e.g., 172.217.14.206) that identifies a device on a network - DNS translates domain names into IP addresses. or geographic location, and a link to review recent account activity.

06

The complete 2FAWhat is 2fa?Two-factor authentication - requiring a second verification step (like a phone code) on top of your password to prove it's really you. implementation checklist

When reviewing AI-generated 2FA code, verify every item:

  • Enrollment requires verification (user enters a code to confirm setup works)
  • Recovery codes are generated and displayed during enrollment
  • Recovery codes are stored hashed, not in plain text
  • TOTPWhat is totp?Time-based One-Time Password - a 6-digit code generated from a shared secret and the current time, changing every 30 seconds. Powers authenticator apps. secrets are encrypted at restWhat is rest?An architectural style for web APIs where URLs represent resources (nouns) and HTTP methods (GET, POST, PUT, DELETE) represent actions on those resources. with a key outside the database
  • Time drift tolerance is implemented in TOTP verification
  • Replay protection prevents the same code from being used twice
  • Recovery codes are single-use and marked as consumed after use
  • Users are warned when recovery codes are running low
  • Code regeneration invalidates all previous codes
  • Failed attempts are rate-limited with progressive lockout
  • Suspicious activity triggers email notifications
  • SessionWhat is session?A server-side record that tracks a logged-in user. The browser holds only a session ID in a cookie, and the server looks up the full data on each request. is not created until all factors are verified
  • 2FA settings changes require re-authenticationWhat is authentication?Verifying who a user is, typically through credentials like a password or token.
  • There is a documented account recovery process for when all factors are lost