fix: Google Workspace MCP connectors — durable refresh tokens + reliable OAuth popup#185
Open
hordruma wants to merge 2 commits into
Open
fix: Google Workspace MCP connectors — durable refresh tokens + reliable OAuth popup#185hordruma wants to merge 2 commits into
hordruma wants to merge 2 commits into
Conversation
Google only issues a refresh token when the authorization request opts into offline access (access_type=offline) and is forced to re-prompt for consent (prompt=consent). The MCP SDK builds a spec-compliant authorization URL and exposes no hook for these proprietary parameters, and Google does not support the OIDC offline_access scope the SDK handles on its own. As a result Google connectors authorized once and then broke as soon as the short-lived access token expired, with no refresh token for the SDK to renew with. - Add providerAuthorizationParams() as the policy for non-standard auth params and apply it generically in DbMcpOAuthProvider.redirectToAuthorization (the SDK's only authorization-URL seam); the redirect path carries no Google specifics, so future providers are a one-line policy change. - Tighten Google host detection to `=== googleapis.com || .googleapis.com`, share it via isGoogleOAuthHost, and align the frontend isGoogleMcpConnector so a host like notgoogleapis.com is no longer treated as Google. - Leave scope resolution to the SDK (documented), which already falls back to the server's advertised scopes; passing auth-server scopes would have requested the wrong ones for Google. - Add backend tests (node:test via tsx) for the policy, host matching, and the provider wiring; add an `npm test` script and exclude test files from build. Co-Authored-By: claude-flow <ruv@ruv.net>
… signals Google's OAuth consent page is served with Cross-Origin-Opener-Policy: same-origin, which severs window.opener and makes popup.closed unreadable from the opener window. That broke the connector OAuth popup two ways: the callback's window.opener.postMessage never reached the opener, and a COOP-blocked popup.closed read reports a false "closed" — surfacing a spurious "OAuth authorization window was closed" error even though the backend had already exchanged the code and stored the tokens. Poll the backend for the connector's oauthConnected flag as the source of truth, keep postMessage as a fast path for providers that don't sever the opener, and drop the unreliable popup.closed rejection. Co-Authored-By: claude-flow <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01HSWSVK1PQozcrmZGgBhFXS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Google Workspace MCP connectors (Drive / Gmail / Calendar on
*.googleapis.com) were unusable past the first hour: they authorized once and then broke when the short-lived access token expired, and the OAuth connect popup could report a false failure. This PR makes the connection durable and the connect flow reliable.Problems
No refresh token was ever obtained. Google only issues a refresh token when the authorization request opts into offline access (
access_type=offline) and is forced to re-prompt for consent (prompt=consent), and it does not implement the OIDCoffline_accessscope the MCP SDK would otherwise use. The SDK builds a spec-compliant authorization URL and exposes no hook for these proprietary params, so a Google connector got only an access token and broke as soon as it expired (theencrypted_refresh_tokencolumn stayed null, so there was nothing to renew with).OAuth popup completion was mis-detected under COOP. Google's consent page is served with
Cross-Origin-Opener-Policy: same-origin, which severswindow.openerand makespopup.closedunreadable from the opener. As a result the callback'swindow.opener.postMessagenever reached the app, and a COOP-blockedpopup.closedread reports a false "closed" — surfacing a spurious "OAuth authorization window was closed" error even though the backend had already exchanged the code and stored the tokens.Changes
Backend —
backend/src/lib/mcp/oauth.ts(commitf9b5bf1)providerAuthorizationParams()policy returning{ access_type: "offline", prompt: "consent" }for Google hosts, applied generically inredirectToAuthorization()(the SDK's only authorization-URL seam) — the redirect path carries no Google specifics, so future providers are a one-line policy change.isGoogleOAuthHost()with tightened matching (=== googleapis.comor.googleapis.com), so look-alikes likenotgoogleapis.comare no longer treated as Google.isGoogleMcpConnector()host check.oauth.test.ts), annpm testscript, and exclude test files from the build.Frontend —
connectors/page.tsx(commitbfb5c11)oauthConnectedflag (the source of truth) instead of relying on COOP-brokenpostMessage/popup.closed. KeeppostMessageas a fast path for providers that don't sever the opener, and drop the false-positivepopup.closedrejection.Testing
access_type=offline+prompt=consent, the correct Drive/Gmail scopes, PKCE (S256), and the right redirect URI.encrypted_refresh_tokennow populated) — confirmed via both the API and the UI.tools/listagainstdrivemcp.googleapis.comreturns the Drive tools.Notes
create_draft) remain intentionally gated behind the existing "confirmation required" flag; this PR does not change that behavior.🤖 Generated with claude-flow
https://claude.ai/code/session_01HSWSVK1PQozcrmZGgBhFXS