Repository navigation
Security Critical: Client_secret is shared in OAuth2 flow with authenticated function tool #3845
Description
Describe the bug
The protocol publicly share the client_secret to the ADK client. This information MUST stay confidential and never shared
To Reproduce
- 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]
)
- Create a session
- 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
fix: solve google#3845
llalitkumarrr commentedon Feb 11, 2026
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.idofl commentedon Feb 15, 2026
I can confirm this issue still exists in ADK 1.25
This issue was previously reported #2128 but was not resolved.
llalitkumarrr commentedon Feb 23, 2026
Hello @seanzhou1023 ,
Could you please have a look at this?
4 remaining items
guilmour commentedon May 14, 2026
I can confirm this issue still exists in ADK 1.33.0.
joar commentedon May 27, 2026
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:
docs(bqaa): restore concrete OAuth credential-exposure warning
docs(bqaa): align plugin docs with Python 2.9 and Java 1.9, add dashb…
docs(bqaa): align plugin docs with Python 2.9 and Java 1.9, add dashb…
GWeale commentedon Sep 28, 2026
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
idofl commentedon Sep 29, 2026
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?
guillaumeblaquiere commentedon Sep 29, 2026
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.
idofl commentedon Sep 29, 2026
@guillaumeblaquiere which auth scheme and auth credentials did you use for your test?
guillaumeblaquiere commentedon Sep 30, 2026
I'm testing it on the BigQuery toolset
idofl commentedon Sep 30, 2026
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?
idofl commentedon Oct 1, 2026
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.
guillaumeblaquiere commentedon Oct 2, 2026
@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
docs(bigquery-analytics): align BigQueryAgentAnalyticsPlugin docs wit…