HomeMethodThe window you cannot read

Why an expired 2FA code still works — and why no app can tell you for how long

The authenticator is the readable half: a 30-second period, six digits, SHA-1, floor(seconds / 30), HMAC, truncate, modulo. The project's own case study walks that path and stops at the other half — how many steps a verifier is willing to accept. That number is not in your app.

Scope. Run this against a verifier you operate, or one you have written permission to test: a self-hosted service, a dev instance, your own account inside a published bug-bounty scope. Do not probe someone else's login. Rate limiting and lockout are why windows are hard to map: you often get a handful of submissions before a delay starts, which is barely enough to test one step either side. Prefer code you can read.

What "valid for 30 seconds" does not mean

RFC 6238 (May 2011, Informational) defines the moving factor as a division, not a countdown.

T = floor((Current UNIX time − T0) / X)      default X = 30 s, T0 = 0

The three knobs, and who owns each

KnobDefault / spec guidanceWhere it livesHow you learn it
Time step X30 s. The spec recommends keeping this default; smaller steps raise the number of valid OTPs per periodBoth sides — visible in the appStatic. The published APK trace shows setPeriod(30)
Digit countRFC defines 6, 7 and 8. Verification systems are told to use at least 8 during generation and show 6–8 to users for high-value servicesBoth sidesStatic. this(key, "SHA1", 6) in the same trace
Acceptance windowThe spec recommends at most one time step as network delay. A larger window trades security for reliabilityVerifier onlyBehaviour only — see the table below

The asymmetry. A verifier that tolerates two steps back also tolerates two steps forward unless it checks direction. "My old code still works" and "my future code already works" are often the same policy. Testing the forward half is what separates a measurement from a guess.

Resynchronisation

The spec's resynchronisation section describes a verifier that, having accepted a code from an off-by-N step, records the offset for that token and applies it later. Its worked example: with a 30-second step and tolerance of two backward steps, the maximum elapsed drift it can accept is about 89 seconds — 29 remaining in the calculated step plus 60 for the two extra — and reaching it costs three comparisons (current, minus one, minus two). It also recommends a limit on how far out of synch a user may be, in both directions.

Behaviour you observeFixes the window asKills the rival hypothesis because
Backward accepted to −k, forward rejectedWindow is one-sided (backward k)A symmetric window would accept forward codes too
Backward −k and forward +k acceptedSymmetric ±kDirection is not being checked
A shifted code is accepted after the window should have expired, but only following an off-window successDrift is recorded per tokenA fixed window cannot produce this, and re-enrolment should wipe the effect
Second use of the same accepted code rejectedReplay protection is liveThis is the expected result
The same code accepted twiceA replay hole, not generosityReplay inside one window is what verifiers are told to prevent

One test per step, at most. Log timestamps and outcomes. Do not hammer a login form — a lockout ends the experiment.

In this tabHMAC-SHA1 · no network

Paste a base32 secret you enrolled yourself. Codes use the browser's own HMAC-SHA-1 via crypto.subtle. No key, code, or timestamp is sent anywhere.

current unix time: —

OffsetStep TCodeMeaning
Paste your own base32 secret to generate codes.
// output appears here

How to ask for the half you can read

Read the OTP defaults only, and stop before the UI. Report: time step,
digit count, hash algorithm, and the period's default value — each with
the class, method and source range. Do not explain two-factor auth, and
do not assume anything about the server. Then list every parameter you
found that the verifier could override at enrolment, because those are
the ones that cannot be measured from the app alone.

The defaults are in the app. The enrolment payload — which may carry a different period or digit count per account — decides whether local codes match the server at all.

Findings and non-findings

ClaimEvidenceConfidenceWhat it does not prove
Step 30 s, six digits, SHA-1, floor(sec/period), dynamic truncation, modulo digit count, zero paddingPublished case study on Aegis 3.4.3 (inspected with REA 6.1.0, 9 Oct 2026)HighThat APK version only, and only Aegis
The acceptance window cannot be recovered staticallyThe verifier runs server-side. No local file contains itHighIt does not say a window is hard to bound by observation
Window of at most one step is the spec's recommendation; about 89 s and 3 comparisons in the two-back exampleRFC 6238 §§4.1, 5.2, 6 (May 2011)HighWhat any given product actually implements
Most services accept one step backForum folklore, not measured by usNot verified — excluded—

What we could not confirm. This protocol was not run against any third-party login, and those results would not be published here. The tables are the method and the spec's own numbers.

Next

Sources: RFC 6238 (IETF, May 2011, Informational) §§4.1, 5.1–5.5, 6, Appendix B · Aegis Authenticator (GPL-3.0) and the published REA case study, inspected with REA 6.1.0 on 9 Oct 2026 · Independent site, not affiliated with the REA authors.