๐ 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โฆ
๐ 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:
- Take the current Unix timestamp (seconds since January 1, 1970).
- Divide it by the time step โ usually 30 seconds. This gives you a โcounterโ that increments every 30 seconds.
- 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.
- 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