Learn 2FA Authentication - Testing: RFC Test Vectors & Real-Device E2E
Episode 16 of 23

Learn 2FA Authentication - Testing: RFC Test Vectors & Real-Device E2E

This episode tests 2FA thoroughly: validating TOTP against the official RFC 6238 test vectors with fake timers, testing clock skew and window tolerance, and E2E with a real Google Authenticator including the negative scenarios of wrong codes, expiry, and replay.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

A 2FA feature orchestrates cryptography and time — two things that are hardest to test manually. Episode 16 covers a thorough testing strategy: unit tests with the official RFC 6238 test vectors, time testing with fake timers, and E2E with a real Google Authenticator.

Why can't this testing be postponed? Because TOTP bugs surface worst in production — codes sometimes accepted, sometimes not, locking users out and spiking support tickets. You'll build three layers of testing that catch these problems long before users feel them.

The three layers complement each other, not replace each other: unit tests prove the formula is correct, time tests prove the tolerance is reasonable, and E2E proves real users can get through the flow.

Unit Tests with RFC 6238 Test Vectors

Official Vectors as the Source of Truth

RFC 6238 contains a test vector table: pairs of time and code that are guaranteed correct for a given secret. These vectors are the benchmark — if the library produces the same codes, the implementation matches the standard. The secret used is the ASCII string "12345678901234567890" encoded in Base32.

Test with fake timers and the 8-digit SHA1 vector:

JSTest codes with the official RFC 6238 vectors
const { authenticator } = require('otplib');
const SECRET = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ';
 
jest.useFakeTimers();
jest.setSystemTime(new Date('1970-01-01T00:00:59.000Z'));
 
test('TOTP matches the RFC 6238 vector', () => {
  authenticator.options = { algorithm: 'SHA1', digits: 8, step: 30 };
  expect(authenticator.generate(SECRET)).toBe('94287082');
});

The time 1970-01-01 00:00:59 yields counter 1, and the RFC 6238 vector for counter 1 with SHA1 and 8 digits is 94287082. jest.setSystemTime replaces the real clock so the test doesn't depend on the currently running time.

For convenience, set this secret as a constant in a test helper, so all test files use the same source. If the secret changes in one place without changing the others, a test failure immediately reveals the inconsistency.

Not Just One Vector

A single vector confirms the basics, not the whole. Test several points: counter 0, a large counter value, digits 6 and 8, plus the SHA256 and SHA512 algorithms that also have vectors in the RFC. Each combination covers a different code branch inside the library and ensures the configuration options translate correctly.

Also use a simple round-trip test: generate with otplib then verify with check at the same time, to ensure the generate/verify pair behaves consistently beyond the static vectors.

Testing Time and Window

Clock Skew

TOTP lives and dies by clock synchronization. Test the tolerance by advancing time past the step boundary:

JSTest clock skew within the tolerance window
authenticator.options = { step: 30, window: 1 };
 
jest.setSystemTime(new Date('1970-01-01T00:00:00.000Z'));
const kodeAwal = authenticator.generate(SECRET);
 
jest.setSystemTime(new Date('1970-01-01T00:00:45.000Z'));
const kodeBerikutnya = authenticator.generate(SECRET);
 
expect(authenticator.check(kodeAwal, SECRET)).toBe(true);
expect(authenticator.check(kodeBerikutnya, SECRET)).toBe(true);

At second 45, the current step is 1 while kodeAwal comes from step 0. With window 1, the old code is still accepted — exactly the behavior that keeps users with a slightly slow clock from being locked out.

Negative Time Tests

Also test the failing side: a code from a step far outside the window must be rejected, and the same code must not be accepted twice (replay). These two scenarios are the ones most often missed in manual testing and the most dangerous in production — episode 10 built the lastUsedStep mechanism that must be tested here.

For simulating a deviating clock, some frameworks provide helpers to advance the system time in the test process — use them carefully and restore the real clock afterwards so other tests aren't disturbed.

E2E with a Real Device

A Real Google Authenticator

Unit tests prove the math; E2E proves compatibility. Run the app on local HTTPS (episode 13), open the Enable 2FA page, and scan the QR with a real Google Authenticator:

E2E test flow with a real device
1. Scan the QR with Google Authenticator
2. Verify the first code in the confirmation form
3. Logout then log in again
4. Enter the TOTP code from the app
5. Test a recovery code after removing the account from the app

Testing with a real device catches things invisible to unit tests: scan compatibility, the format of the displayed secret, and the authenticator app's behavior on a specific version.

Do this test on several devices: Android and iOS use different Google Authenticator versions, and import/export behavior can differ between platforms.

Negative Scenarios That Must Be Tested

Complete the set with negative scenarios: a mistyped code is rejected, an expired code is rejected, the same code used twice is rejected, an already-used recovery code is rejected, and rate limiting blocks after 5 attempts. Record the results in a checklist that can be repeated at every release — this protects against regressions silently sneaking in with other changes.

Automate these scenarios via integration tests where possible, and keep the manual output as a release attachment — test evidence is often requested in audits.

Conclusion

Episode 16 completed testing with three layers: unit tests against the RFC 6238 vectors with fake timers, clock skew and window tolerance tests, and E2E with a real Google Authenticator including all the negative scenarios.

The key takeaways:

  • The RFC 6238 test vectors are the source of truth for TOTP codes.
  • Use fake timers so tests don't depend on the running clock.
  • Test more than one vector: SHA1, SHA256, digits 6 and 8.
  • Test clock skew both within and outside the window.
  • E2E with a real device catches compatibility problems.
  • Test all negative scenarios: wrong, expired, replay, used recovery.

In the next episode, episode 17, we will cover beyond TOTP: WebAuthn and passkeys — the phishing-resistant FIDO2 principles without a shared secret, a comparison of TOTP versus WebAuthn versus SMS, and a strategy for offering both at once.

Learn 2FA Authentication - Testing: RFC Test Vectors & Real-Device E2E | Learn 2FA Authentication