Skip to content

OAuth2 credential_key includes redirect_uri, breaking credential lookup across deployment URLs #5691

@doughayden

Description

@doughayden

🔴 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:

  1. 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).
  2. 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.
  3. 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).
  4. Run the agent again against the same session state from step 2.
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions