TOTP Implementation in TREK: Complete Two-Factor Authentication Guide

TREK implements RFC 6238 TOTP-based 2FA using the otplib library with AES-256-GCM encrypted secrets, bcrypt-hashed backup codes, and a three-stage REST API workflow that separates password authentication from token verification.

The TREK repository provides a production-ready two-factor authentication system built on the Time-based One-Time Password (TOTP) standard. This Node.js/TypeScript implementation leverages the otplib library for cryptographic operations while enforcing strict security controls through encrypted storage and constant-time comparison operations.

TOTP Setup and Enrollment Flow

The enrollment process consists of two distinct phases: temporary secret generation and permanent activation.

Generating the Initial Secret

When a user initiates 2FA setup via POST /api/auth/mfa/setup, the setupMfa(userId, userEmail) function in server/src/services/authService.ts (lines 1030–1047) executes the following:

  1. Calls authenticator.generateSecret() to create a random base32 secret
  2. Constructs an otpauth:// URL via authenticator.keyuri for QR code generation
  3. Stores the pending secret in an in-memory Map (mfaSetupPending) with a 15-minute TTL (MFA_SETUP_TTL_MS)
  4. Generates an SVG QR code asynchronously using the qrcode library (QRCode.toString)

The endpoint returns the raw secret, the OTPAuth URL, and the QR code SVG to the client.

Enabling MFA with Verification

The enableMfa(userId, code) function (lines 1050–1072) finalizes enrollment:

  • Retrieves the pending secret using getPendingMfaSecret
  • Validates the user-supplied TOTP code with authenticator.verify({ token, secret })
  • Generates 10 backup codes (8-character hex strings formatted as XXXX-XXXX) via generateBackupCodes
  • Hashes each backup code with bcrypt (hashBackupCodeBcrypt) for secure storage
  • Encrypts the TOTP secret using encryptMfaSecret before persisting to SQLite
  • Sets mfa_enabled = 1 and stores the JSON-encoded backup code hashes

Upon success, the endpoint returns the plaintext backup codes to the user for safekeeping.

Login Verification and Token Exchange

TREK implements a stepped authentication flow that prevents session issuance until MFA verification completes.

Password-Only Authentication Stage

During loginUser (MFA branch at lines 1058–1065), after successful password validation:

  • If user.mfa_enabled is true, TREK issues a short-lived JWT (5-minute expiration, purpose: 'mfa_login') instead of a session token
  • Returns { mfa_required: true, mfa_token } to the client
  • No session cookie or persistent token is issued at this stage

This MFA token serves as a temporary credential that grants access only to the verification endpoint.

TOTP Code Verification

The verifyMfaLogin({ mfa_token, code, remember_me }) function (lines 1111–1162) handles the final step:

  1. Validates the MFA token JWT and loads the user record
  2. Decrypts the stored TOTP secret using decryptMfaSecret from mfaCrypto.ts
  3. Attempts verification via authenticator.verify against the decrypted secret
  4. Fallback mechanism: If TOTP verification fails, checks against bcrypt-hashed backup codes using matchBackupCode with crypto.timingSafeEqual
  5. Removes used backup codes from the stored array (single-use enforcement)
  6. Updates last_login timestamp and issues the final session JWT via generateToken

The endpoint returns the standard session token only after successful TOTP or backup code validation.

Cryptographic Security Layer

Secret Encryption at Rest

The server/src/services/mfaCrypto.ts module implements AES-256-GCM encryption:

  • encryptMfaSecret(plain) generates a random 12-byte IV, encrypts the plaintext, concatenates IV + auth tag + ciphertext, and base64-encodes the result
  • decryptMfaSecret(blob) reverses this process to recover the original secret for verification

The raw TOTP secret never touches the SQLite database in plaintext form.

Backup Code Management

Backup codes provide account recovery when authenticator devices are unavailable:

  • Format: 8-character hexadecimal strings displayed as XXXX-XXXX
  • Storage: Each code is hashed with bcrypt using hashBackupCodeBcrypt before storage
  • Verification: matchBackupCode performs constant-time comparison using crypto.timingSafeEqual to prevent timing attacks
  • Rotation: Used codes are immediately removed from the JSON array stored in the database

REST API Endpoints

The MFA workflow exposes the following endpoints:

  • POST /api/auth/mfa/setup – Initiates enrollment, returns secret and QR code
  • POST /api/auth/mfa/enable – Verifies initial code and activates MFA
  • POST /api/auth/login – Standard login; returns mfa_token instead of session when MFA is enabled
  • POST /api/auth/mfa/verify-login – Validates TOTP or backup code, issues session token
  • POST /api/auth/mfa/disable – Deactivates MFA and clears stored secrets/backup codes

All endpoints pass through the RateLimitService (5 attempts per 15 minutes) and emit audit events via writeAudit.

Code Examples


# 1️⃣ Start TOTP enrollment

curl -X POST https://trek.example.com/api/auth/mfa/setup \
  -H "Authorization: Bearer $SESSION_JWT"

# Response:

# {

#   "secret":"JBSWY3DPEHPK3PXP",

#   "otpauth_url":"otpauth://totp/TREK:alice%40example.com?secret=JBSWY3DPEHPK3PXP&issuer=TREK",

#   "qr_svg":"<svg ...>"

# }

# 2️⃣ Enable MFA with verification code

curl -X POST https://trek.example.com/api/auth/mfa/enable \
  -H "Authorization: Bearer $SESSION_JWT" \
  -d '{"code":"123456"}' -H "Content-Type: application/json"

# Response:

# {

#   "success":true,

#   "mfa_enabled":true,

#   "backup_codes":["1A2B‑3C4D","5E6F‑7A8B", …]

# }

# 3️⃣ Login (password only)

curl -X POST https://trek.example.com/api/auth/login \
  -d '{"email":"alice@example.com","password":"s3cr3t"}' \
  -H "Content-Type: application/json"

# Response:

# { "mfa_required":true, "mfa_token":"eyJhbGciOi..." }

# 4️⃣ Complete MFA verification

curl -X POST https://trek.example.com/api/auth/mfa/verify-login \
  -d '{"mfa_token":"eyJhbGciOi...","code":"123456"}' \
  -H "Content-Type: application/json"

# Final response:

# {

#   "token":"<session JWT>",

#   "user":{...}

# }

Summary

  • TREK implements RFC 6238 TOTP using the otplib library with authenticator.generateSecret() and authenticator.verify()
  • Secrets are encrypted at rest using AES-256-GCM in server/src/services/mfaCrypto.ts and never stored in plaintext
  • The authentication flow uses a short-lived MFA JWT (5 minutes) to bridge password verification and TOTP validation
  • 10 backup codes (bcrypt-hashed) provide recovery options, verified using constant-time comparison
  • All MFA endpoints enforce rate limiting (5 attempts per 15 minutes) and comprehensive audit logging

Frequently Asked Questions

What library does TREK use for TOTP generation?

TREK uses the otplib library, specifically the authenticator API for generating base32 secrets (generateSecret), creating OTPAuth URLs (keyuri), and verifying time-based tokens (verify).

How are TOTP secrets stored in the database?

Secrets are encrypted using AES-256-GCM before storage. The encryptMfaSecret function in server/src/services/mfaCrypto.ts generates a random 12-byte IV, encrypts the secret, and stores the base64-encoded concatenation of IV, authentication tag, and ciphertext. Decryption occurs only during the verification phase.

What happens if a user loses their authenticator device?

Users can authenticate using backup codes generated during MFA enrollment. These are 8-character hex strings displayed as XXXX-XXXX. Each backup code is single-use; TREK removes verified codes from the stored hash array immediately after use. Users should store these codes securely during initial setup.

How long is the MFA token valid during login?

The MFA token issued during POST /api/auth/login is a JWT with a 5-minute expiration (purpose: 'mfa_login'). This limits the window for replay attacks and ensures that abandoned authentication attempts cannot be completed later.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →