Skip to content

fix(auth): persist refresh_token so OAuth survives across restarts#35

Open
BrentBaccala wants to merge 1 commit into
ArtyMcLabin:mainfrom
BrentBaccala:fix-oauth-refresh-token-persistence
Open

fix(auth): persist refresh_token so OAuth survives across restarts#35
BrentBaccala wants to merge 1 commit into
ArtyMcLabin:mainfrom
BrentBaccala:fix-oauth-refresh-token-persistence

Conversation

@BrentBaccala

Copy link
Copy Markdown

Problem

The server loses authentication roughly every hour and requires a manual re-auth. The root cause is that it never reliably retains a refresh_token, so once the ~1-hour access_token expires there is nothing to refresh from. This matches upstream GongRzhe/Gmail-MCP-Server Issue #50.

There are two independent contributing bugs:

  1. generateAuthUrl() omits prompt: 'consent'. With only access_type: 'offline', Google returns a refresh_token solely on the first consent for a given (user, OAuth client) pair. Every subsequent auth flow is treated as silent re-authentication and returns an access_token only — and the auth handler then overwrites credentials.json with that refresh-token-less response, discarding any prior refresh_token.

  2. No on('tokens') listener. google-auth-library rotates the access_token silently in memory, but nothing persists the rotated token to disk. On the next process start the server re-reads a now-stale token.

Fix

  1. Add prompt: 'consent' to generateAuthUrl() so Google always re-prompts and therefore always returns a refresh_token.
  2. Add an oauth2Client.on('tokens', …) listener that merges rotated tokens back into credentials.json, preserving the refresh_token and the existing { tokens, scopes } credential wrapper, written with mode 0600.

Together these make the credential durable across both access_token expiry and process restarts.

Testing

  • npm run build (tsc) compiles clean on Node 25.
  • After re-auth, credentials.json is written with a populated tokens.refresh_token, and the MCP server connects and lists labels end-to-end.
  • The branch is rebased on the current main tip; the change is isolated to src/index.ts (+20 lines) and does not touch the recently-added draft-lifecycle / phishing tools.

This PR was researched and written by an AI assistant (Claude) on behalf of Brent Baccala (cosine@freesoft.org), based on source analysis of src/index.ts and a reproduced ~hourly auth-drop on this fork. The fix mirrors the root-cause analysis in upstream Issue GongRzhe#50.

The server loses authentication roughly every hour because it never
reliably retains a refresh_token. Two independent causes, both matching
upstream GongRzhe/Gmail-MCP-Server Issue GongRzhe#50:

1. generateAuthUrl() lacked `prompt: 'consent'`. With only
   `access_type: 'offline'`, Google returns a refresh_token solely on
   the first consent for a given (user, OAuth client) pair; subsequent
   auth flows return an access_token only. Adding `prompt: 'consent'`
   forces a re-prompt, so a refresh_token is always returned.

2. There was no `on('tokens')` listener, so google-auth-library's
   silent access_token rotation updated only the in-memory client —
   on the next process start the server re-read a now-stale token from
   disk. The listener merges rotated tokens back into credentials.json,
   preserving the refresh_token (and the existing `{ tokens, scopes }`
   credential wrapper), and writes the file with mode 0600.

Together these make the credential durable across both access_token
expiry and process restarts.

Ref: GongRzhe#50

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant