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:
| Method | Type | Strength | UX |
|---|---|---|---|
| Password | Something you know | Weak alone (phishable, reusable) | Familiar |
| TOTP | Something you have | Strong as 2nd factor (short-lived codes) | 6-digit code from phone |
| Recovery codes | Something you have | Emergency backup (single-use) | Enter a saved code |
| Magic links | Something you have (email) | Moderate (single-factor, email-dependent) | Click a link |
| Passkeys | Something you have + are | Strongest (phishing-resistant, cryptographic) | Biometric prompt |
Checklist
TOTP
- Secret generated with
otpauthlibrary (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/serverfor 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/methodsendpoint returns available methods for an email -
/me/securityendpoint 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?