Skip to content

fix(oauth): bind Google refresh to the client that issued the token - #12106

Merged
diegosouzapw merged 7 commits into
diegosouzapw:release/v3.8.51from
HouMinXi:fix/antigravity-per-connection-oauth-client
Aug 30, 2026
Merged

diegosouzapw merged 7 commits into
diegosouzapw:release/v3.8.51from
HouMinXi:fix/antigravity-per-connection-oauth-client

Conversation

@HouMinXi

Copy link
Copy Markdown
Contributor

fix(oauth): bind Google refresh to the client that issued the token

Problem

A Google refresh token only works against the OAuth client that issued it.
PROVIDERS.antigravity.clientId resolves the env override
(ANTIGRAVITY_OAUTH_CLIENT_ID) first, so the moment an operator configures a
custom web client — for the public redirect URI flow — every existing
connection's refresh starts presenting the new client, and Google answers
401 unauthorized_client for all of them.

Observed in production: switching to a custom client for
https://<public-host>/callback made four healthy antigravity/agy
connections refresh-dead within minutes (access tokens would have expired an
hour later). gemini shares the same refresh case and embeds a different
desktop client (681255809395-...), so any per-provider fallback must also be
keyed correctly.

Fix

Record which client issued the connection at authorize time
(providerSpecificData.oauthClient: "builtin" | "custom", stamped in
postExchange from the same config used for the code exchange) and make the
refresh path select credentials per connection:

  • unmarked connections — everything created before the marker existed —
    refresh against the embedded desktop client of their own provider
    (gemini and antigravity embed different clients),
  • custom-marked connections refresh against the configured client,
  • with no env override both branches resolve to the same client and behavior
    is unchanged.

Verification

  • New regression tests cover the three marker states plus the
    gemini-vs-antigravity builtin distinction (their client ids differ; the
    fallback is keyed by provider).
  • Bug-injection round trip: reverting to the global PROVIDERS[provider]
    lookup fails the unmarked/builtin cases; the per-connection selection
    passes all four.
  • Production validation on the live instance: with the custom web client
    still configured in env, the previously failing refreshes (30+ consecutive
    unauthorized_client) all succeed post-deploy; zero failures across all
    connections.

A Google refresh token only works against the OAuth client that issued
it. PROVIDERS.antigravity.clientId resolves the env override
(ANTIGRAVITY_OAUTH_CLIENT_ID) first, so the moment an operator configures
a custom web client, every existing connection's refresh starts
presenting the new client and Google answers 401 unauthorized_client for
all of them — observed in production when switching to a custom client
for the public redirect URI: four healthy antigravity/agy connections
went refresh-dead within minutes, access tokens expiring an hour later.

Record which client issued the connection at authorize time
(providerSpecificData.oauthClient: builtin|custom, stamped in
postExchange from the same config used for the code exchange) and make
the refresh path select credentials per connection: unmarked
connections — everything created before the marker existed — refresh
against the embedded desktop client, custom-marked ones against the
configured client. With no env override both branches resolve to the
same built-in client and behavior is unchanged.

Bug-injection verified: reverting to the global PROVIDERS lookup fails
the unmarked/builtin cases; the per-connection selection passes all
three.

Signed-off-by: Minxi Hou <houminxi@gmail.com>
The postExchange marker compared config against ANTIGRAVITY_CONFIG by
reference; createAntigravityOAuthProvider receives that same object when
no runtime override exists, so the comparison could never tell a custom
config apart from the default one. Compare by value against the raw
embedded client id instead, harden selectGoogleRefreshClient against
malformed PROVIDERS entries, and remove unused imports/vars the review
flagged.

Signed-off-by: Minxi Hou <houminxi@gmail.com>
gemini and antigravity embed different desktop OAuth clients
(681255809395-... vs 1071006060591-...), but the refresh fallback
returned the antigravity one for every provider in the Google case. A
pre-existing gemini connection would have started refreshing against
the antigravity client and failed with 401 unauthorized_client the
same way the incident connections did. Key the built-in client per
provider and cover the distinction with a regression test.

Signed-off-by: Minxi Hou <houminxi@gmail.com>
@HouMinXi
HouMinXi requested a review from diegosouzapw as a code owner August 30, 2026 10:00
A bare "custom" marker silently moved a connection onto whatever custom
client the env points at NOW. When the operator rotates
ANTIGRAVITY_OAUTH_CLIENT_ID to a different custom client, the old
connection's refresh token belongs to the previous client and the
refresh must not present the new one. Store the literal id
(custom:<clientId>) and only refresh with the configured client when
the ids match; a mismatch falls back to the embedded client, which
leaves the refresh failing with 401 — the honest signal that the
connection needs re-authorization.

Signed-off-by: Minxi Hou <houminxi@gmail.com>
…viders

Narrow the marker to the literal union ("builtin" | custom:<id> |
undefined) so call sites get compile-time checking, make the builtin
provider lookup explicit (unknown Google-family providers now throw
instead of silently borrowing the antigravity client), and pin the
gemini env-override behavior with a test verified against the live
module: an unmarked gemini connection keeps refreshing with the gemini
builtin even when GEMINI_OAUTH_CLIENT_ID carries a custom client.

Signed-off-by: Minxi Hou <houminxi@gmail.com>
…ltin client

Export BUILTIN_ANTIGRAVITY_CLIENT and BUILTIN_GEMINI_CLIENT directly so
provider modules can perform direct value comparisons without bouncing
through selectGoogleRefreshClient, and narrow oauthClientMarker from
`GoogleOauthClientMarker | unknown` to `GoogleOauthClientMarker`.

Signed-off-by: Minxi Hou <houminxi@gmail.com>
…RefreshCall

Apply GoogleOauthClientMarker to AntigravityPostExchange.oauthClient and
add explicit parameter types to captureRefreshCall test helper.

Signed-off-by: Minxi Hou <houminxi@gmail.com>
@diegosouzapw
diegosouzapw merged commit 039a425 into diegosouzapw:release/v3.8.51 Aug 30, 2026
11 of 16 checks passed
@HouMinXi
HouMinXi deleted the fix/antigravity-per-connection-oauth-client branch September 16, 2026 13:57
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…iegosouzapw#12106)

Vincula o refresh OAuth do Google ao client que emitiu o token, com teste próprio (`google-oauth-client-binding.test.ts`). Validado no worktree combinado. Obrigado!
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.

2 participants