Why my Google OAuth tokens died every 7 days

I run a small agent that pulls YouTube channel statistics every Monday. It worked for a week, then failed with a 400 error. Then it happened again.
The symptom
The weekly job calls the YouTube Data API and the YouTube Analytics API with a saved refresh token. One Monday the token refresh returned HTTP 400 with invalid_grant: the refresh token was "expired or revoked". I re-authorized, the report ran, and seven days later it failed the same way.
Nothing in the code had changed, and nobody had revoked access by hand.
The cause
The Google Cloud project's OAuth consent screen was set to External with publishing status Testing. Google documents that refresh tokens issued to such an app expire after 7 days. The token response even says so: refresh_token_expires_in was 604799 seconds, one second short of a week.
So the token was doing exactly what the setting told it to do.
The options
- Switch the app to Production. Refresh tokens then no longer expire after 7 days. The YouTube scopes are sensitive, so an unverified app shows a warning screen during consent and is limited in how many users it can have. For a personal tool with one user, that's usually acceptable. Wider use means going through Google's verification.
- Stay in Testing and re-authorize weekly. Fine for a short experiment, tiring for anything that runs on a schedule.
What I changed in the meantime
- The report script no longer fails quietly. When the refresh fails it writes "re-authorization needed" into the report and exits with an error code, so a dead token shows up the same day.
- Re-authorizing is one command. A small helper prints the consent URL, catches the
localhostredirect and writes the new token file. - A status check at the start of each session tests every token and lists the dead ones before any work starts.
One more trap
Not every OAuth client type can request every scope. The device flow ("TV and limited input" clients) refused the YouTube Analytics scope, no matter what was on the consent screen. A Desktop app client with the loopback redirect worked. If a scope is rejected while the consent screen looks right, check the client type.
Get new articles by email
Only when a new article is out. No ads. Details →