Build1 distinct publisher3 min readPublished
A dev.to walkthrough of Auth0-style reuse detection puts the whole detector in two lookups: is this token spent, and is its family still active. Delete-on-rotate can answer neither of them.
The Engineer · Build desk

Compiled by The EngineerSomething wrong?How this is made
The detector is two branches of one function. The sample `rotateRefreshToken` checks `record.status === 'spent'`, then checks `family.status !== 'active'`, and throws `invalid_grant` either way [11]. The client learns nothing from the difference. The difference lives in the audit write, which carries a type of `refresh_token_reuse` plus the subject and the family id [17]. That split is correct engineering: the error code is a channel an attacker can also read, and the forensics belong somewhere only you can read [11][19].
The token value itself is 32 random bytes rendered base64url [12]. That is 256 bits of entropy in 43 characters unpadded [14]. It is opaque, so the server resolves it by value; a JWT-encoded refresh token is resolved by its `jti` claim instead [5]. Either way the server has to hold a record for every value it has ever issued in that family, because the spent marker is the recognizer [4].
So price the storage honestly. Rows retained per family equal rotations plus one [15]. Pruning is only safe when the family itself dies, since a deleted spent row is a replay you can no longer identify [4]. Delete-on-rotate is the cheapest possible way to forget the single thing you needed to remember.
There is a second cost, and the pseudocode shows it plainly. The reuse branch keys on the status of the presented token, not on which party presented it [16]. The walkthrough's claim is that a spent token can only reappear if two parties held it at once [7]. Grant that, and the response is still to revoke the whole family, including the active token the legitimate client is holding, forcing a full re-authentication [8]. If any benign path in your client can present a value it already exchanged, the penalty falls on the real user's session. Test that path before you ship the revoke-all default.
What transfers from this piece is not the vendor shape. It is the state requirement. Rotation on its own only shrinks the replay window, because the server cannot separate the attacker's exchange from the app's [2], and a static token leaves a credential valid for a TTL the author puts at 30 to 90 days with no signal to the owner [1]. The two lookups that produce the signal are exactly what a naive implementation skips when it deletes old tokens rather than marking them spent [13].
That has a scheduling consequence teams underrate. Detection is prospective only. Change the write path today and you gain lineage for families opened after the change; the sessions already running were never recorded, so their spent values are gone [4][13]. My read, in a context where refresh tokens live on mobile clients: the family record is worth its row count, and the revoke-all step is worth its support tickets, because rejecting the reused token alone leaves a live credential in play [10]. If your refresh tokens are short-lived and confined to a first-party web session, the arithmetic may land elsewhere, and I would want to see the forced re-auth rate before defending the default.
Ranked by verification strength, evidence, and original report placement.
A static, long-lived refresh token is a bearer secret with an unlimited replay window; if it leaks, from a compromised mobile device, a logged network request or a misconfigured CI cache, the attacker holds a valid credential for as long as the token's TTL allows, often 30-90 days, with zero signal to the legitimate owner.
Rotation on its own only shrinks the window: every refresh call returns a new refresh token and burns the old one, but an attacker who grabs a token can still race the real client and use it once, and the server has no way to tell attacker use apart from app use.
Every refresh token belongs to a family created when the first refresh token is issued, typically at login, holding a family_id, an ordered lineage array of tokens marked used or active, the subject, the client, and a status of active or revoked.
Each rotation appends to the lineage array and marks the previous token spent, not deleted; the server needs to remember that earlier tokens existed and were already consumed so it can recognize them if they show up again.
On every POST /oauth/token with grant_type=refresh_token, the server looks the token up by its opaque value, or by the jti claim in JWT-encoded refresh tokens, and resolves its family.
If the presented token matches the family's current active token, the normal path runs: mark it spent, generate a new token, append it to the lineage, and return it.
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 30, 2026
Follow any of these and your For You feed starts watching them — no settings page required.
build
Your JWT Login Probably Has Exactly One Kill Switch: Log Everyone Out1 distinct publisher
build
The /userinfo fallback that quietly made Auth0 a hard dependency on every request1 distinct publisher
build
Axonius runs one agent per tenant on AgentCore, and tracks model cost the same way1 distinct publisher
leadership
Naming an AI agent moves the blame from its owner to the technology1 distinct publisher
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Auditable code, unaudited claims
Two very different grades of evidence sit side by side. The algorithm and the sample function are self-verifying — you can read the three throw sites and the 32-byte token generator yourself. Everything about the world outside the snippet, including what Auth0 emits on reuse and what "most production implementations" do about retries, rests on one developer's post with no vendor documentation, no test, no incident and no second engineer weighing in. The post also argues against itself on the central inference.
Asserted, never shown
dev.to says Auth0, Okta and most production servers run this with a configurable grace period, and that is the closest thing to uptake anywhere in our coverage. No console setting, changelog line, version, deployment or breach report accompanies it, so there is nothing to measure — only a claim about other people's systems made by someone outside them.
The title outruns the algorithm
"Before It's Ever Replayed" is the single thing this design cannot do: the spent-row check fires on the second presentation, which is to say after the replay has happened. Inside the piece the overreach is smaller — one "can only happen" that the retry discussion later walks back — and the core argument, that deletion destroys the evidence, is stated no more strongly than it deserves.
Vendor-branded, not vendor-funded
Auth0's name is in the title of a post that is not Auth0's, and the pattern being explained is generic OAuth practice rather than anything proprietary — vendor branding here buys search traffic. Against that pull, the author strips the example of any SDK and tells readers who use a provider that they configure this rather than build it. No sponsorship or affiliation is disclosed, and none is visible in the text.
Sure about the state machine, unsure about the world
We can be firm on the mechanics — the code is short, complete and internally consistent, and the storage and error-opacity consequences follow from it directly. We are much weaker on whether the described vendor behaviour and the industry-standard grace period exist as stated, since one uncorroborated post is the whole record.