hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses Two-Factor and Passwordless Auth with @hectoday/http

Why Passwords Are Not Enough

  • The Problem with Passwords
  • Project Setup

TOTP (Time-Based One-Time Passwords)

  • How TOTP Works
  • Generating Secrets and QR Codes
  • Enabling 2FA on an Account
  • Verifying TOTP on Login
  • Time Windows and Clock Drift

Recovery

  • Recovery Codes
  • Disabling 2FA
  • Account Recovery When Everything Is Lost

Magic Links

  • How Magic Links Work
  • Building Magic Link Login
  • Security Considerations

WebAuthn and Passkeys

  • What Are Passkeys?
  • Registration Flow
  • Authentication Flow
  • Passkeys as Second Factor or Primary

Putting It All Together

  • Multi-Method Auth
  • Auth Method Checklist and Capstone

Auth Method Checklist and Capstone

What we built

Starting from a password-only auth system, we added four additional auth methods and a recovery system:

MethodTypeStrengthUX
PasswordSomething you knowWeak alone (phishable, reusable)Familiar
TOTPSomething you haveStrong as 2nd factor (short-lived codes)6-digit code from phone
Recovery codesSomething you haveEmergency backup (single-use)Enter a saved code
Magic linksSomething you have (email)Moderate (single-factor, email-dependent)Click a link
PasskeysSomething you have + areStrongest (phishing-resistant, cryptographic)Biometric prompt

Checklist

TOTP

  • Secret generated with otpauth library (20+ byte random, base32-encoded)
  • QR code generated for easy import into authenticator apps
  • Text secret provided as manual entry fallback
  • 2FA not enabled until user confirms with a valid code
  • Login flow becomes two-step when 2FA is enabled
  • Pending sessions cannot access protected routes
  • Time window of ±1 (accepts current, previous, and next 30-second window)

Recovery codes

  • 10 codes generated at 2FA setup
  • Codes hashed with SHA-256 before storage
  • Codes shown to user exactly once
  • Each code is single-use (marked as used after consumption)
  • Recovery codes accepted at the 2FA step (alternative to TOTP)
  • Remaining code count visible to the user

Disabling and recovery

  • Disabling 2FA requires a valid TOTP code or recovery code (not just a password)
  • Disabling deletes the secret and all recovery codes (forces fresh setup on re-enable)
  • Admin-initiated 2FA reset available for account recovery
  • 2FA reset actions are logged

Magic links

  • Token hashed with SHA-256 before storage
  • Token expires in 15 minutes
  • Token is single-use
  • Previous unused tokens invalidated when a new one is requested
  • Same response for existing and non-existing emails (no enumeration)
  • Rate limited (3 per 15 minutes per email)

Passkeys / WebAuthn

  • Registration uses @simplewebauthn/server for challenge generation and verification
  • Public key stored, private key never leaves the device
  • Challenge stored temporarily with expiry (5 minutes)
  • Counter incremented and checked on each authentication (clone detection)
  • Origin (rpID) verified to prevent cross-origin attacks
  • Passkeys usable as 2nd factor or primary (passwordless) login

Multi-method

  • Login page offers password, passkey, and magic link options
  • /auth/methods endpoint returns available methods for an email
  • /me/security endpoint shows current auth configuration
  • Fallback chains are clear to the user (TOTP → recovery code → support)

Security tradeoffs

Every auth method makes tradeoffs. Understanding them helps you make the right recommendation for your users:

Password only: Weakest. Vulnerable to phishing, reuse, and credential stuffing. But universally understood and requires no special hardware.

Password + TOTP: Strong. Two factors. But TOTP codes can be phished in real-time (attacker relays the code). Phone loss requires recovery codes.

Password + passkey: Strongest practical option. Two factors, one phishing-resistant. But requires a device with WebAuthn support.

Passkey only: Strong single-factor. Phishing-resistant. Best UX (one step). But single-factor means device compromise is enough.

Magic link only: Convenient. No password to remember. But email compromise is full account compromise. Suitable for low-risk apps.

All methods available: Most flexible. Each user chooses their level. But more code to maintain and more attack surface to monitor.

The complete project structure

src/
  app.ts                  # setup(), all routes
  server.ts               # starts the server
  db.ts                   # schema with all tables
  auth.ts                 # authenticate, has2FA, hasPasskeys
  sessions.ts             # sessions with pending state
  cookies.ts              # cookie helpers
  totp.ts                 # TOTP generation and verification
  recovery.ts             # recovery code generation and verification
  magic-link.ts           # magic link token creation and verification
  routes/
    auth.ts               # POST /login, POST /login/2fa, POST /auth/methods
    totp.ts               # /me/2fa/setup, /me/2fa/verify, /me/2fa/disable
    recovery.ts           # recovery code endpoints
    magic-link.ts         # /auth/magic-link, /auth/magic-link/verify
    passkeys.ts           # registration and authentication flows
    settings.ts           # /me/security

Test the complete system

npm run dev

# === Password login (no 2FA) ===
curl -c cookies.txt -X POST http://localhost:3000/login \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"password123"}'
# Single-step login — session created

# === Enable TOTP ===
curl -b cookies.txt -X POST http://localhost:3000/me/2fa/setup
# Save the secret, scan QR code
curl -b cookies.txt -X POST http://localhost:3000/me/2fa/verify \
  -H "Content-Type: application/json" \
  -d '{"code":"CODE_FROM_AUTHENTICATOR"}'
# 2FA enabled — save recovery codes

# === Password + TOTP login ===
curl -c cookies.txt -X POST http://localhost:3000/login \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"password123"}'
# { "requiresTwoFactor": true }
curl -b cookies.txt -X POST http://localhost:3000/login/2fa \
  -H "Content-Type: application/json" \
  -d '{"code":"CODE_FROM_AUTHENTICATOR"}'
# Login complete

# === Magic link login ===
curl -X POST http://localhost:3000/auth/magic-link \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]"}'
# Check console for link
curl -c cookies.txt "http://localhost:3000/auth/magic-link/verify?token=TOKEN"
# Logged in without a password

# === Check security settings ===
curl -b cookies.txt http://localhost:3000/me/security
# Shows TOTP status, passkeys, recovery code count

Challenges

Challenge 1: Add passkey management. Build GET /me/passkeys (list), DELETE /me/passkeys/:id (remove), and PUT /me/passkeys/:id (rename). Require re-authentication (password or TOTP) before removing a passkey.

Challenge 2: Add login notifications. After any successful login, send a notification to the user’s email with the login method, IP address, and timestamp. Let users flag suspicious logins.

Challenge 3: Add session management. Build GET /me/sessions (list active sessions with device info) and DELETE /me/sessions/:id (revoke a specific session). This lets users see where they are logged in and sign out remotely.

Challenge 4: Enforce 2FA for sensitive actions. Even after login, require re-authentication (TOTP or passkey) before changing the email, disabling 2FA, or deleting the account. This is called “step-up authentication.”

Which auth method combination provides the strongest security?

A user has TOTP enabled, lost their phone, and lost their recovery codes. What should happen?

← Multi-Method Auth Back to course →

© 2026 hectoday. All rights reserved.