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

Time Windows and Clock Drift

The 30-second window problem

TOTP codes are valid for exactly one 30-second window. If the user’s phone says 10:00:15 and the server says 10:00:31, they are in different windows. The phone generates a code for window T, the server expects a code for window T+1. The code is rejected even though the user did nothing wrong.

This happens because of clock drift: the phone’s clock and the server’s clock are not perfectly synchronized.

The solution: accept adjacent windows

When verifying a code, accept codes from the current window and one window on each side:

const delta = totp.validate({ token: code, window: 1 });

window: 1 means: check the current time step, the previous time step, and the next time step. This gives a 90-second acceptance window (30 seconds × 3 windows) centered on the current time.

If delta is:

  • 0 — the code matches the current window
  • -1 — the code matches the previous window (the user’s clock is behind)
  • 1 — the code matches the next window (the user’s clock is ahead)
  • null — the code does not match any window (invalid)

Why not a larger window?

A larger window (like window: 5) would accept codes from 11 windows (5 past + current + 5 future = 330 seconds). This is more lenient, but it also means an intercepted code is valid for over 5 minutes instead of 90 seconds. The larger the window, the more time an attacker has to use a captured code.

window: 1 is the standard default used by Google, GitHub, and most services. It balances usability (clocks are rarely off by more than 30 seconds) and security (the acceptance window is still short).

Preventing code reuse

Even within the acceptance window, a code should only work once. Without this check, an attacker who intercepts a code has the full window to replay it.

Track the last used time step:

// Add to totp_secrets table: last_used_counter INTEGER
// After successful verification:
db.prepare("UPDATE totp_secrets SET last_used_counter = ? WHERE user_id = ?").run(
  currentCounter,
  userId,
);

// Before accepting a code, check:
if (counter <= lastUsedCounter) {
  // This code (or an older one) was already used
  return false;
}

This is optional for most apps (the 30-second window makes replay attacks impractical in most threat models), but important for high-security applications.

What if the clock is very wrong?

If a user’s phone clock is off by minutes (not seconds), window: 1 will not help. The user’s authenticator will generate codes for a time step far from the server’s current step.

Causes of large clock drift: disabled automatic time sync, traveling across time zones with manual clock settings, or a device that has been off for a long time and has not synced with NTP.

The fix is on the user’s side: enable automatic time sync in phone settings. Your app can show a helpful error message: “If your code is not working, check that your phone’s time is set to automatic.”

Exercises

Exercise 1: Temporarily set window: 0 (exact match only). Generate a code, wait 25 seconds, then submit it. It will likely fail because you crossed a window boundary. Set it back to window: 1 and try the same test.

Exercise 2: Check the time on your phone and your server. If they differ by more than a few seconds, the TOTP code might fail with window: 0 but succeed with window: 1.

Why is window: 1 (not window: 5 or higher) the standard for TOTP verification?

← Verifying TOTP on Login Recovery Codes →

© 2026 hectoday. All rights reserved.