2FA Implementation - nself-org/nchat GitHub Wiki
Complete production-ready 2FA implementation with TOTP support for nchat.
This implementation provides a complete Two-Factor Authentication system using Time-based One-Time Passwords (TOTP). It supports authenticator apps like Google Authenticator, Authy, Microsoft Authenticator, and any other TOTP-compatible app.
-
TOTP-based authentication using
speakeasylibrary - QR code generation for easy setup
- Manual entry support for devices that can't scan QR codes
- Backup codes (10 recovery codes) with bcrypt hashing
- Remember device functionality (30-day trust period)
- Device fingerprinting for trusted device management
- Password verification before critical operations
- Multi-step setup wizard with progress indication
- Real-time code countdown (30-second refresh)
- Backup code management (download, copy, print)
- Trusted devices list with expiry tracking
- Activity logging for security auditing
- Bcrypt-hashed backup codes (never stored in plain text)
- Device fingerprinting using browser/system info
- Time-based tokens with drift tolerance (±30 seconds)
- One-time backup codes (invalidated after use)
- Password verification for disable/regenerate operations
- Audit trail of 2FA attempts
src/
├── components/
│ ├── auth/
│ │ ├── TwoFactorSetup.tsx # Complete setup wizard
│ │ ├── TwoFactorVerify.tsx # Login verification modal
│ │ └── index.ts # Exports
│ └── settings/
│ └── TwoFactorSettings.tsx # 2FA management UI
├── lib/
│ └── 2fa/
│ ├── totp.ts # TOTP utilities
│ ├── backup-codes.ts # Backup code management
│ └── device-fingerprint.ts # Device identification
├── hooks/
│ └── use-2fa.ts # React hook for 2FA
└── app/
└── api/
└── auth/
└── 2fa/
├── setup/route.ts # Initialize setup
├── verify-setup/route.ts # Verify & enable
├── verify/route.ts # Login verification
├── status/route.ts # Get 2FA status
├── disable/route.ts # Disable 2FA
├── backup-codes/route.ts # Regenerate codes
└── trusted-devices/route.ts # Manage devices
- id: uuid (PK)
- user_id: uuid (FK, unique)
- secret: text (encrypted TOTP secret)
- is_enabled: boolean
- enabled_at: timestamp
- last_used_at: timestamp
- created_at: timestamp
- updated_at: timestamp- id: uuid (PK)
- user_id: uuid (FK)
- code_hash: text (bcrypt hash)
- used_at: timestamp (null if unused)
- created_at: timestamp- id: uuid (PK)
- user_id: uuid (FK)
- device_id: text (SHA-256 fingerprint)
- device_name: text
- device_info: jsonb
- trusted_until: timestamp
- last_used_at: timestamp
- created_at: timestamp- id: uuid (PK)
- user_id: uuid (FK)
- ip_address: text
- user_agent: text
- success: boolean
- attempt_type: text (totp | backup_code)
- created_at: timestampconst response = await fetch('/api/auth/2fa/setup', {
method: 'POST',
body: JSON.stringify({ userId, email })
})
// Returns:
{
secret: "base32-encoded-secret",
qrCodeDataUrl: "data:image/png;base64,...",
otpauthUrl: "otpauth://totp/...",
backupCodes: ["XXXX-XXXX", ...],
manualEntryCode: "XXXX XXXX XXXX XXXX"
}- Open authenticator app
- Scan QR code OR manually enter secret
- App generates 6-digit codes every 30 seconds
const response = await fetch('/api/auth/2fa/verify-setup', {
method: 'POST',
body: JSON.stringify({
userId,
secret,
code: "123456",
backupCodes: ["XXXX-XXXX", ...]
})
})
// Enables 2FA and stores hashed backup codes- User downloads or copies backup codes
- Each code can only be used once
- Codes are bcrypt-hashed in database
const response = await fetch(`/api/auth/2fa/status?userId=${userId}`)
if (response.data.isEnabled) {
// Show 2FA verification
}const deviceId = getCurrentDeviceFingerprint()
const response = await fetch(
`/api/auth/2fa/trusted-devices?userId=${userId}&deviceId=${deviceId}&action=check`
)
if (response.data.isTrusted) {
// Skip 2FA verification
} else {
// Require 2FA code
}const response = await fetch('/api/auth/2fa/verify', {
method: 'POST',
body: JSON.stringify({
userId,
code: '123456', // or backup code
rememberDevice: true, // optional
}),
})import { TwoFactorSetup } from '@/components/auth/TwoFactorSetup'
function MyComponent() {
const [showSetup, setShowSetup] = useState(false)
const { user } = useAuth()
return (
<>
<button onClick={() => setShowSetup(true)}>Enable 2FA</button>
<TwoFactorSetup
open={showSetup}
onComplete={() => {
setShowSetup(false)
// Reload 2FA status
}}
onCancel={() => setShowSetup(false)}
userId={user.id}
email={user.email}
/>
</>
)
}import { TwoFactorVerify } from '@/components/auth/TwoFactorVerify'
function LoginPage() {
const [show2FA, setShow2FA] = useState(false)
const [userId, setUserId] = useState('')
const handleLogin = async (email, password) => {
// ... authenticate user
// If 2FA is enabled:
setUserId(user.id)
setShow2FA(true)
}
return (
<>
{/* Login form */}
<TwoFactorVerify
open={show2FA}
onVerified={(rememberDevice) => {
// Complete login
router.push('/chat')
}}
onCancel={() => setShow2FA(false)}
userId={userId}
/>
</>
)
}import { TwoFactorSettings } from '@/components/settings/TwoFactorSettings'
function SecuritySettings() {
return (
<div>
<h1>Security Settings</h1>
<TwoFactorSettings />
</div>
)
}import { use2FA } from '@/hooks/use-2fa'
function MyComponent() {
const {
status,
loading,
isEnabled,
initializeSetup,
verifyAndEnable,
disable,
regenerateBackupCodes,
listTrustedDevices,
} = use2FA()
if (loading) return <Loader />
return (
<div>
<p>2FA Status: {isEnabled ? 'Enabled' : 'Disabled'}</p>
{status && (
<>
<p>Backup Codes: {status.backupCodes.unused} remaining</p>
<p>Trusted Devices: {status.trustedDevices.length}</p>
</>
)}
</div>
)
}Initialize 2FA setup - generates secret and QR code
Request:
{
"userId": "uuid",
"email": "[email protected]"
}Response:
{
"success": true,
"data": {
"secret": "base32-secret",
"qrCodeDataUrl": "data:image/png;base64,...",
"otpauthUrl": "otpauth://totp/...",
"backupCodes": ["XXXX-XXXX", ...],
"manualEntryCode": "XXXX XXXX XXXX"
}
}Verify code and enable 2FA
Request:
{
"userId": "uuid",
"secret": "base32-secret",
"code": "123456",
"backupCodes": ["XXXX-XXXX", ...]
}Response:
{
"success": true,
"message": "2FA enabled successfully"
}Verify 2FA code during login
Request:
{
"userId": "uuid",
"code": "123456",
"rememberDevice": true
}Response:
{
"success": true,
"message": "Verification successful",
"usedBackupCode": false
}Get 2FA status for user
Response:
{
"success": true,
"data": {
"isEnabled": true,
"enabledAt": "2026-02-01T10:00:00Z",
"lastUsedAt": "2026-02-01T12:00:00Z",
"backupCodes": {
"total": 10,
"unused": 8,
"used": 2
},
"trustedDevices": [...]
}
}Disable 2FA (requires password)
Request:
{
"userId": "uuid",
"password": "user-password"
}Regenerate backup codes (requires password)
Request:
{
"userId": "uuid",
"password": "user-password"
}Response:
{
"success": true,
"data": {
"codes": ["XXXX-XXXX", ...]
}
}List trusted devices
Response:
{
"success": true,
"data": {
"devices": [...],
"total": 3
}
}Remove trusted device
- ✅ Google Authenticator (iOS, Android)
- ✅ Authy (iOS, Android, Desktop)
- ✅ Microsoft Authenticator (iOS, Android)
- ✅ 1Password (password manager with TOTP)
- ✅ Bitwarden (password manager with TOTP)
- ✅ LastPass Authenticator
- ✅ Any TOTP-compatible app
- Industry-standard TOTP - Uses RFC 6238 time-based algorithm
- Secure secret generation - 32-byte entropy (256 bits)
- Bcrypt-hashed backups - Backup codes never stored in plain text
- Device fingerprinting - Multiple signals for device identification
- Time drift tolerance - ±30 second window for clock differences
- Audit logging - All attempts logged with IP and user agent
- Always require password for disable/regenerate operations
- Enforce backup code limits - Warn when < 3 codes remain
- Expire trusted devices - 30-day maximum trust period
- Monitor failed attempts - Alert on repeated failures
- Secure QR transmission - Only show during setup, never logged
- Rate limiting - Prevent brute force attacks
- Device fingerprinting is not foolproof (can be spoofed)
- QR code must be transmitted securely (HTTPS only)
- Backup codes - Users may lose them
- Time sync - User devices must have accurate clocks
When NEXT_PUBLIC_USE_DEV_AUTH=true, you can test with:
// owner@nself.org - Has 2FA enabled
// admin@nself.org - No 2FA
// member@nself.org - No 2FA- Open setup wizard
- Generate QR code and secret
- Scan QR code with authenticator app
- Verify 6-digit code
- Save backup codes (download/copy)
- Confirm setup completion
- Sign in with 2FA-enabled account
- See 2FA verification prompt
- Enter correct code (success)
- Enter incorrect code (failure)
- Use backup code (success, code invalidated)
- Remember device (skip 2FA on next login)
- View 2FA status
- View backup codes remaining
- Regenerate backup codes
- View trusted devices list
- Remove trusted device
- Disable 2FA
describe('2FA Flow', () => {
it('should complete setup', async () => {
// Initialize setup
const setup = await fetch('/api/auth/2fa/setup', {...})
expect(setup.data.secret).toBeDefined()
// Generate valid code
const code = generateTOTPToken(setup.data.secret)
// Verify and enable
const verify = await fetch('/api/auth/2fa/verify-setup', {
body: { code, secret: setup.data.secret, ... }
})
expect(verify.success).toBe(true)
})
it('should verify login with TOTP', async () => {
const code = generateTOTPToken(userSecret)
const verify = await fetch('/api/auth/2fa/verify', {
body: { code, userId, rememberDevice: false }
})
expect(verify.success).toBe(true)
})
it('should verify login with backup code', async () => {
const verify = await fetch('/api/auth/2fa/verify', {
body: { code: 'XXXX-XXXX', userId, rememberDevice: false }
})
expect(verify.success).toBe(true)
expect(verify.usedBackupCode).toBe(true)
})
})- Clock sync issue: Ensure device time is accurate
- Wrong secret: Verify QR code was scanned correctly
- Code expired: TOTP codes change every 30 seconds
- Already used: Each code is single-use
- Wrong format: Codes are 8 characters (XXXX-XXXX)
- Case sensitive: Codes are uppercase
- Cookies disabled: Trust requires cookies/localStorage
- Private browsing: Trust data not persisted
- Different browser: Each browser is a different device
- Camera permissions: Check app has camera access
- Display quality: Ensure QR code is sharp and clear
- Use manual entry: Fall back to typing secret key
- Database migration: Create 2FA tables
- Add UI components: Setup wizard, verify modal
- Update login flow: Check 2FA status, prompt for code
- Configure settings: Add 2FA section to security settings
- Export user data: Get list of users with 2FA enabled
- Disable old 2FA: For each user
- Send re-setup emails: Prompt users to re-enable
- Grace period: Allow both systems temporarily
- QR generation: ~50ms (cached on setup)
- TOTP verification: <1ms (no network calls)
- Backup code verification: ~100ms (bcrypt compare)
- Device fingerprint: ~10ms (client-side only)
- WebAuthn/FIDO2 support (hardware keys)
- SMS-based 2FA (lower security, higher convenience)
- Email-based 2FA codes
- Biometric authentication
- Risk-based authentication (adaptive 2FA)
- Account recovery flow without backup codes
- Admin force-enable 2FA for all users
- 2FA grace period for new users
This implementation is part of the nchat project. See main project LICENSE.
Built using:
-
speakeasy- TOTP implementation -
qrcode- QR code generation -
bcryptjs- Password/backup code hashing - Radix UI - Component primitives
- Next.js 15 - App framework