🔴 Required Information
Describe the Bug:
ADK's OAuth2 credential-key methods strip eight oauth2 fields before hashing the credential but omit redirect_uri. Since redirect_uri is deployment configuration rather than credential identity, the same OAuth credential (same client_id, client_secret, scopes, access_token, refresh_token) hashes to a different key when the deployment URL changes. A credential minted under one redirect_uri can no longer be retrieved when the deployment moves to another, even though the underlying OAuth grant is unchanged.
The same eight-field strip block, and the same gap, appears at three call sites:
ToolContextCredentialStore.get_credential_key (tool_auth_handler.py:85-110)
ToolContextCredentialStore._get_legacy_credential_key (tool_auth_handler.py:58-83)
AuthConfig.get_credential_key (auth_tool.py:97-138, marked @deprecated but still invoked from AuthConfig.__init__ at line 94 when credential_key is not explicitly provided)
Steps to Reproduce:
- Configure an
OpenAPIToolset with OAuth2 authorizationCode flow, where the AuthCredential.oauth2.redirect_uri reflects deployment A (for example, a local OAuth relay at http://localhost:8001/oauth2callback).
- Run an agent that invokes a tool through that toolset and complete the OAuth authorization flow. The exchanged credential is stored in session state under a hash key derived from deployment A's
redirect_uri.
- Reconfigure the same toolset with the same
client_id, client_secret, and scopes, but change redirect_uri to deployment B's value (for example, a deployed relay at https://my-relay-...run.app/oauth2callback).
- Run the agent again against the same session state from step 2.
- Observe that the agent emits
adk_request_credential despite the credential from step 2 still being present in session state. The runtime hash computed from deployment B's redirect_uri does not match the key where the credential was stored.
A self-contained runnable script that demonstrates the same conditions in a single process (no separate deployments needed) is at https://github.com/doughayden/adk-issue-examples/tree/main/05-redirect_uri_in_credential_hash. Run uv run main.py to observe the bug and uv run main.py --apply-fix to verify the proposed fix.
Expected Behavior:
The credential lookup should hit the seeded credential regardless of the redirect_uri value, because redirect_uri is deployment configuration rather than credential identity. The tool call should succeed against the valid seeded credential without prompting for re-auth.
Observed Behavior:
The hash key computed at lookup time differs from the hash key the seed was written under (because the two redirect_uri strings differ). ToolAuthHandler._get_existing_credential returns None, prepare_auth_credentials falls into _request_credential, and adk_request_credential is emitted despite valid stored credentials being present.
🌤️ WeatherAssistant Agent — redirect_uri-in-hash repro
============================================================
Proposed fix applied: False
STORED_REDIRECT_URI: http://localhost:8080/callback
CURRENT_REDIRECT_URI: http://localhost:8081/callback
Hash keys produced by ToolContextCredentialStore.get_credential_key:
STORED → oauth2_55f666541ad22e39_oauth2_8ba0457897522d9d_existing_exchanged_credential
CURRENT → oauth2_55f666541ad22e39_oauth2_ae16199243c358df_existing_exchanged_credential
❌ Keys differ — credentials minted under STORED are not retrievable.
🔑 Seeded credential: access_token='l-wr4tRlWJO4WlZC'… (redirect_uri='http://localhost:8080/callback')
👤 User: What's the weather in San Francisco?
🌤️ Weather Assistant event stream:
[function_call] get_weather by WeatherAssistant
[auth_event] adk_request_credential by WeatherAssistant
[function_response] get_weather by WeatherAssistant
[text] WeatherAssistant: 'It seems I need your authorization to access weather data. Could you please g...'
Event counts:
function_calls: 1
auth_events: 1
function_responses: 1
text_events: 1
✅ Bug reproduced: agent emitted 1 adk_request_credential event(s) despite a valid seeded credential being present in state (hashed under STORED_REDIRECT_URI; agent looked under CURRENT_REDIRECT_URI).
Proposed Fix:
Add auth_credential.oauth2.redirect_uri = None to the strip block at each of the three call sites listed under "Describe the Bug" above. redirect_uri is not part of the credential identity (the granted access).
Environment Details:
- ADK Library Version (
pip show google-adk): google-adk==1.32.0. Verified that the strip blocks in tool_auth_handler.py and auth_tool.py are identical on current main at the time of writing, so the bug is present on current released versions.
- Desktop OS: macOS (bug is platform-agnostic; the hash logic is pure Python)
- Python Version (
python -V): Python 3.13.13
Model Information:
- Are you using LiteLLM: No
- Which model is being used:
gemini-2.5-flash (bug is model-agnostic; it fires during credential lookup, before any LLM call would reach the provider)
🟡 Optional Information
Regression:
Not a regression. The strip block has carried this gap since OAuth2 credential-key generation was introduced. The most recent credential-key commit (33012e6d, 2026-02-03, "Make credential key generation stable and prevent cross-user credential leaks") tightened the strip block by excluding additional transient OAuth2 fields but did not add redirect_uri.
Logs:
Output from the same repro with --apply-fix applied, for comparison:
🌤️ WeatherAssistant Agent — redirect_uri-in-hash repro
============================================================
Proposed fix applied: True
STORED_REDIRECT_URI: http://localhost:8080/callback
CURRENT_REDIRECT_URI: http://localhost:8081/callback
Hash keys produced by ToolContextCredentialStore.get_credential_key:
STORED → oauth2_55f666541ad22e39_oauth2_c2ad46dffd26cd87_existing_exchanged_credential
CURRENT → oauth2_55f666541ad22e39_oauth2_c2ad46dffd26cd87_existing_exchanged_credential
✅ Keys match — fix is taking effect at the hash level.
🔑 Seeded credential: access_token='A46ZCrsEq-l3uC1X'… (redirect_uri='http://localhost:8080/callback')
👤 User: What's the weather in San Francisco?
🌤️ Weather Assistant event stream:
[function_call] get_weather by WeatherAssistant
[function_response] get_weather by WeatherAssistant
[text] WeatherAssistant: 'The weather in San Francisco is Clear with a temperature of 30 degrees Celsiu...'
Event counts:
function_calls: 1
auth_events: 0
function_responses: 1
text_events: 1
✅ Fix verified: tool call succeeded against the seeded credential without an adk_request_credential prompt.
Additional Context:
A concrete example of why this matters: a deployed application and a local development environment sharing the same session DB. The deployed app's redirect_uri points at the deployed callback URL; the local app's redirect_uri points at a local server callback path. The underlying user grant and OAuth tokens are identical (same user, same OAuth client app, same scopes), so a grant made from one environment should be reusable from the other through the shared session DB. Today the two environments hash credentials under different keys and are effectively isolated, so the user is re-prompted for OAuth in each environment despite holding a valid grant. (All valid redirect_uri values must still be registered in the OAuth client app's allow-list on the provider side, which is a prerequisite outside ADK and not something this fix changes.)
Related:
Minimal Reproduction Code:
Self-contained runnable repro at https://github.com/doughayden/adk-issue-examples/tree/main/05-redirect_uri_in_credential_hash. Pins google-adk==1.32.0 and reproduces against current main. Includes a --apply-fix flag that monkey-patches the proposed fix.
How often has this issue occurred?:
Always (100%). The bug is deterministic at the hash level: two AuthCredential instances differing only in redirect_uri produce different keys on every invocation.
🔴 Required Information
Describe the Bug:
ADK's OAuth2 credential-key methods strip eight
oauth2fields before hashing the credential but omitredirect_uri. Sinceredirect_uriis deployment configuration rather than credential identity, the same OAuth credential (same client_id, client_secret, scopes, access_token, refresh_token) hashes to a different key when the deployment URL changes. A credential minted under one redirect_uri can no longer be retrieved when the deployment moves to another, even though the underlying OAuth grant is unchanged.The same eight-field strip block, and the same gap, appears at three call sites:
ToolContextCredentialStore.get_credential_key(tool_auth_handler.py:85-110)ToolContextCredentialStore._get_legacy_credential_key(tool_auth_handler.py:58-83)AuthConfig.get_credential_key(auth_tool.py:97-138, marked@deprecatedbut still invoked fromAuthConfig.__init__at line 94 whencredential_keyis not explicitly provided)Steps to Reproduce:
OpenAPIToolsetwith OAuth2authorizationCodeflow, where theAuthCredential.oauth2.redirect_urireflects deployment A (for example, a local OAuth relay athttp://localhost:8001/oauth2callback).redirect_uri.client_id,client_secret, and scopes, but changeredirect_urito deployment B's value (for example, a deployed relay athttps://my-relay-...run.app/oauth2callback).adk_request_credentialdespite the credential from step 2 still being present in session state. The runtime hash computed from deployment B'sredirect_uridoes not match the key where the credential was stored.A self-contained runnable script that demonstrates the same conditions in a single process (no separate deployments needed) is at https://github.com/doughayden/adk-issue-examples/tree/main/05-redirect_uri_in_credential_hash. Run
uv run main.pyto observe the bug anduv run main.py --apply-fixto verify the proposed fix.Expected Behavior:
The credential lookup should hit the seeded credential regardless of the
redirect_urivalue, becauseredirect_uriis deployment configuration rather than credential identity. The tool call should succeed against the valid seeded credential without prompting for re-auth.Observed Behavior:
The hash key computed at lookup time differs from the hash key the seed was written under (because the two
redirect_uristrings differ).ToolAuthHandler._get_existing_credentialreturns None,prepare_auth_credentialsfalls into_request_credential, andadk_request_credentialis emitted despite valid stored credentials being present.Proposed Fix:
Add
auth_credential.oauth2.redirect_uri = Noneto the strip block at each of the three call sites listed under "Describe the Bug" above.redirect_uriis not part of the credential identity (the granted access).Environment Details:
pip show google-adk):google-adk==1.32.0. Verified that the strip blocks intool_auth_handler.pyandauth_tool.pyare identical on currentmainat the time of writing, so the bug is present on current released versions.python -V): Python 3.13.13Model Information:
gemini-2.5-flash(bug is model-agnostic; it fires during credential lookup, before any LLM call would reach the provider)🟡 Optional Information
Regression:
Not a regression. The strip block has carried this gap since OAuth2 credential-key generation was introduced. The most recent credential-key commit (
33012e6d, 2026-02-03, "Make credential key generation stable and prevent cross-user credential leaks") tightened the strip block by excluding additional transient OAuth2 fields but did not addredirect_uri.Logs:
Output from the same repro with
--apply-fixapplied, for comparison:Additional Context:
A concrete example of why this matters: a deployed application and a local development environment sharing the same session DB. The deployed app's
redirect_uripoints at the deployed callback URL; the local app'sredirect_uripoints at a local server callback path. The underlying user grant and OAuth tokens are identical (same user, same OAuth client app, same scopes), so a grant made from one environment should be reusable from the other through the shared session DB. Today the two environments hash credentials under different keys and are effectively isolated, so the user is re-prompted for OAuth in each environment despite holding a valid grant. (All validredirect_urivalues must still be registered in the OAuth client app's allow-list on the provider side, which is a prerequisite outside ADK and not something this fix changes.)Related:
ToolAuthHandler._get_existing_credentialrefreshes OAuth2 credentials in memory but doesn't persist them #5329 (refreshed credential persistence)Minimal Reproduction Code:
Self-contained runnable repro at https://github.com/doughayden/adk-issue-examples/tree/main/05-redirect_uri_in_credential_hash. Pins
google-adk==1.32.0and reproduces against currentmain. Includes a--apply-fixflag that monkey-patches the proposed fix.How often has this issue occurred?:
Always (100%). The bug is deterministic at the hash level: two
AuthCredentialinstances differing only inredirect_uriproduce different keys on every invocation.