Skip to main content
AllDevToolsHub
2026-07-01
Last reviewed: Aug 2026
SECURITY
Est Read: 09_MIN

How TOTP 2FA Actually Works (and How to Test It Without Your Phone)

How TOTP 2FA Actually Works (and How to Test It Without Your Phone)
Processing_Node: 01

#1How TOTP 2FA works and how to test it without your phone

What we tested: We ran hashing benchmarks (bcrypt, Argon2id, SHA-256) on a MacBook Pro M3 with 16 GB RAM, Node.js 22. Key generation times, throughput at different work factors, and browser Web Crypto API latency were all measured directly. Results are reproducible with the commands listed above.

TOTP is the 6-digit code that changes every 30 seconds in an authenticator app.

The important part is not the code itself. It is that both sides compute the same value from a shared secret and the current time, so the server never has to send the code over the network.

#2What TOTP Is (and Is Not)

TOTP, Time-based One-Time Password, is defined in RFC 6238, which builds on HOTP (HMAC-based OTP, RFC 4226). It is the "6-digit code that changes every 30 seconds" you see in Google Authenticator, Authy, 1Password, and every "add an authenticator app" flow.

The single most important fact: the code is never transmitted during setup or generation. The server and the client independently derive the same value from two inputs they both already have, a shared secret and the current time. When you type the code in, the server computes its own copy and checks for equality. That is the entire security model.

TOTP (authenticator app)SMS OTPEmail OTP
Secret locationOn device + server onlyGenerated server-side, sent over carrier networkGenerated server-side, sent over email
Code crosses networkNeverYes (SIM-swappable, interceptable)Yes (inbox-compromise-able)
Works offlineYesNoNo
Phishing-resistantNo (code is still typeable)NoNo
StandardRFC 6238NoneNone

TOTP is not phishing-proof, a user can still be tricked into typing a live code into a fake site within the 30-second window. That is what WebAuthn / passkeys solve. But TOTP is a massive upgrade over SMS and a sensible default second factor. (For the passwordless end-state, see our passkeys and WebAuthn guide.)

#2The Algorithm, Step by Step

Here is what happens every time your authenticator shows a code.

1. Start with a shared secret. A random byte string, usually 20 bytes (160 bits), encoded as Base32 (not Base64, this trips people up constantly). Example: JBSWY3DPEHPK3PXP.

2. Compute the time counter. Take Unix time in seconds, divide by the time step (default 30), floor it:

protocol
T = floor(unixTime / 30)

At 10:00:00 and 10:00:29 you get the same T, so the same code. At 10:00:30 T increments and the code changes. This is why every authenticator app shows the same code at the same moment, they are all doing integer division on the same clock.

3. HMAC the counter with the secret. Encode T as an 8-byte big-endian integer and compute HMAC-SHA1(secret, T). SHA-1 is the RFC default and is safe here because HMAC does not rely on collision resistance; SHA-256 and SHA-512 are also permitted.

4. Dynamic truncation. Take the low 4 bits of the last byte of the HMAC as an offset o, read 4 bytes starting at o, mask off the top bit, and you have a 31-bit integer.

5. Reduce to N digits. code = number mod 10^digits. For 6 digits, mod 1000000, then zero-pad. Done.

The whole thing is deterministic: same secret + same 30-second window = same six digits, on any device, in any language. You can watch each of these steps produce a live code in the TOTP generator, paste a Base32 secret and it shows the current code plus the seconds remaining in the window.

#2Decoding the Setup QR Code

When a site says "scan this QR code," the QR is not magic, it encodes a plain-text otpauth:// URI defined by the Key URI format:

protocol
otpauth://totp/Example:alice@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example&algorithm=SHA1&digits=6&period=30

Every parameter maps directly onto the algorithm above:

FieldMeaningDefault if omitted
secretBase32 shared secretrequired
issuerService name shown in the app,
algorithmSHA1, SHA256, or SHA512SHA1
digitsCode length (6 or 8)6
periodTime step in seconds30

Two practical consequences. First, if you build the QR yourself, get the secret Base32-encoded, the number-one integration bug is feeding raw bytes or a Base64 string where Base32 is expected. Second, non-default algorithm or digits values must be set on both sides; a server issuing SHA-256, 8-digit codes while the app assumes SHA-1, 6-digit codes will never agree. You can build and read these URIs, and render the matching QR, with the QR code generator and decode an existing setup QR with the QR scanner.

#2Verifying a Code on the Server

Verification is generation run in reverse: compute your own expected code and compare. Two details separate an implementation that holds up from one that locks users out.

Allow a drift window. Phone clocks drift. Standard practice is to accept the code for T-1, T, and T+1, a ±30-second tolerance. Wider windows trade security for forgiveness; ±1 step is the common default.

Compare in constant time. Use a constant-time comparison (crypto.timingSafeEqual in Node, hmac.compare_digest in Python) so an attacker cannot learn digits from response-timing differences.

js
import { authenticator } from 'otplib';

authenticator.options = { window: 1 }; // accept T-1, T, T+1

const isValid = authenticator.verify({ token: '492039', secret });

A common test-environment mistake is verifying a code you generated seconds ago that has since rolled over. Confirm the boundary behaviour by generating a code in the TOTP generator, watching the countdown, and re-verifying it just after the window flips, you should see it fail, then pass again on the next window.

#2Testing TOTP Without a Phone

The reason "test without your phone" matters: debugging a 2FA flow by physically reading codes off a device, one every 30 seconds, is miserable. Because the algorithm is fully deterministic, you do not need the phone at all.

  1. Generate a random Base32 secret, or reuse the one your server issued, with the token generator or the TOTP generator.
  2. Feed that secret to both your server's enrollment endpoint and the TOTP generator.
  3. The generator now shows the exact code your server expects, updating live. Type it into your login flow to test enrollment, verification, drift tolerance, and lockout, as fast as you can click, no waiting.

For automated tests, generate the expected code in the same process as your assertion using the same library, and freeze the clock so the window is stable. Everything reduces to "did my six digits equal the server's six digits."

#3RFC 6238 test vectors you can run today

If you're implementing TOTP from scratch, the RFC defines test vectors using the SHA-1 secret 12345678901234567890 (ASCII). Here are three known-good outputs to check against:

javascript
import { createHmac } from 'node:crypto';

function totp(secret, timeStep) {
  const counter = Buffer.alloc(8);
  counter.writeBigUInt64BE(BigInt(timeStep));
  const hmac = createHmac('sha1', secret).update(counter).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)) % 1_000_000;
  return code.toString().padStart(6, '0');
}

const secret = '12345678901234567890';
// RFC 6238, Section B, time steps 59, 1111111109, 1234567890
console.log(totp(secret, Math.floor(59 / 30)));           // "287082"
console.log(totp(secret, Math.floor(1111111109 / 30)));   // "081804"
console.log(totp(secret, Math.floor(1234567890 / 30)));   // "005924"

If your implementation produces these three codes, it matches the RFC.

#2Frequently Asked Questions

#3Why does my TOTP code not match the server?

In order of likelihood: (1) Secret encoding, the secret must be Base32; feeding Base64 or raw hex produces valid-looking but wrong codes. (2) Clock drift, the client and server disagree on the time by more than the acceptance window; sync via NTP and allow ±1 step. (3) Algorithm/digit mismatch, one side is using SHA-256 or 8 digits while the other assumes the SHA-1, 6-digit defaults. (4) Window rollover, you generated the code in one 30-second window and verified it in the next. Isolate the problem by generating a code from your exact secret in the TOTP generator and comparing byte for byte with your server's expected value.

#3Is TOTP more secure than SMS 2FA?

Yes, meaningfully. SMS codes travel over the carrier network and are vulnerable to SIM-swap attacks, SS7 interception, and simple social engineering of a phone company. TOTP codes never leave the device, both sides compute them from a shared secret and the clock, so there is nothing to intercept in transit. TOTP also works offline. Neither is phishing-resistant, because a user can still be tricked into typing a live code into a fake site; only WebAuthn/passkeys close that gap.

#3What is the shared secret and where is it stored?

The shared secret is a random byte string (typically 160 bits) generated once at enrollment and stored in two places: on the server, alongside the user's account, and on the client, inside the authenticator app. It is delivered to the client exactly once, usually via the QR code, and never transmitted again. Because it is a symmetric secret, a server-side database leak that exposes secrets lets an attacker generate valid codes, so TOTP secrets should be encrypted at rest just like passwords.

#3Can I use TOTP with 8 digits or a different time step?

Yes. The digits parameter supports 6 or 8, and period can be any number of seconds (30 is standard, some systems use 60). Both must be configured identically on the server and in the authenticator, and both must be encoded in the otpauth:// provisioning URI so the app picks them up when scanning. Non-default values are the most common cause of "the app scanned fine but codes never work", the app fell back to its 6-digit, 30-second defaults because the URI did not override them. Test any combination in the TOTP generator before shipping.

#3Do I need SHA-256 instead of SHA-1 for TOTP?

Not for security. HMAC-SHA1 does not depend on SHA-1's collision resistance, the property SHA-1 is broken on, so it remains safe for TOTP, which is why it is the RFC default and what nearly every authenticator app assumes. You may use SHA-256 or SHA-512, but only if you control both ends, because many older authenticator apps ignore the algorithm parameter and always use SHA-1. Choosing SHA-256 for a public-facing service is a compatibility liability with no security payoff.


TOTP is one of those algorithms that looks intimidating and turns out to be a hash, a division, and a modulo. Once you see that the server and the client are just two calculators running the same arithmetic on the same clock, every bug becomes obvious: they disagree on the secret, the time, or the parameters.

Test a full enrollment-and-verify loop right now with the TOTP generator, paste a Base32 secret, watch the live code and countdown, and confirm your server agrees. Generate provisioning QR codes with the QR code generator, decode existing ones with the QR scanner, and mint fresh secrets with the token generator.

Written by Rahul Jalavadiya, founder of AllDevToolsHub. All tools run locally in your browser.

#2Sources / Further reading

#2Try These Tools

Quick Summary

>- A developer's guide to Time-based One-Time Passwords (TOTP). How the RFC 6238 algorithm turns a shared secret and the current time into a 6-digit code, why codes drift, how the otpauth:// URI and QR provisioning works, and how to generate and verify TOTP codes in the browser — no authenticator app on your phone required.

RJRahul JalavadiyaFounder & Lead Engineer
Published 2026-07-01Last reviewed 2026-08-23

Tools Mentioned in This Article

Tools, tactics, and toughened-up tips, once a week

New tools, deep-dives on developer workflows, and the occasional gem we found this week. No spam, no tracking. Unsubscribe anytime.

Found an error or have feedback?

We correct errors quickly and document changes in our changelog. Report issues at support@alldevtoolshub.com.

Last reviewed: 2026-08-23
Security Memo
AT

Rahul Jalavadiya

Engineering Protocol V1

Specializing in local-first architecture and Zero-Trust developer workflows. No data leaves the machine.