We’re migrating a public app to expiring offline access tokens and building the refresh path around the documented retry behavior. Before we depend on it, we want to know whether it is stable.
For offline tokens, Shopify returns the same refreshed token response for up to 1 hour after the original refresh, so a refresh interrupted by a transient outage can be safely retried once connectivity is restored.
We confirmed it against a dev store. Presenting an already-redeemed refresh token inside the window returns the same access_token and the same refresh_token, with a distinct x-request-id per call and cache-control: no-store / cf-cache-status: DYNAMIC, so it is genuinely re-served rather than cached. A token two generations behind returns 401 invalid_request, “This request requires an active refresh_token”. We have raw request and response captures and can share them if useful.
We also confirmed the token endpoint is not sender-constrained: the TLS handshake sends no CertificateRequest (so no mTLS per RFC 8705), and no DPoP proof is involved (RFC 9449).
That combination is what prompts the question. RFC 9700 section 4.14.2 says:
Authorization servers MUST utilize one of these methods to detect refresh token replay by malicious actors for public clients:
Sender-constrained refresh tokens: the authorization server cryptographically binds the refresh token to a certain client instance, e.g., by utilizing [RFC8705] or [RFC9449].
Refresh token rotation: the authorization server issues a new refresh token with every access token refresh response. The previous refresh token is invalidated, but information about the relationship is retained by the authorization server. If a refresh token is compromised and subsequently used by both the attacker and the legitimate client, one of them will present an invalidated refresh token, which will inform the authorization server of the breach. The authorization server cannot determine which party submitted the invalid refresh token, but it will revoke the active refresh token. This stops the attack at the cost of forcing the legitimate client to obtain a fresh authorization grant.
Our questions:
-
Is the 1-hour same-response window a contract we can build on for the foreseeable future, or is it subject to change?
-
Is there work planned to move toward 4.14.2, either sender-constraining, or rotation where a reused refresh token revokes the active one?
The reason we ask is that the two answers lead to opposite designs, and the second one is expensive for us to get wrong. Under the current behavior, a retry after an interrupted refresh recovers quietly. Under 4.14.2 rotation, that same retry would revoke the store’s active token and force a fresh authorization grant, which for us means waiting for a merchant to reopen the app, which they may not ever actually do. It technically goes beyond just legit transient retries, any app with concurrent workers can accidentally use the same refresh tokens.