2FA Quick Reference - nself-org/nchat GitHub Wiki

2FA Quick Reference Card

Implementation Date: January 30, 2026 Version: v0.3.0


Files Created

Database

.backend/migrations/012_2fa_system.sql

Utilities

src/lib/2fa/totp.ts              # TOTP generation/verification
src/lib/2fa/backup-codes.ts      # Backup code management
src/lib/2fa/device-fingerprint.ts # Device trust system

API Routes

src/app/api/auth/2fa/setup/route.ts
src/app/api/auth/2fa/verify-setup/route.ts
src/app/api/auth/2fa/verify/route.ts
src/app/api/auth/2fa/disable/route.ts
src/app/api/auth/2fa/backup-codes/route.ts
src/app/api/auth/2fa/status/route.ts
src/app/api/auth/2fa/trusted-devices/route.ts

Components

src/components/auth/TwoFactorVerify.tsx  # Login verification modal
src/components/settings/TwoFactorSettings.tsx (existing, updated)

Hooks

src/hooks/use-2fa.ts             # React hook for 2FA operations

Quick Commands

Apply Migration

cd .backend
nself db migrate up

Test 2FA Setup

# Start services
pnpm backend:start
pnpm dev

# Navigate to:
http://localhost:3000/settings/security

Check 2FA Status (SQL)

-- Check if user has 2FA enabled
SELECT user_has_2fa_enabled('user-uuid');

-- Count active 2FA users
SELECT get_2fa_active_users_count();

-- Check backup codes remaining
SELECT count_remaining_backup_codes('user-uuid');

-- Check if device is trusted
SELECT is_device_trusted('user-uuid', 'device-hash');

API Usage Examples

1. Start Setup

POST /api/auth/2fa/setup
Body: { userId: string, email: string }
Response: {
  success: true,
  data: {
    secret: string,
    qrCodeDataUrl: string,
    backupCodes: string[],
    manualEntryCode: string
  }
}

2. Verify and Enable

POST /api/auth/2fa/verify-setup
Body: {
  userId: string,
  secret: string,
  code: string,
  backupCodes: string[]
}
Response: { success: true }

3. Verify During Login

POST /api/auth/2fa/verify
Body: {
  userId: string,
  code: string,
  rememberDevice: boolean
}
Response: {
  success: true,
  usedBackupCode: boolean
}

4. Get Status

GET /api/auth/2fa/status?userId={userId}
Response: {
  success: true,
  data: {
    isEnabled: boolean,
    backupCodes: { total, unused, used },
    trustedDevices: Device[]
  }
}

React Hook Usage

import { use2FA } from '@/hooks/use-2fa'

function Component() {
  const {
    status,           // Current 2FA status
    setupData,        // Setup data (QR, codes, etc.)
    loading,          // Loading state
    error,            // Error message
    startSetup,       // Start setup flow
    verifyAndEnable,  // Verify and enable 2FA
    disable,          // Disable 2FA
    regenerateBackupCodes,  // Get new backup codes
    removeTrustedDevice,    // Remove trusted device
    refreshStatus,    // Refresh status
  } = use2FA()

  // Start setup
  const handleSetup = async () => {
    const result = await startSetup()
    if (result.success) {
      // Show QR: setupData.qrCodeDataUrl
      // Show codes: setupData.backupCodes
    }
  }

  // Verify code
  const handleVerify = async (code: string) => {
    const result = await verifyAndEnable(code)
    if (result.success) {
      // 2FA enabled!
    }
  }

  return (
    <div>
      {status?.isEnabled ? 'Enabled' : 'Disabled'}
      <button onClick={handleSetup}>Enable 2FA</button>
    </div>
  )
}

Component Usage

TwoFactorVerify (Login Modal)

import { TwoFactorVerify } from '@/components/auth/TwoFactorVerify'

function LoginPage() {
  const [showTwoFactor, setShowTwoFactor] = useState(false)
  const [userId, setUserId] = useState('')

  return (
    <TwoFactorVerify
      open={showTwoFactor}
      userId={userId}
      onVerified={(rememberDevice) => {
        // Complete login
      }}
      onCancel={() => setShowTwoFactor(false)}
    />
  )
}

Database Schema

Tables

nchat_user_2fa_settings (
  id, user_id, secret, is_enabled,
  enabled_at, last_used_at, created_at, updated_at
)

nchat_user_backup_codes (
  id, user_id, code_hash, used_at, created_at
)

nchat_user_trusted_devices (
  id, user_id, device_id, device_name, device_info,
  trusted_until, last_used_at, created_at
)

nchat_2fa_verification_attempts (
  id, user_id, ip_address, user_agent,
  success, attempt_type, created_at
)

Indexes

  • idx_2fa_settings_user_id - Fast user lookups
  • idx_backup_codes_user_id - Backup code queries
  • idx_trusted_devices_user_id - Device trust checks
  • idx_2fa_attempts_user_time - Rate limiting

Security Checklist

  • TOTP secrets stored securely
  • Backup codes hashed with bcrypt
  • Device fingerprints hashed (SHA-256)
  • Rate limiting (5 attempts / 15 min)
  • Verification attempts logged
  • RLS policies enabled
  • Single-use backup codes
  • 30-day device trust expiry
  • TODO: Encrypt secrets at rest
  • TODO: Password verification before disable

Common Issues

Issue: QR code won't scan

Solution: Use manual entry code: setupData.manualEntryCode

Issue: "Invalid code" error

Solution:

  1. Check server time (NTP sync)
  2. Use backup code instead
  3. Wait for next 30-second window

Issue: Device not remembered

Solution:

  1. Check localStorage enabled
  2. Verify device fingerprint consistent
  3. Check trusted_until not expired

Testing Steps

  1. Setup Flow

    • Navigate to Settings → Security
    • Click "Enable 2FA"
    • Scan QR code with Google Authenticator
    • Enter 6-digit code
    • Save backup codes
  2. Login Flow

    • Log out
    • Log in with email/password
    • Enter TOTP code
    • Check "Remember this device"
    • Verify login successful
  3. Backup Code Flow

    • Log out
    • Log in with email/password
    • Click "Use backup code instead"
    • Enter backup code (XXXX-XXXX)
    • Verify login successful
    • Check code marked as used
  4. Device Trust

    • Log in with "Remember device" checked
    • Log out and log in again
    • Verify 2FA skipped (device trusted)
  5. Disable Flow

    • Navigate to Settings → Security
    • Click "Disable 2FA"
    • Confirm action
    • Verify 2FA disabled

Monitoring Queries

Check 2FA adoption

SELECT
  COUNT(*) FILTER (WHERE is_enabled) as enabled_users,
  COUNT(*) as total_users,
  ROUND(100.0 * COUNT(*) FILTER (WHERE is_enabled) / COUNT(*), 2) as adoption_rate
FROM nchat_user_2fa_settings;

Recent verification attempts

SELECT
  created_at,
  success,
  attempt_type,
  ip_address
FROM nchat_2fa_verification_attempts
ORDER BY created_at DESC
LIMIT 20;

Users with low backup codes

SELECT
  u.email,
  count_remaining_backup_codes(u.id) as remaining_codes
FROM auth.users u
JOIN nchat_user_2fa_settings s ON s.user_id = u.id
WHERE s.is_enabled = true
HAVING count_remaining_backup_codes(u.id) <= 3
ORDER BY remaining_codes;

Support

Documentation

  • Full Implementation: docs/2FA-Implementation-Summary.md
  • This Quick Reference: docs/2FA-Quick-Reference.md

Resources


Last Updated: January 30, 2026 Status: Production Ready ✓

⚠️ **GitHub.com Fallback** ⚠️