Demo, all content is generated
Question

Google OAuth refresh_token is null, and the one I had stopped working after a week

Solved · 3402 views · asked by lily_chen · edited

My app reads users' Google Calendar in a nightly job. Two problems:

  1. For most users refresh_token comes back null from the token exchange. Only the first time I logged in myself I got one.
  2. The one I did get now fails with:
{ "error": "invalid_grant", "error_description": "Token has been expired or revoked." }

It worked for exactly 7 days.

What I’ve tried

Cursor added access_type: 'offline' to the auth URL, still null for users who already connected. Checked the client secret is correct.

Comment
Is the consent screen in Testing or In production? amir_h · edited

3 answers

Marked as helpful by the asker
amir_h · edited

Both are standard Google behaviour.

1. Null refresh token. Google only returns a refresh token on the first consent. Users who already granted access get none on later logins. Force it:

access_type=offline&prompt=consent

prompt=consent shows the consent screen again and returns a fresh refresh token. Store it the moment you get it; if you overwrite it with null on a later login, you lose it (check your code for that, it's common).

2. Expires after 7 days. Your OAuth consent screen is in Testing mode. In testing, refresh tokens for external users expire after 7 days. Publish the app (OAuth consent screen → Publish app). For Calendar scopes, Google will ask for verification, which takes a while, so start early.

Comment
I was literally overwriting it with null on every login. And yes, Testing mode. Two bugs, one answer, thank you lily_chen · edited
Welcome. Start the verification now, it can take weeks for Calendar scopes. amir_h · edited
The 7 days in testing mode thing got me last year too. It's in the docs but nobody finds it elif_y · edited
chidi_eze · edited

Also handle invalid_grant gracefully in the nightly job: mark that user's connection as broken and email them to reconnect. Users can revoke access at any time from their Google account, so this will keep happening even after you publish.

Comment
Added a reconnect email, good call lily_chen · edited
jonas_k · edited

Since you're storing refresh tokens now: encrypt them at rest (e.g. with a key from your env, not in the database). A leaked table of Google refresh tokens with Calendar scope is a very bad day.

Comment