Skip to content

Refreshed credentials not persisted to file store in OAuth 2.1 code path #588

@chiragdshah

Description

@chiragdshah

Bug

When running in streamable-http transport mode, the OAuth 2.1 code path in service_decorator.py does not persist refreshed credentials back to the LocalDirectoryCredentialStore. This causes daily re-authentication when Google rotates refresh tokens.

Root Cause

The per-request flow in service_decorator.py (line ~292) retrieves credentials via OAuth21SessionStore.get_credentials_with_validation(), which creates a fresh Credentials object from the in-memory session store. When the google-auth library auto-refreshes the access token (and Google rotates the refresh token), the updated tokens only exist on that ephemeral Credentials object.

The persistence code in google_auth.py:get_credentials() (lines ~789-792) correctly calls credential_store.store_credential() after refresh — but this code path is not used by the OAuth 2.1 service decorator.

Meanwhile, save_credentials_to_session() logs: "Could not save credentials to session store - no user email found for session" — confirming the in-memory session store also fails to update.

Impact

  • The on-disk credential file in ~/.google_workspace_mcp/credentials/ goes stale after the initial auth
  • When Google rotates the refresh token (common on Workspace accounts with refresh_token_expires_in: 604799 / 7 days), the old on-disk refresh token is revoked
  • On server restart (or when the in-memory token expires), the server loads the stale file and cannot refresh → user must re-authenticate daily

Reproduction

  1. Run workspace-mcp --transport streamable-http with a Google Workspace account
  2. Authenticate and make API calls
  3. Note the modified timestamp on ~/.google_workspace_mcp/credentials/<email>.json
  4. Wait for the access token to expire (~1 hour) and make more API calls (tokens refresh in memory)
  5. Restart the server
  6. Observe that the credential file timestamp hasn't changed since step 2
  7. If the refresh token was rotated during step 4, authentication fails

Suggested Fix

In service_decorator.py, after build(service_name, version, credentials=credentials) returns (or via a post-request hook), check if credentials were refreshed and persist them:

service = build(service_name, version, credentials=credentials)

# Persist refreshed credentials to file store
if user_google_email and credentials.token:
    from auth.credential_store import get_credential_store
    credential_store = get_credential_store()
    credential_store.store_credential(user_google_email, credentials)

Note: since google-auth refreshes lazily (during the first API call, not during build()), a more robust approach would be to wrap Credentials.refresh() or add a token-refresh callback that triggers persistence.

Environment

  • workspace-mcp v1.14.3
  • Transport: streamable-http
  • Google Workspace account with refresh token rotation enabled
  • macOS, running via LaunchAgent

Workaround

Monkey-patching google.oauth2.credentials.Credentials.refresh() to write updated tokens to the credential store directory after each refresh.

Metadata

Metadata

Assignees

Labels

can't reproduceUnable to replicate, more information is neededquestionFurther information is requested

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions