fix(auth): persist refreshed OAuth2 credentials to store

Merge https://github.com/google/adk-python/pull/5350
Fixes #5329

Co-authored-by: Xuan Yang <xygoogle@google.com>
COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/5350 from voidborne-d:fix/persist-refreshed-oauth2-credential 0ef3d8c19616b45f932b15ee2a200bc74c8334b3
PiperOrigin-RevId: 913922074
This commit is contained in:
d 🔹
2026-05-11 15:50:45 -07:00
committed by Copybara-Service
parent 6d89d2194a
commit 218ea76e30
2 changed files with 70 additions and 3 deletions
@@ -242,6 +242,12 @@ class ToolAuthHandler:
existing_credential = await refresher.refresh(
existing_credential, self.auth_scheme
)
# Persist the refreshed credential so the next invocation
# reads the new tokens instead of the stale pre-refresh ones.
# Without this, providers that rotate refresh_tokens on each
# refresh (e.g. Salesforce, many OIDC providers) will fail
# because the old refresh_token has already been invalidated.
self._store_credential(existing_credential)
return existing_credential
return None
@@ -13,6 +13,7 @@
# limitations under the License.
from typing import Optional
from unittest.mock import AsyncMock
from unittest.mock import MagicMock
from unittest.mock import patch
@@ -29,6 +30,7 @@ from google.adk.sessions.session import Session
from google.adk.tools.openapi_tool.auth.auth_helpers import openid_dict_to_scheme_credential
from google.adk.tools.openapi_tool.auth.auth_helpers import token_to_scheme_credential
from google.adk.tools.openapi_tool.auth.credential_exchangers.auto_auth_credential_exchanger import OAuth2CredentialExchanger
from google.adk.tools.openapi_tool.openapi_spec_parser import tool_auth_handler
from google.adk.tools.openapi_tool.openapi_spec_parser.tool_auth_handler import ToolAuthHandler
from google.adk.tools.openapi_tool.openapi_spec_parser.tool_auth_handler import ToolContextCredentialStore
from google.adk.tools.tool_context import ToolContext
@@ -223,9 +225,7 @@ async def test_openid_connect_existing_token(
assert result.auth_credential == existing_credential
@patch(
'google.adk.tools.openapi_tool.openapi_spec_parser.tool_auth_handler.OAuth2CredentialRefresher'
)
@patch.object(tool_auth_handler, 'OAuth2CredentialRefresher')
@pytest.mark.asyncio
async def test_openid_connect_existing_oauth2_token_refresh(
mock_oauth2_refresher, openid_connect_scheme, openid_connect_credential
@@ -292,3 +292,64 @@ async def test_openid_connect_existing_oauth2_token_refresh(
assert result.state == 'done'
# The result should contain the refreshed credential after exchange
assert result.auth_credential is not None
@patch.object(tool_auth_handler, 'OAuth2CredentialRefresher')
@pytest.mark.asyncio
async def test_refreshed_credential_is_persisted_to_store(
mock_oauth2_refresher, openid_connect_scheme, openid_connect_credential
):
"""Test that refreshed OAuth2 credentials are persisted back to the store."""
# Create existing OAuth2 credential with an "old" refresh token.
existing_credential = AuthCredential(
auth_type=AuthCredentialTypes.OPEN_ID_CONNECT,
oauth2=OAuth2Auth(
client_id='test_client_id',
client_secret='test_client_secret',
access_token='old_access_token',
refresh_token='old_refresh_token',
),
)
# The refresher will return a credential with rotated tokens.
refreshed_credential = AuthCredential(
auth_type=AuthCredentialTypes.OPEN_ID_CONNECT,
oauth2=OAuth2Auth(
client_id='test_client_id',
client_secret='test_client_secret',
access_token='new_access_token',
refresh_token='new_refresh_token',
),
)
mock_refresher_instance = MagicMock()
mock_refresher_instance.is_refresh_needed = AsyncMock(return_value=True)
mock_refresher_instance.refresh = AsyncMock(return_value=refreshed_credential)
mock_oauth2_refresher.return_value = mock_refresher_instance
tool_context = create_mock_tool_context()
credential_store = ToolContextCredentialStore(tool_context=tool_context)
# Store the existing (stale) credential.
key = credential_store.get_credential_key(
openid_connect_scheme, openid_connect_credential
)
credential_store.store_credential(key, existing_credential)
handler = ToolAuthHandler(
tool_context,
openid_connect_scheme,
openid_connect_credential,
credential_store=credential_store,
)
await handler.prepare_auth_credentials()
# The critical assertion: the *refreshed* credential must now be in the
# store so that the next invocation reads the new tokens, not the old ones.
persisted = credential_store.get_credential(
openid_connect_scheme, openid_connect_credential
)
assert persisted is not None
assert persisted.oauth2.access_token == 'new_access_token'
assert persisted.oauth2.refresh_token == 'new_refresh_token'