Back to sensacat

Home  /  Troubleshooting

· SensaCat Team

OAuth Token Revoked Without Notice

invalid_grant is final. No endpoint revives a dead refresh token, so the fix is always re-authorization. The useful question is why it died.

If you are seeing invalid_grant with a description saying the token has been expired or revoked, the refresh token is gone permanently. There is no endpoint that restores it and no retry that helps. The user has to run the authorization flow again.

That is the fix. The rest of this page is about why it happened, because if you do not find the cause it will happen again on the same schedule.

The Google causes, in order of likelihood

The 7-day rule catches almost everyone once. Google documents that a project with an OAuth consent screen set to External user type and a publishing status of Testing is issued refresh tokens that expire in 7 days.

The exception is narrow and worth knowing: it does not apply if the only scopes requested are a subset of name, email address and profile, through userinfo.email, userinfo.profile, openid or their OpenID Connect equivalents. Any other scope and the 7-day clock runs.

It is a fixed clock, not an inactivity timer. A script calling the API every hour still loses the token on day seven, which is why it looks so much like a random failure.

Cause Signal Fix
External + Testing consent screen Dies every 7 days, like clockwork Publish to production, or set Internal
6 months without a refresh call Long-idle integrations Refresh on a schedule, monthly is enough
Over 100 live tokens per user per client Oldest silently invalidated Reuse stored tokens; stop re-authorizing
Gmail scope + user changed password One user, sudden Prompt that user to reconnect
User removed your app One user, sudden Prompt that user to reconnect
Workspace admin restricted scopes admin_policy_enforced Admin must allowlist the client ID
Token from the OAuth Playground Dies after 24 hours Generate through your own client

The 6-month rule counts refresh calls, not API calls. Using an access token you already hold does not keep the refresh token alive, which is why an integration that is clearly in use can still expire.

The 100-token cap invalidates the oldest token silently when exceeded. Applications that re-run the consent flow on every login rather than storing what they were given hit this and blame something else.

Diagnosing which one you hit

The error response is the same for all of them, so the distinction comes from the pattern rather than the message.

Every connection dying at once points to a client-level setting: the consent screen, a rotated client secret, or an admin policy. One user dying alone points to that user revoking, changing a password, or being removed from the Workspace.

A clean 7-day cadence is unmistakable once you look for it. Check the timestamps of your last few failures against each other before investigating anything else.

Handle it as a state, not an exception

A dead refresh token is a normal condition in any application holding third-party credentials at scale, not an unexpected error. Treat it accordingly.

Store a status on each connection. On invalid_grant, mark it disconnected, stop retrying immediately rather than hammering the token endpoint, and surface a reconnect prompt to that specific user. Alert on the first occurrence, not the thousandth.

The part you can get ahead of

Revocation by a user is unpredictable. Expiry is not. A refresh token approaching its 6-month idle limit, a client secret with a known expiry date, and a consent screen still sitting in Testing are all knowable in advance.

Those belong in a credential inventory with dates and owners rather than in your error handler. SensaCat tracks that kind of expiry; the broader treatment is in credential expiry monitoring.