Skip to content

fix(oauth): let Google refresh path run instead of forcing hourly reconsent#210

Merged
philmerrell merged 1 commit into
developfrom
fix/google-oauth-refresh-loop
May 2, 2026
Merged

fix(oauth): let Google refresh path run instead of forcing hourly reconsent#210
philmerrell merged 1 commit into
developfrom
fix/google-oauth-refresh-loop

Conversation

@philmerrell

Copy link
Copy Markdown
Contributor

Closes #209.

Summary

Users connected to Google connectors had to click Reconnect roughly every hour because the OAuth refresh path silently never ran. Three coordinated changes fix this.

  • A — drop the auto-disconnect on 401. _handle_auth_failure used to write the durable disconnect flag whenever a tool returned a 401. The retry then called AgentCore with force_authentication=True, which bypasses the vault entirely — including the still-valid refresh_token — and prompts for full re-consent. Now a 401 just clears the local cache and retries; _gate re-fetches from AgentCore (no force_authentication) so the refresh runs transparently. The disconnect flag is now reserved for the explicit Disconnect button.

  • B — TTL on the in-process cache. oauth_token_cache used to hold tokens forever. Once warmed, the cache returned the same access_token until eviction — so AgentCore was never re-asked, and never got the chance to refresh. The cache now has a default TTL of 3000s (under Google's 3600s access_token lifetime), so we proactively re-fetch and let AgentCore handle refresh before the upstream rejects the token.

  • C — prompt=consent on the explicit re-consent path. custom_parameters_for(...) now takes a force_authentication flag and adds prompt=consent for Google when set. Google only re-issues a refresh_token on subsequent grants if the consent screen is shown — without this, a Disconnect/Reconnect cycle leaves the vault with an access_token but no refresh_token, putting the user right back in the hourly-reconsent loop. First-time consent and silent refreshes are unaffected.

Also drops the now-dead mark_disconnected plumbing (parameter, type, method, base_agent closure) per the no-dead-code convention.

Test plan

  • Unit tests pass for all three touched files (156 tests across test_oauth_token_cache.py, test_oauth_consent_hook.py, test_agentcore_identity.py)
  • Cache TTL verified with both mocked-clock and real-clock tests
  • Force-auth path asserts prompt=consent propagation; silent refresh asserts it is not sent
  • 401 retry path asserts the disconnect flag is not written (regression guard for the hourly-reconnect bug)
  • Manually verify a Google connector tool call after the access token expires — should refresh silently without a Reconnect prompt
  • Manually verify Disconnect → Reconnect issues a fresh refresh_token (consent screen appears, subsequent calls work past the 1h boundary)

🤖 Generated with Claude Code

…onsent

Closes #209.

Three coordinated changes so AgentCore Identity's refresh flow actually
gets to run:

* Add a TTL (default 3000s) to the in-process token cache. Without it,
  warmed entries were returned past the upstream 3600s lifetime, which
  meant AgentCore was never re-asked and never got the chance to refresh.

* Stop writing the durable disconnect flag from the AfterToolCallEvent
  401 retry path. A 401 just clears the cache; the BeforeToolCallEvent
  retry re-fetches from AgentCore (force_authentication=False) so refresh
  runs transparently. The disconnect flag is now reserved for the
  explicit Disconnect button in the settings page.

* Add prompt=consent to Google's customParameters when the caller is
  forcing a fresh consent (force_authentication=True). Google only
  re-issues a refresh_token on subsequent grants if the consent screen
  is shown — without this, a Disconnect/Reconnect cycle leaves the vault
  with an access_token but no refresh_token, putting the user back in
  the hourly-reconsent loop on the next access-token expiry.

Also drops the now-unused mark_disconnected wiring (parameter, type,
method, base_agent closure) per the no-dead-code rule.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@philmerrell philmerrell merged commit 59edc58 into develop May 2, 2026
24 checks passed
@philmerrell philmerrell deleted the fix/google-oauth-refresh-loop branch May 2, 2026 04:59
philmerrell added a commit that referenced this pull request May 4, 2026
Google's hourly-reconsent loop returns. The fix from #210 only sent
`prompt=consent` when the durable disconnect flag was set, but the
vault can lose its refresh token in other ways (testing-mode
refresh-token expiry, user revocation at Google, or any path where
AgentCore returns an authorize URL without our disconnect endpoint
being involved). In those cases Google saw a previously-consented
user, skipped the consent screen, and re-issued an access_token
without a refresh_token — back into the loop on the next expiry.

`initiate_consent` is by definition a "walk me through consent" path
(the user clicked Connect/Reconnect after seeing they were not
connected), so always pass `force_authentication=True` to
`custom_parameters_for` here. This decouples the customParameters
builder from the SDK's vault-bypass flag — `prompt=consent` always
reaches Google on this path, while the silent-refresh fast path on
`/status` and the agent-side gate is unaffected.

Affects Google providers only; the vendor baseline returns {} for
every other provider regardless of the flag.

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