Skip to main content
Back to blog
April 28, 2026|8 min read

Two-Factor Authentication Setup Guide: TOTP, SMS & Hardware Keys

Two-factor authentication is the single most effective control to prevent account takeover. This guide covers every 2FA method, how to implement TOTP from scratch, and what to verify on your live site.

ZeriFlow Team

1,501 words

Two-Factor Authentication Setup Guide: TOTP, SMS & Hardware Keys

Two-factor authentication (2FA) blocks over 99% of automated account takeover attacks, yet millions of web applications ship without it. Whether you are building a new app or hardening an existing one, this guide covers every 2FA method, how each works under the hood, and how to implement TOTP authentication with production-ready code.

Check your site's security configuration: Free ZeriFlow scan in 60 seconds →

Why Two-Factor Authentication Is Non-Negotiable in 2026

Passwords alone are broken. Credential stuffing attacks now run at billions of attempts per day using leaked databases from past breaches. Even a strong, unique password can be phished in seconds by a convincing fake login page.

2FA adds a second layer that attackers typically cannot replicate without physical access to your device or phone. The numbers are stark: Google's internal research found that SMS-based 2FA blocked 100% of automated bot attacks, 99% of bulk phishing attacks, and 66% of targeted attacks. Hardware keys pushed those numbers to 100% across all three categories.

The business case is equally clear. Regulations like PCI-DSS 4.0, SOC 2, and HIPAA now explicitly require multi-factor authentication for privileged access. Cyber insurance providers are beginning to deny claims when MFA was available but not enabled.

TOTP: The Gold Standard for App-Based 2FA

Time-based One-Time Passwords (TOTP) are defined in RFC 6238 and are the technology behind Google Authenticator, Authy, and every other authenticator app. The flow works like this:

  1. 1Your server generates a random secret (typically 160-bit base32 encoded).
  2. 2The user scans a QR code that encodes otpauth://totp/YourApp:user@example.com?secret=BASE32SECRET&issuer=YourApp.
  3. 3Both the server and the app independently compute HOTP(secret, floor(current_unix_time / 30)).
  4. 4The app displays the result; the user types it in; the server verifies.

Because both sides use the same clock and same secret, no network call is needed. The code is valid for 30 seconds and can never be reused.

Implementation with speakeasy (Node.js):

javascript
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');

// Step 1: Generate secret for new user
async function setupTOTP(userId, userEmail) {
  const secret = speakeasy.generateSecret({
    name: `YourApp (${userEmail})`,
    issuer: 'YourApp',
    length: 32
  });

  // Store secret.base32 encrypted in your database
  await db.users.update(userId, {
    totp_secret: encrypt(secret.base32),
    totp_enabled: false // not yet verified
  });

  // Return QR code for the user to scan
  const qrDataUrl = await QRCode.toDataURL(secret.otpauth_url);
  return { qrDataUrl, manualCode: secret.base32 };
}

// Step 2: Verify and activate
function verifyAndActivateTOTP(userId, token) {
  const user = db.users.findById(userId);
  const secret = decrypt(user.totp_secret);

  const verified = speakeasy.totp.verify({
    secret,
    encoding: 'base32',
    token,
    window: 1 // allow 30s clock drift
  });

  if (verified) {
    db.users.update(userId, { totp_enabled: true });
    return generateBackupCodes(userId); // always generate backup codes
  }
  return false;
}

// Step 3: Verify at login
function verifyTOTPAtLogin(userId, token) {
  const user = db.users.findById(userId);
  if (!user.totp_enabled) return true; // 2FA not set up

  return speakeasy.totp.verify({
    secret: decrypt(user.totp_secret),
    encoding: 'base32',
    token,
    window: 1
  });
}

Python equivalent with pyotp:

python
import pyotp
import qrcode
from io import BytesIO

def setup_totp(user_id: str, user_email: str) -> dict:
    secret = pyotp.random_base32()
    totp = pyotp.TOTP(secret)
    provisioning_uri = totp.provisioning_uri(
        name=user_email,
        issuer_name="YourApp"
    )
    # Store encrypt(secret) in database
    db.users.update(user_id, totp_secret=encrypt(secret))
    return {"secret": secret, "uri": provisioning_uri}

def verify_totp(user_id: str, token: str) -> bool:
    user = db.users.get(user_id)
    totp = pyotp.TOTP(decrypt(user.totp_secret))
    return totp.verify(token, valid_window=1)

Always generate 8-10 single-use backup codes when a user enables TOTP. Store them hashed (bcrypt), not plain text. Display them once and tell the user to print or save them offline.

SMS 2FA: Convenient but Weaker

SMS OTPs are widely understood by non-technical users, which makes adoption rates higher. The risk is SIM-swapping: an attacker bribes or social-engineers a carrier employee to transfer your number to a new SIM.

Use SMS 2FA when your audience is less technical and you cannot enforce app-based 2FA. Never use SMS 2FA alone for high-privilege operations (admin panels, financial transactions). If you do implement SMS:

  • Use a reputable provider (Twilio, AWS SNS) — do not build your own SMS gateway.
  • Make codes 6 digits, valid for 5 minutes maximum, single-use.
  • Rate limit OTP requests per phone number: 3 attempts per 10 minutes is a reasonable default.
  • Log every OTP send with IP address and user agent for abuse detection.

Hardware Security Keys: Maximum Assurance

FIDO2 / WebAuthn hardware keys (YubiKey, Google Titan, etc.) are phishing-proof because the browser cryptographically binds the authentication to the exact origin. A fake login page on paypa1.com will never get a valid assertion for paypal.com.

For applications handling sensitive data — banking, healthcare, administrative panels — hardware keys should be the default MFA option. The WebAuthn API is now supported in all major browsers without plugins.

javascript
// Registration (simplified)
const publicKeyCredentialCreationOptions = {
  challenge: crypto.getRandomValues(new Uint8Array(32)),
  rp: { name: "YourApp", id: "yourapp.com" },
  user: { id: userId, name: userEmail, displayName: userName },
  pubKeyCredParams: [{ alg: -7, type: "public-key" }], // ES256
  authenticatorSelection: { userVerification: "preferred" },
  timeout: 60000
};

const credential = await navigator.credentials.create({
  publicKey: publicKeyCredentialCreationOptions
});
// Send credential.response to server for verification

2FA protects the login step. But if your session cookie is stolen after login, the attacker walks in anyway. Every session cookie must carry three flags:

  • Secure — only transmitted over HTTPS, never HTTP.
  • HttpOnly — inaccessible to JavaScript, blocking XSS-based cookie theft.
  • SameSite=Strict (or Lax) — prevents the cookie from being sent in cross-site requests, blocking CSRF.

ZeriFlow's scanner automatically checks all your cookies for these flags as part of its 80+ security checks. If any session cookie is missing HttpOnly or Secure, the scan reports it as a finding with remediation guidance — catching issues that slip through code review.

http
Set-Cookie: session=abc123; Path=/; Secure; HttpOnly; SameSite=Strict

Beyond flags, rotate the session ID immediately after a successful 2FA verification. This closes the session fixation window: an attacker who planted a known session ID before login can no longer use it post-authentication.

A poorly designed account recovery flow can completely bypass 2FA. Common mistakes:

  • Email-only recovery that skips 2FA: If an attacker can trigger a password reset and access your email, they own your account. Recovery flows should either re-verify 2FA or use pre-generated backup codes.
  • Security questions: Do not use them. They are guessable or findable on social media.
  • Support bypasses: Your support team should not be able to disable 2FA without the user's explicit consent and a verified identity check.

Best practice: when a user loses their 2FA device, require them to use one of their backup codes. If they have no backup codes, require identity verification through a separate out-of-band channel before recovering access.


FAQ

### Q: Should I force 2FA for all users or make it optional? A: For consumer apps, make it optional but actively encourage enrollment with banners and security scores. For B2B apps and any admin interface, enforce 2FA as a mandatory policy. PCI-DSS 4.0 requires MFA for all access to the cardholder data environment without exception.

### Q: How do I handle 2FA for API clients? A: API clients authenticate with API keys or short-lived tokens, not interactive 2FA flows. Enforce 2FA on the human login that creates and manages API keys. For machine-to-machine auth, use mTLS or signed JWTs with short expiration instead.

### Q: What window should I use for TOTP verification? A: A window of 1 (accepting the current code plus one period before and after) handles typical clock drift without meaningfully weakening security. Never use a window greater than 2.

### Q: Is TOTP vulnerable to phishing? A: Yes. A real-time phishing proxy can capture a TOTP code and replay it within the 30-second window. Hardware keys (WebAuthn/FIDO2) are the only 2FA method that is genuinely phishing-proof because the origin is cryptographically bound.

### Q: How should I store backup codes? A: Hash each backup code with bcrypt (cost ≥ 12) before storing, exactly like passwords. Display the plaintext codes exactly once to the user during setup, then discard them. Invalidate a backup code immediately after it is used.


Conclusion

Two-factor authentication is one of the highest-ROI security investments you can make. Start with TOTP app-based 2FA — it is free, well-understood, and blocks the vast majority of attacks. Add WebAuthn support for users who need phishing-proof authentication. Harden your session cookies with Secure, HttpOnly, and SameSite flags to protect the authenticated session itself.

Run a free ZeriFlow scan → — no credit card required.

Ready to check your site?

Run a free security scan in 30 seconds.

Related articles

Keep reading