Why does a client that caches and replays one pushed-authorization request_uri get its authorization requests rejected?
answer
- a handle to one prepared request
- short expires_in, then it resolves to nothing
- per-attempt values inside it
- client must use once, server should
- refresh tolerated, replay not a contract
basics
~20 sBecause the reference is short-lived and meant for one attempt. RFC 9126 expects a brief expires_in, requires the client to use a value once, and the authorization server is expected to treat it as one-time use, tolerating at most a browser refresh.
solid answer
~50 sA `request_uri` is a handle to one stored authorization request, not a durable client identifier. Two rules kill the cached copy. First, lifetime: the `expires_in` returned with it is expected to be short — the specification's own example range of 5 to 600 seconds is an example rather than a requirement — and once past it the reference resolves to nothing. Second, single use: the pushed parameters contain values unique to one attempt, such as the CSRF `state` and the proof-key challenge, so the client is required to use a value only once, and the authorization server is expected to treat it as one-time use while being permitted to tolerate the duplicate a browser refresh produces. A server also checks the reference was issued to the `client_id` presenting it. Even where a replay survives the authorization endpoint, the stale per-attempt values break the code exchange that follows.
code
pseudocode · 12 lineson authorization_request(client_id, request_uri):
entry = lookup(request_uri)
if entry is absent:
reject
if entry.client_id is not client_id:
reject
if now is after entry.expires_at:
reject
if entry.consumed and not tolerating_user_agent_reload:
reject
mark entry consumed
continue with entry.parametersgo deeper
Recall that the reference stands for one prepared request and expires quickly. Push a new one for each sign-in rather than storing the value you got the first time.
Explain the two independent rules — a short expires_in and one-time use — and why the values pushed with the request, such as state and the code challenge, belong to a single attempt.
Diagnose the intermittent version: a refresh a server tolerates hides the defect, the flow runs on stale stored parameters, and the failure surfaces one leg later at the code exchange.
The lesson generalises: when a specification puts a requirement on one party and a recommendation on the other, clients that calibrate on observed server behaviour ship a defect that appears only after a server changes.
## What the reference actually is When a client pushes an authorization request, the authorization server stores that **parameter set** and returns a handle to it. The handle identifies one prepared request. It is not a client identifier, not a session, and not a capability the client may keep. Everything about how it is specified follows from that: high entropy so it cannot be guessed, a short lifetime, and a binding to the client that pushed it. ## The two rules that defeat a cache **Lifetime.** The response carries `expires_in` beside the `request_uri`, and the lifetime the specification expects is short — its own example range of 5 to 600 seconds is illustrative, not a requirement, and a deployment sets its own. The window only has to cover the gap between the client pushing and the browser arriving. After it, the reference resolves to nothing and the authorization request is rejected. **Single use.** Some of the pushed values belong to exactly one attempt: the `state` the client will match on the way back, and the challenge bound to the code exchange. Replaying the reference replays those values. The specification therefore splits the obligation: - The **client** is required to use a `request_uri` value only once. - The **authorization server** is expected to treat it as one-time use, but is **permitted** to allow a duplicate — because a resource owner reloading or refreshing the page produces a second, entirely innocent presentation of the same reference. That is a genuine SHOULD/MAY split and it is worth stating precisely, because it explains the symptom: the cached reference sometimes works, which is exactly what makes the bug survive testing. ## The checks a server runs on a presented reference 1. Does the reference resolve to a stored request at all? 2. Was it issued to the `client_id` presenting it? A reference issued to one client is not usable by another. 3. Has its lifetime passed? 4. Has it already been consumed, and is this a case the server chooses to tolerate? Only then are the stored parameters used to run the authorization flow. ## Why replay fails even when the endpoint tolerates it Suppose the server allows the duplicate. The flow proceeds with the **stored** parameters — including the original `state` and the original challenge. Two things then go wrong downstream: - The client's own `state` check compares the returned value against whatever it generated for the current attempt, and a cached reference returns yesterday's. - The code exchange presents a verifier that must correspond to the challenge inside the stored request. A client that generated a fresh secret for this attempt cannot satisfy a challenge pushed for an earlier one. So the failure moves rather than disappearing, which is the classic shape of this bug report: "authorization works, the token call fails". ## What the correct pattern looks like | Mistake | Symptom | Correct behaviour | |---|---|---| | Caching one reference at startup | Works briefly, then every login fails | Push once per authorization attempt | | Sharing one reference across users | Wrong `state` returned, or cross-user confusion | One reference per attempt, per user agent | | Treating a tolerated refresh as permission to replay | Intermittent success that hides the defect | Treat tolerance as a courtesy to the browser, never a contract | | Retrying the redirect after a long delay | Rejection with no obvious cause | Re-push and redirect with a fresh reference | ## The diagnostic When authorization requests fail intermittently and the failures correlate with elapsed time or with repeated sign-ins from the same client instance, look at where the reference is produced. A push per attempt costs one back-channel round trip and removes the entire class. If the round trip is genuinely the problem, the answer is to push later — as close to the redirect as possible — not to reuse what was pushed.
- Why is a server allowed to accept the same request_uri twice at all?Because a resource owner reloading the authorization page presents it again, and failing that reload would break an ordinary browser interaction for no security gain. The allowance is scoped to that case; it is a recommendation to treat the reference as one-time use, relaxed for the refresh, not an invitation to replay.
- Why does a replayed reference often pass the authorization endpoint and fail at the token endpoint instead?Because the stored request still contains the per-attempt values pushed with it. The flow runs on the original `state` and the original challenge, so the mismatch surfaces when the client checks the returned state or presents a verifier generated for a different attempt.
- Is one reference usable by a second client that happens to learn it?No. The server checks that the reference was issued to the `client_id` presenting it, so a leaked reference is not a request another client can drive. That check is also why `client_id` accompanies `request_uri` on the authorization request.
saying these in an interview costs you the question
- Treats a request_uri as a durable per-client identifier
- Says every server must reject a reused request_uri outright
- Assumes the reference lives as long as the user's session
- Concludes replay is supported because a refresh worked
- Thinks a leaked reference lets any client drive that request
- Expects the failure to appear at the authorization endpoint every time