fix(oauth): bind Google refresh to the client that issued the token - #12106
Merged
diegosouzapw merged 7 commits intoAug 30, 2026
Conversation
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>
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
merged commit Aug 30, 2026
039a425
into
diegosouzapw:release/v3.8.51
11 of 16 checks passed
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!
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.
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.clientIdresolves the env override(
ANTIGRAVITY_OAUTH_CLIENT_ID) first, so the moment an operator configures acustom web client — for the public redirect URI flow — every existing
connection's refresh starts presenting the new client, and Google answers
401
unauthorized_clientfor all of them.Observed in production: switching to a custom client for
https://<public-host>/callbackmade four healthy antigravity/agyconnections refresh-dead within minutes (access tokens would have expired an
hour later).
geminishares the same refresh case and embeds a differentdesktop 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 inpostExchangefrom the same config used for the code exchange) and make therefresh path select credentials per connection:
refresh against the embedded desktop client of their own provider
(gemini and antigravity embed different clients),
is unchanged.
Verification
gemini-vs-antigravity builtin distinction (their client ids differ; the
fallback is keyed by provider).
PROVIDERS[provider]lookup fails the unmarked/builtin cases; the per-connection selection
passes all four.
still configured in env, the previously failing refreshes (30+ consecutive
unauthorized_client) all succeed post-deploy; zero failures across allconnections.