โ† Back to list

๐Ÿ” TOTP Explained: How Authenticator App Works

Ever wondered whatโ€™s really happening when your authenticator app shows a new number every 30 seconds? Letโ€™s break it down โ€” simplyโ€ฆ

Sarwang Jain ยท 2026-02-22 13:45 ยท 0 claps ยท 9.4 min read
#totp #authentication #two-factor-authentication
Open on Medium โ†—

๐Ÿ” TOTP Explained: How Authenticator App Works

Ever wondered whatโ€™s really happening when your authenticator app shows a new number every 30 seconds? Letโ€™s break it down โ€” simply, clearly, and with real code you can run today.

You open your favorite app, type in your password, and thenโ€ฆ youโ€™re asked for a 6-digit code from your authenticator. You glance at Google Authenticator or Microsoft Authenticator, type in the number, and youโ€™re in. Easy.

But hereโ€™s whatโ€™s quietly amazing about that experience โ€” that code was never sent to you. No SMS, no email, no server pushing anything to your phone. Both your device and the server independently figured out the same 6 digits. At the same time. And hereโ€™s the kicker โ€” your phone didnโ€™t even need an internet connection to do it. Like magic โ€” except itโ€™s math.

That magic has a name: TOTP, or Time-based One-Time Password. And by the end of this article, youโ€™ll understand exactly how it works โ€” and even build one yourself.

Ready? Letโ€™s dive in.

So, What Even Is TOTP?

TOTP stands for Time-based One-Time Password. Itโ€™s the standard behind most authenticator apps today, formally defined in RFC 6238 by the Internet Engineering Task Force (IETF) back in May 2011.

Hereโ€™s the core idea in plain English:

You and the server share a secret. You both look at the current time. You both run the same calculation. You both get the same short code.

Thatโ€™s it. No network call needed for the code itself. The server can verify it because it can do the same math you did. And since the code is tied to time, it expires in 30 seconds โ€” making it nearly useless if someone steals it.

The Big Picture โ€” How TOTP Works Step by Step

Letโ€™s walk through the flow, nice and easy.

Step 1: Setup (the one-time handshake)

When you enable 2FA on a website, the server generates a random secret โ€” a sequence of random bytes, usually at least 160 bits long. It shares this secret with your authenticator app, typically by showing you a QR code that you scan. After that, the secret lives in two places only: the serverโ€™s database and your phoneโ€™s authenticator app.

Step 2: Generating the code

When you need a code, hereโ€™s what happens under the hood:

  1. Take the current Unix timestamp (seconds since January 1, 1970).
  2. Divide it by the time step โ€” usually 30 seconds. This gives you a โ€œcounterโ€ that increments every 30 seconds.
  3. Run that counter through an HMAC (Hash-based Message Authentication Code) function using the shared secret as the key. RFC 6238 supports HMAC-SHA-1, HMAC-SHA-256, or HMAC-SHA-512.
  4. Take a small slice of those bytes (called dynamic truncation) and convert it into a 6-digit number.

The result? Both your phone and the server follow the same steps, get the same answer, and the code matches. โœ…

Step 3: Verification

When you submit the code, the server runs the same calculation โ€” and usually checks a small window (like ยฑ1 step) around the current time, just to be forgiving if your clock is slightly off.

Hereโ€™s a simple way to think about it visually:

         Your Phone                           Server
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
Shared Secret + Current Time       Shared Secret + Current Time
              โ†“                                  โ†“
       HMAC Calculation                   HMAC Calculation
              โ†“                                  โ†“
         6-digit Code โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ Does it match? โœ…

The Math Behind It (Donโ€™t Worry, Itโ€™s Friendly)

The formula from RFC 6238 looks like this:

T  = floor((Current Unix Time - T0) / X)
TOTP = HOTP(K, T)

Where:

  • T is the time-based counter
  • T0 is the Unix time to start counting from (usually 0, meaning January 1, 1970)
  • X is the time step in seconds (usually 30)
  • K is your shared secret
  • HOTP is the underlying HMAC-based calculation from RFC 4226

For example, if the current Unix time is 1,700,000,000:

T = floor(1700000000 / 30) = 56,666,666

This counter value 56,666,666 is fed into the HMAC function with your secret. The output is truncated into a 6-digit code. Thirty seconds later, the counter becomes 56,666,667, and the code changes.

Letโ€™s Write Some Code!

Enough theory โ€” letโ€™s see this in action. Here are working examples in both Node.js and the browser.

Node.js Example

const crypto = require('crypto');

// Decode a Base32 secret to a Buffer
function base32Decode(base32) {
  const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
  let bits = 0, value = 0;
  const output = [];

  for (const char of base32.toUpperCase().replace(/=+$/, '')) {
    value = (value << 5) | alphabet.indexOf(char);
    bits += 5;
    if (bits >= 8) {
      output.push((value >>> (bits - 8)) & 255);
      bits -= 8;
    }
  }

  return Buffer.from(output);
}

// Generate a TOTP code
function generateTOTP(secret, digits = 6, step = 30) {
  const counter = Math.floor(Date.now() / 1000 / step);

  // Convert counter to 8-byte buffer (big-endian)
  const counterBuffer = Buffer.alloc(8);
  counterBuffer.writeUInt32BE(Math.floor(counter / 2**32), 0);
  counterBuffer.writeUInt32BE(counter >>> 0, 4);

  // HMAC-SHA1 with the secret
  const secretBuffer = base32Decode(secret);
  const hmac = crypto.createHmac('sha1', secretBuffer).update(counterBuffer).digest();

  // Dynamic truncation
  const offset = hmac[hmac.length - 1] & 0xf;
  const code = (
    ((hmac[offset] & 0x7f) << 24) |
    ((hmac[offset + 1] & 0xff) << 16) |
    ((hmac[offset + 2] & 0xff) << 8) |
    (hmac[offset + 3] & 0xff)
  ) % Math.pow(10, digits);

  return String(code).padStart(digits, '0');
}

// Verify a TOTP code (with ยฑ1 step window)
function verifyTOTP(token, secret, window = 1, digits = 6, step = 30) {
  const currentStep = Math.floor(Date.now() / 1000 / step);

  for (let i = -window; i <= window; i++) {
    const counter = currentStep + i;
    const counterBuffer = Buffer.alloc(8);
    counterBuffer.writeUInt32BE(Math.floor(counter / 2**32), 0);
    counterBuffer.writeUInt32BE(counter >>> 0, 4);

    const secretBuffer = base32Decode(secret);
    const hmac = crypto.createHmac('sha1', secretBuffer).update(counterBuffer).digest();
    const offset = hmac[hmac.length - 1] & 0xf;

    const code = (
      ((hmac[offset] & 0x7f) << 24) |
      ((hmac[offset + 1] & 0xff) << 16) |
      ((hmac[offset + 2] & 0xff) << 8) |
      (hmac[offset + 3] & 0xff)
    ) % Math.pow(10, digits);

    if (String(code).padStart(digits, '0') === token) return true;
  }

  return false;
}

// --- Try it out ---
const secret = 'JBSWY3DPEHPK3PXP'; // Base32 encoded secret
const code = generateTOTP(secret);
console.log('Generated TOTP:', code);
const isValid = verifyTOTP(code, secret);
console.log('Is valid?', isValid); // true

Run this with node totp.js and you'll see a real, working 6-digit code โ€” the same one an authenticator app would generate for this secret right now.

Setting Up the QR Code

To let users scan your secret with Google Authenticator or Microsoft Authenticator, you need to package it in a special otpauth:// URI format, then turn that into a QR code.

The URI looks like this:

otpauth://totp/MyApp:alice@example.com?secret=JBSWY3DPEHPK3PXP&issuer=MyApp&period=30&digits=6

Hereโ€™s how to generate and display a QR code in a Node.js/Express setup:

const express = require('express');
const crypto = require('crypto');
const app = express();

// Generate a random Base32 secret for a new user
function generateSecret(bytes = 20) {
  const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
  const randomBytes = crypto.randomBytes(bytes);
  let secret = '';
  let bits = 0, value = 0;

  for (const byte of randomBytes) {
    value = (value << 8) | byte;
    bits += 8;
    while (bits >= 5) {
      secret += alphabet[(value >>> (bits - 5)) & 31];
      bits -= 5;
    }
  }

  if (bits > 0) secret += alphabet[(value << (5 - bits)) & 31];
  return secret;
}

app.get('/setup-2fa', (req, res) => {
  const userEmail = 'your-mail@example.com'; // in production, get this from session
  const issuer = 'YourApp';
  const secret = generateSecret();

  // Build otpauth URI
  const label = encodeURIComponent(`${issuer}:${userEmail}`);
  const otpauthUri = `otpauth://totp/${label}?secret=${secret}&issuer=${encodeURIComponent(issuer)}&period=30&digits=6`;

  // Build QR code URL using QuickChart
  const qrUrl = `https://quickchart.io/qr?text=${encodeURIComponent(otpauthUri)}&size=250`;

  // In production: save `secret` to the user's record in your database
  // NEVER expose the secret to the client after setup is complete!
  res.send(`
    <h2>Scan this QR code with your authenticator app</h2>
    <img src="${qrUrl}" alt="QR Code" />
    <p>Your secret (store this safely): <code>${secret}</code></p>
  `);
});

app.listen(3000, () => console.log('Server running on http://localhost/3000'));

Once the user scans the QR code, their authenticator app stores the secret and starts generating codes โ€” no further communication needed.

Browser Example (Web Crypto API)

You can also generate TOTP codes entirely in the browser, no server required, using the Web Crypto API:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>TOTP Demo</title>
</head>
<body>
  <h2>๐Ÿ” Browser TOTP Generator</h2>
  <label>Secret (Base32): <input id="secret" value="JBSWY3DPEHPK3PXP" /></label><br><br>
  <button onclick="generate()">Generate Code</button>
  <p>Your TOTP code: <strong id="code">โ€”</strong></p>
  <p>Expires in: <strong id="timer">โ€”</strong>s</p>

  <script>
    const base32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';

    function base32Decode(str) {
      str = str.toUpperCase().replace(/=+$/, '');
      let bits = 0, value = 0;
      const output = [];

      for (const c of str) {
        value = (value << 5) | base32Chars.indexOf(c);
        bits += 5;
        if (bits >= 8) { output.push((value >>> (bits - 8)) & 0xff); bits -= 8; }
      }

      return new Uint8Array(output);
    }

    async function generate() {
      const secret = document.getElementById('secret').value.trim();
      const step = 30;
      const counter = Math.floor(Date.now() / 1000 / step);
      const remaining = step - (Math.floor(Date.now() / 1000) % step);

      // Encode counter as 8-byte big-endian
      const counterBuffer = new ArrayBuffer(8);
      const view = new DataView(counterBuffer);
      view.setUint32(0, Math.floor(counter / 0x100000000), false);
      view.setUint32(4, counter >>> 0, false);

      // Import secret as HMAC-SHA1 key
      const keyData = base32Decode(secret);
      const key = await crypto.subtle.importKey(
        'raw', keyData, { name: 'HMAC', hash: 'SHA-1' }, false, ['sign']
      );

      // Sign the counter
      const signature = await crypto.subtle.sign('HMAC', key, counterBuffer);
      const hmac = new Uint8Array(signature);

      // Dynamic truncation
      const offset = hmac[hmac.length - 1] & 0xf;
      const code = (
        ((hmac[offset] & 0x7f) * 0x1000000) +
        ((hmac[offset + 1] & 0xff) * 0x10000) +
        ((hmac[offset + 2] & 0xff) * 0x100) +
        (hmac[offset + 3] & 0xff)
      ) % 1000000;
      document.getElementById('code').textContent = String(code).padStart(6, '0');
      document.getElementById('timer').textContent = remaining;
    }

    // Auto-refresh every second
    setInterval(generate, 1000);
    generate();
  </script>
</body>
</html>

Save this as totp.html, open it in your browser, and watch the code update live every 30 seconds. Try entering the same secret in Google Authenticator โ€” the codes will match! ๐ŸŽ‰

โœˆ๏ธ Yes, It Works Offline โ€” Hereโ€™s Why Thatโ€™s Brilliant

This one surprises a lot of people, so letโ€™s take a moment to appreciate it.

Open Google Authenticator on a plane with airplane mode on. No Wi-Fi, no cellular signal, completely cut off from the internet. Your authenticator app will still show you a perfectly valid, working TOTP code.

How? Because TOTP needs exactly two things to generate a code: the shared secret and the current time. Both of those already live on your phone. Thereโ€™s nothing to fetch, nothing to download, no server to ping.

Think about what this means in contrast to SMS-based 2FA. With SMS codes, if youโ€™re in a basement with no signal, traveling internationally without a data plan, or your carrier has an outage โ€” youโ€™re locked out. TOTP has none of those problems. Once youโ€™ve scanned the QR code during setup, your authenticator app is completely self-sufficient.

This is actually one of the reasons security professionals tend to prefer TOTP over SMS 2FA. Not only is it more resilient to network issues, but itโ€™s also immune to SIM-swapping attacks (where a bad actor tricks your carrier into transferring your number to their SIM card). Thereโ€™s no phone number involved at all โ€” just math running quietly on your device.

The only thing your phone does need is an accurate clock. Since the code is derived from the current time, if your device clock drifts significantly (usually more than 60โ€“90 seconds), the codes will stop matching. But thatโ€™s a solved problem โ€” modern phones sync their clocks automatically via the network, and they hold accurate time even when offline for extended periods.

So the next time youโ€™re logging in from a cafe with spotty Wi-Fi, a remote campsite, or mid-flight, and your authenticator app just works โ€” youโ€™ll know exactly why. ๐Ÿ›ซ

Security Tips Worth Knowing ๐Ÿ›ก๏ธ

Now that you know how it works, here are some practices youโ€™ll want to follow when building TOTP into your own apps:

Protect the secret like a password. The shared secret is the key to the kingdom. Store it encrypted in your database. Never log it. Never expose it in an API response after setup.

Use a strong, random secret. RFC 6238 recommends at least 160 bits (20 bytes) of randomness for SHA-1 based secrets. Use crypto.randomBytes() in Node.js, not Math.random().

Keep the verification window small. Accepting codes within ยฑ1 time step (so 90 seconds total) is generally safe and handles clock drift. Donโ€™t stretch it to ยฑ5 or ยฑ10 โ€” that defeats the purpose.

Consider SHA-256 or SHA-512. RFC 6238 explicitly supports stronger hashing algorithms. Just note that some older authenticator apps only support SHA-1, so test compatibility first.

Always offer backup codes. If a user loses their phone, they need a way back in. Generate a set of single-use recovery codes at setup time, hash them, and store only the hashes.

Troubleshooting: โ€œMy Code Isnโ€™t Working!โ€

If the generated code doesnโ€™t match, here are the most common culprits:

Clock drift. TOTP is extremely sensitive to time. If your server or device clock is off by more than a minute, codes will misalign. Keep your system clocks synced via NTP.

Wrong secret encoding. Authenticator apps expect the secret in Base32 format. If youโ€™re passing raw bytes or hex, it wonโ€™t work.

Wrong algorithm. If your server is computing with SHA-256 but the app is using SHA-1, the codes will differ. They must match on both sides.

Wrapping Up

TOTP is one of those things that sounds mysterious until you actually look at it โ€” and then itโ€™s surprisingly elegant. A shared secret, the current time, and a dash of HMAC cryptography. Thatโ€™s genuinely all it takes to create a robust second factor that millions of people use every day.

What we covered together today:

  • Why TOTP works without any network communication at the time of login
  • Why it works completely offline โ€” and why that makes it better than SMS 2FA
  • The algorithm behind it, as defined in RFC 6238
  • How to generate and verify codes in Node.js from scratch
  • How to build the QR code setup flow for your users
  • A fully working browser-based implementation using the Web Crypto API
  • Security best practices to keep your implementation solid

The next step? Try wiring this into a real login flow. Add TOTP setup to a user settings page, save the secret encrypted in your database, and verify the code on every login. Youโ€™ll be surprised how straightforward it is once the concept clicks.

If you found this helpful, feel free to share it with someone whoโ€™s been curious about how 2FA actually works under the hood. And if you have questions, drop them in the comments โ€” Iโ€™d love to hear from you. ๐Ÿ™Œ

References:


๋ฉ”ํƒ€๋ฐ์ดํ„ฐ
post_id
5885e2e6b6d1
slug
totp-explained-how-that-6-digit-code-actually-keeps-you-safe-5885e2e6b6d1
url
https://medium.com/@jainsarwang/totp-explained-how-that-6-digit-code-actually-keeps-you-safe-5885e2e6b6d1
canonical_url
https://medium.com/@jainsarwang/totp-explained-how-that-6-digit-code-actually-keeps-you-safe-5885e2e6b6d1
author_url
https://medium.com/@jainsarwang
status
ok
fetched_at
2026-06-21 19:25:17