Refresh token rotation: best practices
Why refresh token rotation matters for long-lived social platform connections, and the failure modes it prevents.
Refresh token rotation means every refresh call returns a new refresh token and invalidates the old one, so a stolen copy has a shrinking window of usefulness. Best practices: persist the newest token after every refresh, serialize refreshes per connection, persist-then-use rather than use-then-persist, handle both rotating and non-rotating providers, and treat a rejected refresh as terminal.
A static refresh token that lives unchanged for the life of a connection is a single credential that works forever, for anyone who obtains a copy of it. Refresh token rotation replaces that with a chain: every time a refresh token is used, the provider issues a new one and invalidates the old, so a stolen copy has a shrinking window of usefulness instead of an unlimited one. For a social publishing integration, where a connection can legitimately live for years, that difference compounds.
PostGate handles OAuth, publishing and delivery evidence for every network through one API. Start free or read the docs.
What does rotation actually mean?
In a rotating model, a refresh call does not just mint a new access token — it also returns a new refresh token, and the old refresh token stops working the moment the new one is issued. Your system has to store the newest refresh token after every refresh, not assume the one it started with is still valid. This is a meaningful behavior difference from a non-rotating provider, and treating every provider as if it worked the same way is the most common cause of a refresh that mysteriously stops working weeks after launch.
Why does it matter for long-lived platform connections?
A social publishing connection is not a short browser session — it can sit untouched for months between publishes, which is exactly the kind of long-lived credential that benefits most from rotation. If a refresh token is exfiltrated (from a compromised backup, a misconfigured log, or a database breach) and it never rotates, the exposure window is however long it takes someone to notice — potentially indefinite. If it rotates, a stolen token becomes useless the next time the legitimate system performs its own routine refresh, which for an active integration can be a matter of hours or days rather than months.
What failure mode is rotation built to catch?
The real security value of rotation is not the rotation itself — it is what becomes detectable because of it. If an old, already-rotated refresh token is ever presented again, that is a strong signal something is wrong: either a stolen copy is being used after the legitimate system already rotated past it, or a race condition caused two refreshes to fire concurrently. A provider that supports rotation can treat that reuse as a compromise signal and invalidate the entire token family, forcing re-authorization — which is a strictly better outcome than a static token quietly working for both the legitimate system and an attacker indefinitely.
What pitfalls make rotation harder than it sounds?
- Concurrent refresh races. If two processes both attempt to refresh the same connection at nearly the same moment, only one refresh token can win — the other request's refresh token is now stale, and if it retries with the token it still has in memory, that looks identical to a genuine reuse attack. Coordinating refresh through a single writer (a lock, a queue, or an idempotent refresh endpoint) avoids self-inflicted lockouts.
- Persisting the new token before acting on it. If a system requests a refresh, receives a new rotated token, but crashes or fails to persist it before the next attempt, it is left holding a refresh token the provider has already invalidated — with no way back except a full re-authorization. Persist-then-use, not use-then-persist.
- Assuming every provider rotates. Some do, some don't, and some changed their behavior over time. Code that hardcodes an assumption either way breaks the moment it meets a provider on the other model — handle both: store whatever refresh token comes back from a response, and keep using the original if none is returned.
How does this connect to a real, observable signal?
Whether or not a given social platform rotates refresh tokens under the hood, the customer-facing consequence of a broken or rejected refresh looks the same from the outside: the provider refuses the attempt. PostGate surfaces that as profile.reauth_required, carrying a reason of refresh_failed, revoked, or publish_auth_error — the same event a stolen-and-rotated-past token, a manually revoked connection, or a provider-side policy change would all trigger. Whatever the underlying cause on the provider's side, the integration's job is identical: stop retrying the refresh, and route the customer to reconnect.
A short checklist
- Store whatever refresh token a provider returns after every refresh — never assume the original is still valid.
- Serialize refresh attempts per connection to avoid self-inflicted reuse-detection lockouts.
- Persist a newly issued refresh token before discarding the one it replaced.
- Treat a rejected refresh as a terminal state that needs the customer, not a case for indefinite retry.
- Never log a refresh token, rotated or not — the same rule that applies to the access token it protects.
Frequently asked questions
Why did my OAuth refresh token suddenly stop working weeks after launch?
The most common cause is assuming a rotating provider works like a non-rotating one. In a rotating model, each refresh call returns a new refresh token and the old one stops working the moment the new one is issued. If your system keeps using the refresh token it started with instead of storing the newest one after every refresh, the refresh eventually fails.
What happens if an already-rotated refresh token gets used again?
A provider that supports rotation can treat that reuse as a compromise signal — either a stolen copy is being used after the legitimate system rotated past it, or a race condition fired two refreshes concurrently — and invalidate the entire token family, forcing re-authorization. That detection is the real security value of rotation, not the rotation itself.
How do I prevent concurrent refresh races from locking out my own integration?
Coordinate refresh through a single writer — a lock, a queue, or an idempotent refresh endpoint. If two processes refresh the same connection at nearly the same moment, only one new token wins; the loser retrying with its stale in-memory token looks identical to a genuine reuse attack, and can trigger a self-inflicted lockout of the whole token family.
Should I keep retrying a failed refresh token request?
No — treat a rejected refresh as a terminal state that needs the customer, not a case for indefinite retry. PostGate surfaces it as a profile.reauth_required webhook carrying a reason of refresh_failed, revoked, or publish_auth_error. Whatever the underlying cause on the provider's side, the integration's job is the same: stop retrying and route the customer to reconnect.
