Skip to content

Security Critical: Client_secret is shared in OAuth2 flow with authenticated function tool #3845

Closed

Description

@guillaumeblaquiere

Describe the bug
The protocol publicly share the client_secret to the ADK client. This information MUST stay confidential and never shared

To Reproduce

  1. Create a simple agent, with a simple tool, and use authenticated function tool
from google.adk.agents.llm_agent import Agent
from fastapi.openapi.models import OAuth2, OAuthFlows, OAuthFlowAuthorizationCode
from google.adk.auth import AuthCredential, AuthConfig, AuthCredentialTypes, OAuth2Auth
from google.adk.tools import ToolContext
from google.adk.tools.authenticated_function_tool import AuthenticatedFunctionTool

YOUR_OAUTH_CLIENT_ID = "513069150666-mkspihfonim1j4f0mfp8j2v6hkdtep6k.apps.googleusercontent.com"
YOUR_OAUTH_CLIENT_SECRET = "GOCSPX-<redacted>


def log_tool(prompt: str, tool_context: ToolContext) -> None:
    """
    Log the user query with the log_tool

    :param prompt: The user query to log
    :param tool_context: The tool context to access credentials

    :return:     None

    """

    print(f"DEBUG: Received prompt: {prompt}")

    headers = {}
    
    # Retrieve the credential using the context and auth config
    exchanged_credential = tool_context.get_auth_response(oauth_config)
    if not exchanged_credential:
        exchanged_credential = tool_context.get_auth_response(oidc_config)
    
    if exchanged_credential and exchanged_credential.oauth2:
        # Log the token for debug purposes as requested
        print(f"DEBUG: Found credential for user {tool_context.user_id}")

        access_token = exchanged_credential.oauth2.access_token

        print(f"DEBUG: Access Token: {access_token}")

    else:
        print("DEBUG: No credential found in tool_context")

    
oauth_scheme = OAuth2(
    flows=OAuthFlows(
        authorizationCode=OAuthFlowAuthorizationCode(
            authorizationUrl="https://accounts.google.com/o/oauth2/auth",
            tokenUrl="https://oauth2.googleapis.com/token",
            scopes={
                "https://www.googleapis.com/auth/cloud-platform": "BigQuery"
            },
        )
    )
)


oauth_credential = AuthCredential(
    auth_type=AuthCredentialTypes.OAUTH2,
    oauth2=OAuth2Auth(
        client_id=YOUR_OAUTH_CLIENT_ID,
        client_secret=YOUR_OAUTH_CLIENT_SECRET
    ),
)

oauth_config = AuthConfig(
        auth_scheme=oauth_scheme,
        raw_auth_credential= oauth_credential
        )


oauth_authenticated_log_tool = AuthenticatedFunctionTool(func=log_tool, auth_config=oauth_config, response_for_auth_required="Pending User Authorization.")

root_agent = Agent(
    model='gemini-2.5-flash',
    name='root_agent',
    description='A helpful assistant for user questions.',
    instruction='Answer user questions to the best of your knowledge. Log the user query with the log_tool',
    tools=[oauth_authenticated_log_tool]   
)

  1. Create a session
  2. Invoke the secure tool

Sample of ADK response to indicate to the client to run a OAuth2 flow

[
  {
    "modelVersion": "gemini-2.5-flash",
    "content": {
      "parts": [
        {
          "text": "Okay, let's break this down. First, I'm starting with a very simple request, in French, to get information: \"Hello, a quel projet j'ai accès?\"  That's my way of asking, \"Hello, which project do I have access to?\" Pretty straightforward, really.\n\nNow, from an architectural standpoint, I know that translates directly into the capabilities of the `list_project_tool`.  That tool *is* designed to tell me exactly that: which Google Cloud projects are available to me. And the beauty of this is its simplicity –  `list_project_tool` doesn't need any additional parameters; it just does what it says on the tin. So, my action is clear: I need to invoke the `list_project_tool` immediately to get the project list and provide a rapid answer.  This is a core, fundamental action.\n",
          "thought": true
        },
        {
          "functionCall": {
            "id": "adk-278007a2-d9c0-4f42-9be3-813ade149f37",
            "args": {},
            "name": "list_project_tool"
          },
          "thoughtSignature": "CpoDAePx_16-kKCcDZLmBIV4VfmwSfm1hqaUlGKD8fEbq-gEZlt60Xr0pGKHyutmyUR3ICd_Mv5NNP2QIxfSx4qrAE9blXQG4YQesJPe0bxM3IiXZRn2FnclS7xBbRi9RfaFCGzJx1Hp_g4WQDaNZIWqaNSb7aM8Cunx7B1EXPHgNbvIJ3IeIsXEVBd24VGJmpWwjRXtbLOvFiPkRwqk4ryl27Eu3AJwzb2uIRXWGtGWdp4JpNW0nGbRgiZ7By25uVGi6ajw2TGXc8gbOmt-5Jw8yjF2ZHSNcJqcqVIT62jCw6S7ViOhrtYjGW4mw5iV4NqjLOd6HE3v4m2Kemohwqz_-6qftz7hyPiFuOFT3QAWu0SOdVC4b3UX97eBgmzikJbPuvcgYzMpMM4cXoG_19JBEii-Ih4SbfVbxoXj-v4iEU2RHvOiDocC3IXpqnTng_MsuacTLhVbd3xndeexkZNu4yqW94AH5dKKytCmM81n-DQDrEe-WuC2l-O3xTN-AUz4nlg7GlmwPN1fUAxfgTuGoByQSayiDcLZH2g="
        }
      ],
      "role": "model"
    },
    "finishReason": "STOP",
    "usageMetadata": {
      "candidatesTokenCount": 5,
      "candidatesTokensDetails": [
        {
          "modality": "TEXT",
          "tokenCount": 5
        }
      ],
      "promptTokenCount": 156,
      "promptTokensDetails": [
        {
          "modality": "TEXT",
          "tokenCount": 156
        }
      ],
      "thoughtsTokenCount": 86,
      "totalTokenCount": 247,
      "trafficType": "ON_DEMAND"
    },
    "avgLogprobs": -1.7120042800903321,
    "invocationId": "e-7a667312-57eb-4955-965f-b4faf1302ce2",
    "author": "space_agent",
    "actions": {
      "stateDelta": {},
      "artifactDelta": {},
      "requestedAuthConfigs": {},
      "requestedToolConfirmations": {}
    },
    "longRunningToolIds": [],
    "id": "734f13d7-d323-4a8a-bff1-da7fde72109e",
    "timestamp": 1764943012.128835
  },
  {
    "content": {
      "parts": [
        {
          "functionCall": {
            "id": "adk-4cab06d4-397d-4013-ab7b-d1ee3bafac12",
            "args": {
              "functionCallId": "adk-278007a2-d9c0-4f42-9be3-813ade149f37",
              "authConfig": {
                "authScheme": {
                  "type": "oauth2",
                  "flows": {
                    "authorizationCode": {
                      "scopes": {
                        "https://www.googleapis.com/auth/cloud-platform": "BigQuery"
                      },
                      "authorizationUrl": "https://accounts.google.com/o/oauth2/auth",
                      "tokenUrl": "https://oauth2.googleapis.com/token"
                    }
                  }
                },
                "rawAuthCredential": {
                  "authType": "oauth2",
                  "oauth2": {
                    "clientId": "513069150666-mkspihfonim1j4f0mfp8j2v6hkdtep6k.apps.googleusercontent.com",
                    "clientSecret": "GOCSPX-<redacted>"
                  }
                },
                "exchangedAuthCredential": {
                  "authType": "oauth2",
                  "oauth2": {
                    "clientId": "513069150666-mkspihfonim1j4f0mfp8j2v6hkdtep6k.apps.googleusercontent.com",
                    "clientSecret": "GOCSPX-<redacted>",
                    "authUri": "https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=513069150666-mkspihfonim1j4f0mfp8j2v6hkdtep6k.apps.googleusercontent.com&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloud-platform&state=0eEsoJ0HwJynIGynZ0GtOZmAiJWsTz&access_type=offline&prompt=consent",
                    "state": "0eEsoJ0HwJynIGynZ0GtOZmAiJWsTz"
                  }
                },
                "credentialKey": "adk_oauth2_-3537330412975045649_oauth2_-8782076668427256465"
              }
            },
            "name": "adk_request_credential"
          }
        }
      ],
      "role": "user"
    },
    "invocationId": "e-7a667312-57eb-4955-965f-b4faf1302ce2",
    "author": "space_agent",
    "actions": {
      "stateDelta": {},
      "artifactDelta": {},
      "requestedAuthConfigs": {},
      "requestedToolConfirmations": {}
    },
    "longRunningToolIds": [
      "adk-4cab06d4-397d-4013-ab7b-d1ee3bafac12"
    ],
    "id": "65986065-222e-4ab2-b9b1-3b40e9adfa73",
    "timestamp": 1764943015.619703
  },
  {
    "content": {
      "parts": [
        {
          "functionResponse": {
            "id": "adk-278007a2-d9c0-4f42-9be3-813ade149f37",
            "name": "list_project_tool",
            "response": {
              "result": "Pending User Authorization."
            }
          }
        }
      ],
      "role": "user"
    },
    "invocationId": "e-7a667312-57eb-4955-965f-b4faf1302ce2",
    "author": "space_agent",
    "actions": {
      "stateDelta": {},
      "artifactDelta": {},
      "requestedAuthConfigs": {
        "adk-278007a2-d9c0-4f42-9be3-813ade149f37": {
          "authScheme": {
            "type": "oauth2",
            "flows": {
              "authorizationCode": {
                "scopes": {
                  "https://www.googleapis.com/auth/cloud-platform": "BigQuery"
                },
                "authorizationUrl": "https://accounts.google.com/o/oauth2/auth",
                "tokenUrl": "https://oauth2.googleapis.com/token"
              }
            }
          },
          "rawAuthCredential": {
            "authType": "oauth2",
            "oauth2": {
              "clientId": "513069150666-mkspihfonim1j4f0mfp8j2v6hkdtep6k.apps.googleusercontent.com",
              "clientSecret": "GOCSPX-<redacted>"
            }
          },
          "exchangedAuthCredential": {
            "authType": "oauth2",
            "oauth2": {
              "clientId": "513069150666-mkspihfonim1j4f0mfp8j2v6hkdtep6k.apps.googleusercontent.com",
              "clientSecret": "GOCSPX-<redacted>",
              "authUri": "https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=513069150666-mkspihfonim1j4f0mfp8j2v6hkdtep6k.apps.googleusercontent.com&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloud-platform&state=0eEsoJ0HwJynIGynZ0GtOZmAiJWsTz&access_type=offline&prompt=consent",
              "state": "0eEsoJ0HwJynIGynZ0GtOZmAiJWsTz"
            }
          },
          "credentialKey": "adk_oauth2_-3537330412975045649_oauth2_-8782076668427256465"
        }
      },
      "requestedToolConfirmations": {}
    },
    "id": "d2beef7d-abb7-499d-8929-d4e5c11da93c",
    "timestamp": 1764943015.618363
  },
  {
    "modelVersion": "gemini-2.5-flash",
    "content": {
      "parts": [
        {
          "text": "I need your authorization to list the projects you have access to. Could you please grant it?"
        }
      ],
      "role": "model"
    },
    "finishReason": "STOP",
    "usageMetadata": {
      "candidatesTokenCount": 19,
      "candidatesTokensDetails": [
        {
          "modality": "TEXT",
          "tokenCount": 19
        }
      ],
      "promptTokenCount": 358,
      "promptTokensDetails": [
        {
          "modality": "TEXT",
          "tokenCount": 442
        }
      ],
      "totalTokenCount": 377,
      "trafficType": "ON_DEMAND"
    },
    "avgLogprobs": -0.19399306648655942,
    "invocationId": "e-7a667312-57eb-4955-965f-b4faf1302ce2",
    "author": "space_agent",
    "actions": {
      "stateDelta": {},
      "artifactDelta": {},
      "requestedAuthConfigs": {},
      "requestedToolConfirmations": {}
    },
    "id": "889fbcf7-1075-41a1-9c8c-4e46aac5acfd",
    "timestamp": 1764943015.620248
  }
]

Expected behavior
Not to see the client_secret, or see it redacted

Desktop (please complete the following information):

  • OS: Windows with WSL Ubuntu 24
  • Python version(python -V): 3.12
  • ADK version(pip show google-adk): 1.19.0

Model Information:

  • Which model is being used(e.g. gemini-2.5-pro): Gemini 2.5 flash

Additional context
I didn't find a way to solve this easily. There are many classes to handle the authentication (credential service, credential manager, authHandler, oauth exchanger,...)

My wish is to have a in memory map with the client_id as a key, and the client secret as value.
When the function response comes in, the auth_config must be enriched with the client_secret, based on the client secret contained in the exchanged

Activity

  1. added
    auth[Component] This issue is related to authorization
    on Dec 5, 2025
  2. added theissue type on Dec 5, 2025
  3. added a commit that references this issue on Dec 5, 2025
  4. llalitkumarrr commented on Feb 11, 2026

    @llalitkumarrr
    Collaborator

    Hello @guillaumeblaquiere ,

    We apologize for the delayed response. Please upgrade to the latest version (google-adk==1.24.1) to see if you are still experiencing this issue. It would be very helpful for our analysis if you could share your observations after testing this version.

  5. idofl commented on Feb 15, 2026

    @idofl

    I can confirm this issue still exists in ADK 1.25
    This issue was previously reported #2128 but was not resolved.

  6. llalitkumarrr commented on Feb 23, 2026

    @llalitkumarrr
    Collaborator

    Hello @seanzhou1023 ,

    Could you please have a look at this?

  7. added
    needs review[Status] The PR/issue is awaiting review from the maintainer
    on Feb 23, 2026
  8. added
    spam[Status] Issues suspected of having comments which are spam
    on Mar 17, 2026
  9. 4 remaining items

  10. guilmour commented on May 14, 2026

    @guilmour

    I can confirm this issue still exists in ADK 1.33.0.

  11. joar commented on May 27, 2026

    @joar

    I have reported this issue via Google VRP on 2026-02-24 as well (I didn't find this issue at the time): https://issu.ee/487177393

    Google VRP concluded on 2026-04-07 with:

    We have filed a bug with the product team. They will review your report and decide if a fix is required. We'll let you know if the issue was fixed.

  12. GWeale commented on Sep 28, 2026

    @GWeale
    Collaborator

    fixed in v2.9.0 by 393b3b0, the adk_request_credential event no longer carries the client_secret. closing, reopen if you still see it after upgrading

  13. idofl commented on Sep 29, 2026

    @idofl

    I'm using the updated code with ADK Web, and can confirm the client_secret is not included anymore in the client's state.
    However, when an authorization response is returned to the ADK UI, the call to merge the client and server credentials uses two credentials objects, client and server, and both are missing the client_secret. This causes the rest of the code to fail, as the client_secret is lost, and the authorization flow cannot be complete.

    Can you please recheck the fix with an OAuth Authorization Code flow?

  14. guillaumeblaquiere commented on Sep 29, 2026

    @guillaumeblaquiere
    ContributorAuthor

    I also tested and I didn't notice this issue. I was able to get the access token and the refresh token, and the client secret is still here and valid (and in the session state -> Refer to this pull request to cipher the sensitive information in the session state: #6614)

    @idofl You should share your piece of code that you use and that cause this issue. I do not have it on my end.

  15. idofl commented on Sep 29, 2026

    @idofl

    @guillaumeblaquiere which auth scheme and auth credentials did you use for your test?

  16. guillaumeblaquiere commented on Sep 30, 2026

    @guillaumeblaquiere
    ContributorAuthor

    I'm testing it on the BigQuery toolset

    oauth_credentials_config = BigQueryCredentialsConfig(
        client_id=client_id,
        client_secret=client_secret,
        scopes=scopes
    )
    
    oauth_bigquery_toolset = BigQueryToolset(
        credentials_config=oauth_credentials_config,
        tool_filter=["execute_sql"])
    
  17. idofl commented on Sep 30, 2026

    @idofl

    Ok, I see the difference.

    @guillaumeblaquiere, in your case, the BigQueryToolset is getting the tool call after receiving the authorization code, and then handles code exchange under the hood, using the OpenApi tool that all Google API tools use.

    In my case, I'm using the generic McpToolset, with Oauth2 scheme and credentials, and expecting the ADK Auth library to handle the code exchange prior to calling the MCP tool. This doesn't work, as the ADK Auth code expects to find the client_secret in a copy of the Auth Credentials that got cleared of its client_secret attribute.

    @guillaumeblaquiere, any chance you can re-check the fix with a generic McpToolset configured with OAuth scheme and credentials?

  18. idofl commented on Oct 1, 2026

    @idofl

    Looking into the code, now that the client_secret is not included in the tool result, when _store_auth_and_collect_resume_targets is called after signing in, the auth_config is loaded from the request, and the raw_auth_credential is deserialized without the client_secret.

    When the code then tries to exchange the authorization code for an access token, an inner function attempts to restore the client_id and client_secret from the raw_auth_credential, finds None, and starts the authorization process all over again.

    To resolve this, one possible option would be to update CredentialManager and store the raw_auth_credentials before returning None to invoke the adk user authorization. Then, when processing the authorization response callback, after deserializing the args, we can load the raw credentials and restore the client_id and client_secret.

  19. guillaumeblaquiere commented on Oct 2, 2026

    @guillaumeblaquiere
    ContributorAuthor

    @idofl Sorry for the late reply, I was travelling.

    Anyway, I tested with a MCP server and I still not have the client secret in the response

    auth_scheme = OAuth2(
        flows=OAuthFlows(
            authorizationCode=OAuthFlowAuthorizationCode(
                authorizationUrl="https://accounts.google.com/o/oauth2/auth",
                tokenUrl="https://oauth2.googleapis.com/token",
                scopes={
                    "https://www.googleapis.com/auth/bigquery": "Accès complet aux ressources BigQuery"
                },
            )
        )
    )
    
    # 2. Définition des identifiants client OAuth
    auth_credential = AuthCredential(
        auth_type=AuthCredentialTypes.OAUTH2,
        oauth2=OAuth2Auth(
            client_id=client_id,
            client_secret=client_secret,
        ),
    )
    
    # 3. Initialisation du MCP Toolset pointant vers le serveur BigQuery managé
    bigquery_mcp_tool = McpToolset(
        connection_params=StreamableHTTPConnectionParams(
            url="https://bigquery.googleapis.com/mcp"
        ),
        auth_credential=auth_credential,
        auth_scheme=auth_scheme,
    )
    
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

auth[Component] This issue is related to authorizationneeds review[Status] The PR/issue is awaiting review from the maintainer

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions