SkillAgentSearch skills...

google-oauth-nonexpiring-refresh-token

Why your Google OAuth refresh token expires every 7 days — and the publishing-status fix that works WITHOUT completing app verification (single-user Gmail/Calendar/Drive automation, restricted scopes).

Install / Use

claude mcp add MankhongGarden -- npx -y github:MankhongGarden/google-oauth-nonexpiring-refresh-token

If the server publishes to npm under a different name, use that package instead — check the repo README.

About this skill
🔌

MCP Server

Model Context Protocol server

Quality Score

75/100

Category

Automation

Supported Platforms

Claude Code
Claude Desktop

Our assessment of google-oauth-nonexpiring-refresh-token

google-oauth-nonexpiring-refresh-token scores 75/100 on our quality scale, 2626th of 2,901 Automation skills we index.

Its MCP Server is 7.9 KB long, well organised into 11 sections with 2 code examples: a thorough specification that gives an agent plenty to work with.

It has 3 GitHub stars, so there is little community track record yet; judge it on its content.

Substance
29/30
Structure
18/20
Description
15/15
Adoption
3/20
Freshness
11/15

Maintenance, license and trust

  • The repository was last updated about 5 months ago. That is recent enough to be usable, but agent tooling moves fast, so check the instructions against your agent's current version.
  • It is released under the MIT license, a permissive license that allows use, modification and commercial use with attribution.
  • Its trust signals score 90/100, with 1 caution from licensing, adoption, age or documentation. These come from repository metadata, not a code audit — read the skill file before letting an agent act on it.

google-oauth-nonexpiring-refresh-token compared with similar skills

All 4 of these similar skills score higher than google-oauth-nonexpiring-refresh-token; compare them before choosing.

SkillScoreStarsUpdatedFormat
google-oauth-nonexpiring-refresh-token (this skill)by MankhongGarden7535mo agoMCP Server
Agent-Reachby Panniantong10098.8k2d agoCLAUDE.md
headroomby headroomlabs-ai10075.6ktodayCLAUDE.md
CowAgentby zhayujie10047.4k1d agoCLAUDE.md
Scraplingby D4Vinci10088.4ktodayMCP Server

Frequently asked questions

How do I install google-oauth-nonexpiring-refresh-token?
Run claude mcp add MankhongGarden -- npx -y github:MankhongGarden/google-oauth-nonexpiring-refresh-token. The install tabs above show the steps for each supported agent.
Which AI agents does google-oauth-nonexpiring-refresh-token work with?
It is written for Claude Code and Claude Desktop, as a MCP Server file. Other agents that read the same format can often use it too.
Is google-oauth-nonexpiring-refresh-token safe to use?
It is MIT-licensed and scores 90/100 on trust signals. Skills are instructions an agent will follow, so read the file before installing it and do not approve commands you do not understand.
Is google-oauth-nonexpiring-refresh-token still maintained?
The repository was last updated about 5 months ago. That is recent enough to be usable, but agent tooling moves fast, so check the instructions against your agent's current version.

Google OAuth refresh tokens that don't expire every 7 days — without completing verification

TL;DR — If your single-user Google integration (a Gmail/Calendar/Drive script, a personal MCP server, a home automation, a one-off internal tool) keeps dying with invalid_grant: Token has been expired or revoked roughly once a week, the cause is almost never your code. It's your OAuth app's publishing status. Flip it from Testing to In production and the refresh token stops expiring — even while the app stays "unverified." You do not have to pass Google's verification review to get a long-lived token.

This is the part most guides get wrong: they conflate "In production" (a publishing-status toggle you control instantly) with "verified" (a review process that can take weeks). They're two different axes. Token longevity is governed by the first, not the second.


The symptom

You built something that talks to a Google API with an OAuth refresh token. It works. Then ~7 days later, every call fails:

400 invalid_grant
{ "error": "invalid_grant", "error_description": "Token has been expired or revoked." }

You re-run the consent flow, get a fresh token, and exactly a week later it breaks again. You start writing a cron job to re-auth weekly. Don't.


Why it happens

Google issues short-lived (7-day) refresh tokens when your OAuth app's publishing status is Testing — unless the only scopes you request are a subset of the basic name / email / profile set. The moment you request anything beyond that — a sensitive scope (e.g. calendar) or a restricted scope (e.g. gmail.modify, gmail.readonly, Drive full access) — a Testing app's refresh tokens are capped at 7 days.

This is documented behavior. The trap is the next inference everyone makes:

"So to get a non-expiring token for a restricted scope, I have to complete verification."

No. You have to move the app to In production publishing status. Verification is a separate thing.


The two axes people conflate

| Axis | What it is | What it controls | How long it takes | |---|---|---|---| | Publishing status (Testing ↔ In production) | A toggle on your OAuth consent screen you flip yourself | Refresh-token lifetime (7-day vs non-expiring), test-user allowlist requirement | Instant | | Verification (unverified → submitted → verified) | Google's manual review of your app + scopes (privacy policy, domain ownership, sometimes a security assessment for restricted scopes) | The "Google hasn't verified this app" warning screen, the user cap, eligibility to serve restricted scopes to strangers | Days to weeks (restricted scopes can require a paid third-party security assessment) |

The key realization: an app can be In production AND unverified at the same time. That combination is valid, and per Google's documented behavior it issues non-expiring refresh tokens for sensitive and restricted scopes.

What you trade for skipping verification is not token longevity. It's:

  • Users see an "Google hasn't verified this app" interstitial (you click Advanced → Go to {app} (unsafe) to proceed).
  • A user cap applies (on the order of 100 users for unverified apps).
  • You can only let in users you control (yourself + anyone you add).

For a single-user or self-hosted integration — which is exactly the case for personal automations and most MCP servers — none of those three costs matter. You are the only user, you don't care about your own warning screen, and you're nowhere near the cap.


The fix (≈2 minutes)

  1. Open Google Cloud Console → APIs & Services → OAuth consent screen (newer console: Google Auth Platform → Audience).
  2. Find Publishing status. If it says Testing, click "Publish app" / "Push to production".
  3. Confirm. Status now reads In production. You can ignore the verification prompt — leaving it unverified is fine for a self/single-user app.
  4. Re-run your OAuth consent flow once to mint a fresh token under the new status. (Tokens minted while the app was in Testing keep their 7-day clock — the status change does not retroactively extend an already-issued token.)
  5. Inspect the token response. You're looking for a refresh token with no expiry — see the next section for the exact field.

Confirming it on your own app

After publishing to production and re-running consent, inspect the token-exchange response. The single field that tells you everything is refresh_token_expires_in:

// Testing status, restricted scope — short-lived:
{ "access_token": "...", "refresh_token": "...", "expires_in": 3599,
  "refresh_token_expires_in": 604800 }   // 7 days, in seconds

// In production status (even unverified) — non-expiring:
{ "access_token": "...", "refresh_token": "...", "expires_in": 3599 }
//  ^ no refresh_token_expires_in field  →  token does not expire on a timer
  • refresh_token_expires_in: 604800 → still on the 7-day clock (you're likely still in Testing, or re-using a token minted before the switch).
  • refresh_token_expires_in: 0, or the field absent entirely → non-expiring.

That field flips based on publishing status, independent of verification state, and independent of which sensitive/restricted scope you requested. expires_in: 3599 refers to the short-lived access token — that one always rotates hourly and is not the thing you're tracking here.


Things that will still revoke a non-expiring token

"Non-expiring" means no time-based expiry. It does not mean indestructible. A long-lived refresh token still dies if:

  • The user revokes access in their Google Account security settings.
  • Six months of inactivity pass with the token unused.
  • You change the requested scopes (the old grant no longer matches).
  • You exceed the per-user/per-client live-token limit (Google silently revokes the oldest when you mint too many; ~50 live tokens per user/client is the classic ceiling — stop re-authing in a loop).
  • A password reset / security event invalidates outstanding grants.
  • You revert the app back to Testing.

If you start seeing invalid_grant again after this fix, it's one of the above — not the 7-day timer.


When this is NOT the right answer

This recipe is for apps you control the user base of — single-user, self-hosted, internal, or a small allowlist. If you are shipping a public app where strangers sign in:

  • The unverified warning screen and user cap do matter → you need to actually complete verification.
  • Restricted scopes for a public audience may require a CASA security assessment (third-party, paid).
  • Don't ship the "Advanced → unsafe" click-through to real users.

In that case, publishing-status-to-production is still step one (it fixes token longevity), but verification becomes a real requirement for everything else.


Why this writeup exists

Search "Google OAuth refresh token expires in 7 days" and you'll find a lot of correct-but-incomplete answers that stop at "publish your app / it's because you're in Testing." The piece that trips people up is the very next assumption — that production requires verification, so a hobbyist with a restricted scope feels stuck behind a weeks-long review for a token they could have had in two minutes. The two axes are independent. That's the whole insight.


Found this useful?

If this saved you from writing a weekly re-auth cron, a ⭐ helps others find it. I write up the non-obvious gotchas from building small Google / Next.js / automation projects — issues and corrections welcome. If you'd like help wiring up production-grade single-user OAuth (MCP servers, headless token refresh, multi-account separation), see GitHub Sponsors.

License

MIT — see LICENSE. Do whatever you want with the recipe.

Related Skills

View on GitHub
GitHub Stars3
CategoryAutomation
Updated4mo ago
Forks0

Trust signals

90/100

From repository metadata: license, adoption, age and documentation. Not a code audit — see the Safety scan above for what the skill file itself contains.

1 low1 info
google-oauth-nonexpiring-refresh-token — MCP Server: Install & Safety Check | SkillAgent