Articles
Refresh Token Rotation Interview Questions: Reuse Detection, Races, and Revocation
Prepare for refresh token rotation interviews with reuse detection, concurrent refresh races, token-family revocation, lost responses, and recovery tradeoffs.
Refresh token rotation interview questions become interesting when a perfectly legitimate client presents an old token. Two tabs may refresh together. A server may commit a rotation and lose the response. A retry may look identical to reuse by someone who stole the credential. Replacing one string with another does not resolve those cases; you need a state model and an explicit recovery policy.
Start by defining what the server records, which transition is atomic, and what revocation prevents.
What does rotation detect, and what does it leave unresolved?
Official facts: RFC 9700 requires refresh tokens issued to public clients to be sender-constrained or use rotation. Its rotation model replaces the refresh token on each refresh, invalidates the previous one, and retains relationship information so reuse can trigger revocation of the active refresh token. The server cannot determine which presenter is legitimate from reuse alone. OAuth Security Best Current Practice
Editorial inference: the models, traces, and tests below are original interview exercises. They deliberately omit production cryptography, storage, and provider-specific behavior.
A refresh token is presented to the authorization server to obtain access tokens. It is not the credential an ordinary resource API should accept for business requests. Keep those endpoints separate in the diagram. Official facts: RFC 6749 defines the refresh exchange and requires the client to replace its old refresh token if a new one is issued. RFC 6749, section 6
Rotation can expose competing use after a token has been copied. It does not prove that the first presenter was legitimate, prevent every initial malicious refresh, or remove access tokens already issued. Those limits shape the recovery and containment story.
Define the family before writing endpoint pseudocode
Use synthetic labels R0, R1, and R2 for successive refresh tokens. They are teaching identifiers, not usable credentials. A family represents one refresh-token lineage associated with a grant and client. Separate devices or grants may have separate families; do not silently make one replay log out every device unless that is the chosen policy.
For the exercise, keep a family record containing its status, client binding, expiry policy, and current generation. Keep token records containing a lookup digest, family ID, generation, consumption time, and successor reference. A digest supports lookup without retaining ordinary plaintext token values. Actual credential generation and digest design need their own security review.
The central invariant is: an active family has at most one current refresh token, and a revoked family can never create another successor. The next invariant is that consuming R0 and creating R1 belong to the same durable transition.
| Current state | Presented token or event | Strict exercise outcome |
|---|---|---|
| Active family; R0 current | Valid R0 from the bound client | Consume R0 and create R1 atomically |
| Active family; R1 current | Previously consumed R0 | Reject and revoke the family |
| Revoked family | Any token in that family | Reject without issuing a successor |
| Expired family or token | Otherwise valid token | Reject under the defined expiry policy |
| Unknown token | Unrecognized credential | Reject without inventing a family to revoke |
This table specifies a strict policy. Later sections discuss why a provider may define a narrowly bounded overlap policy and why a client must not assume one exists.
Where is the atomic boundary?
Imagine two workers reading R0 as current. Both construct a successor and both return success. The implementation now has two live branches, even though each worker individually “rotated the token.” A transaction around unrelated inserts does not solve this if the competing read and update remain unprotected.
An original design option is to serialize transitions on a stable family record. Within the same transaction, validate the client and token state, check the family is active, consume the current token, insert exactly one successor, and advance the family generation. A conditional update or lock must make competing transitions conflict.
The reuse path must participate in the same ordering. Locking only the old token row is insufficient if another worker can rotate its successor while revocation updates a different record. A stable family lock or an equivalent proven concurrency scheme makes revocation and successor creation comparable in one order.
Do not return success before the state commit. If persistence fails, return a failure without publishing an uncommitted successor. If commit succeeds but response delivery fails, the server has a different problem: the client may still hold R0. That ambiguity cannot be fixed by pretending the transaction rolled back.
Trace two tabs through a strict policy
Tab A and tab B both hold R0. They independently notice that an access token is expiring and send refresh requests. Worker A wins the family transition and creates R1. Worker B then presents the already-consumed R0. Under the strict exercise policy, B's request revokes the family, including R1.
Both browser tabs may belong to the same user. The outcome is still consistent with the policy because the server lacks enough evidence to classify the second bearer presentation as harmless. Treating every duplicate as an attack attribution would overstate the evidence; treating every duplicate as harmless would weaken containment.
A client can reduce accidental races by coordinating one in-flight refresh and sharing its completed result with waiting requests. In a multi-process backend, an in-memory mutex in one worker does not coordinate all workers. In a browser, separate tabs may also need SDK-supported coordination. State the coordination scope explicitly.
After a terminal refresh failure, stop retry loops and follow the provider's reauthorization path. Repeatedly sending the same invalid token does not restore the session and can obscure the original failure in operational logs.
A lost response is different from a rejected refresh
Consider this trace: the server consumes R0, commits R1, and sends a response that never reaches the client. The client times out while still storing R0. From the client's perspective, the timeout does not reveal whether the server committed. Blindly retrying R0 can therefore activate reuse detection.
An interview answer should name the recovery contract before adding retries. With strict rotation and no supported replay mechanism, a fresh authorization may be necessary. A provider-specific overlap window may tolerate selected duplicate exchanges, but its duration and successor behavior must come from that provider's documented contract.
Vendor-specific fact: Auth0 exposes a configurable rotation overlap period to accommodate concurrency and network delay. Its documentation describes which previous token qualifies and when reuse detection applies. This is an Auth0 setting, not an OAuth-wide promise that every old token remains valid briefly. Auth0 rotation configuration
For an original system-design alternative, discuss bounded idempotent response replay. A server would need to identify the same authorized request, bind its replay record to the client and family, and return the same committed successor without creating a second branch. An arbitrary new idempotency key must not let someone redeem an old token again.
There is also a storage consequence: a digest of R1 cannot reconstruct R1 for response replay. Returning the identical credential requires an appropriately protected recoverable response or another carefully reviewed design. “Store only hashes and resend the old response” is incomplete unless you explain where that response can be recovered.
Revoking refresh capability is not instant API logout
A revoked family should fail at the token endpoint immediately under the server's committed state. An already-issued access token may still pass a resource server's validation until its expiry or until that resource server learns about revocation. Draw this as a second boundary rather than hiding it behind the word logout.
Official facts: RFC 7009 defines token revocation and discusses the relationship between tokens, grants, and implementation-dependent propagation. The behavior of related access tokens depends on server support and the deployment. Do not promise universal instantaneous invalidation from a refresh-family update. OAuth Token Revocation
For the exercise, compare short access-token lifetimes, online validation, and a revocation distribution mechanism. Each changes latency, availability, and the maximum stale-authorization interval. If a resource server caches a validation result, include that cache duration in the containment discussion.
Also distinguish family revocation, grant revocation, application-session deletion, and account-wide logout. They may be connected operationally, but they are not automatically the same operation. A useful answer states which credentials remain usable after each event and how the system verifies the result.
Test invariants with a tiny deterministic model
This original Python model represents serialized decisions for one family. It demonstrates expected transitions; it does not implement real concurrent storage, token authentication, expiration, or OAuth error responses.
class Family:
def __init__(self):
self.current = 0
self.consumed = set()
self.revoked = False
def refresh(self, generation):
if self.revoked:
return "rejected"
if generation in self.consumed:
self.revoked = True
return "reuse"
if generation != self.current:
return "rejected"
self.consumed.add(generation)
self.current += 1
return self.current
f = Family()
assert f.refresh(0) == 1
assert f.refresh(0) == "reuse"
assert f.refresh(1) == "rejected"
assert f.current == 1
The last assertion matters: rejected requests must not advance the lineage. Add a separate case where an unknown synthetic generation is rejected without revocation. Then model refresh versus revocation in both serialized orders. If revocation wins first, refresh fails; if refresh commits first, the new successor must become unusable after revocation.
These tests establish the model's policy, not a database's isolation behavior. An integration test should deliberately synchronize two requests after their initial read, then release them together. Check persisted state and returned results, not just HTTP status codes. For the strict policy, no schedule should leave two usable successors or resurrect a revoked family.
What should operations record without storing credentials?
Record coarse outcomes such as success, expired, unknown, reused, and revoked, with non-secret request correlation and an access-controlled family reference. Track refresh contention, reauthorization rate, and invalid-grant spikes. A sudden increase after a client release may indicate broken coordination rather than a sudden increase in theft.
Never place raw refresh tokens, access tokens, or full token responses in ordinary logs. Redaction must cover proxy logs, exception traces, analytics, and support attachments. A debugging field becomes another credential store if it contains the bearer secret.
Retention needs a security purpose too. Removing consumed-token evidence too early can turn recognizable reuse into an unknown-token event. Retaining every record forever creates unnecessary storage and privacy costs. Define retention against the family lifetime and the reuse policy, then test cleanup alongside expiry and revocation.