Skip to content

Commit 0f7856d

Browse files
caohy1988claude
andcommitted
fix(auth): migrate credential storage to secret: scope (Phase 2)
Migrate existing credential writers to use the `secret:` prefix so that OAuth tokens and credentials are never persisted to session storage backends. - Change BIGQUERY_TOKEN_CACHE_KEY to "secret:bigquery_token_cache" - Update SessionStateCredentialService.save_credential and load_credential to prefix credential_key with State.SECRET_PREFIX - Add backward-compatible fallback: load paths try the secret-prefixed key first, then fall back to the legacy unprefixed key so existing sessions migrate without re-authentication - Update and add tests for prefixed keys and legacy fallback Depends on google#5132 (Phase 1: secret: scope infrastructure) Closes google#5112 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ce8d2a3 commit 0f7856d

File tree

4 files changed

+84
-21
lines changed

4 files changed

+84
-21
lines changed

src/google/adk/auth/credential_service/session_state_credential_service.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from typing_extensions import override
2020

2121
from ...agents.callback_context import CallbackContext
22+
from ...sessions.state import State
2223
from ...utils.feature_decorator import experimental
2324
from ..auth_credential import AuthCredential
2425
from ..auth_tool import AuthConfig
@@ -54,6 +55,13 @@ async def load_credential(
5455
Optional[AuthCredential]: the credential saved in the store.
5556
5657
"""
58+
# Try secret-scoped key first, fall back to legacy unprefixed key
59+
# so existing sessions migrate without re-authentication.
60+
result = callback_context.state.get(
61+
State.SECRET_PREFIX + auth_config.credential_key
62+
)
63+
if result is not None:
64+
return result
5765
return callback_context.state.get(auth_config.credential_key)
5866

5967
@override
@@ -78,6 +86,6 @@ async def save_credential(
7886
None
7987
"""
8088

81-
callback_context.state[auth_config.credential_key] = (
89+
callback_context.state[State.SECRET_PREFIX + auth_config.credential_key] = (
8290
auth_config.exchanged_auth_credential
8391
)

src/google/adk/tools/_google_credentials.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -171,11 +171,13 @@ async def get_valid_credentials(
171171
f" {self.credentials_config.external_access_token_key}."
172172
)
173173
# First, try to get credentials from the tool context
174-
creds_json = (
175-
tool_context.state.get(self.credentials_config._token_cache_key, None)
176-
if self.credentials_config._token_cache_key
177-
else None
178-
)
174+
cache_key = self.credentials_config._token_cache_key
175+
creds_json = tool_context.state.get(cache_key, None) if cache_key else None
176+
# Fall back to legacy unprefixed key so existing sessions migrate
177+
# without re-authentication.
178+
if creds_json is None and cache_key and cache_key.startswith("secret:"):
179+
legacy_key = cache_key[len("secret:") :]
180+
creds_json = tool_context.state.get(legacy_key, None)
179181
creds = (
180182
google.oauth2.credentials.Credentials.from_authorized_user_info(
181183
json.loads(creds_json), self.credentials_config.scopes

src/google/adk/tools/bigquery/bigquery_credentials.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from ...features import FeatureName
1919
from .._google_credentials import BaseGoogleCredentialsConfig
2020

21-
BIGQUERY_TOKEN_CACHE_KEY = "bigquery_token_cache"
21+
BIGQUERY_TOKEN_CACHE_KEY = "secret:bigquery_token_cache"
2222
BIGQUERY_SCOPES = [
2323
"https://www.googleapis.com/auth/bigquery",
2424
"https://www.googleapis.com/auth/dataplex.read-write",

tests/unittests/auth/credential_service/test_session_state_credential_service.py

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from google.adk.auth.auth_credential import OAuth2Auth
2424
from google.adk.auth.auth_tool import AuthConfig
2525
from google.adk.auth.credential_service.session_state_credential_service import SessionStateCredentialService
26+
from google.adk.sessions.state import State
2627
import pytest
2728

2829

@@ -265,10 +266,11 @@ async def test_state_persistence_across_operations(
265266
# Save credential
266267
await credential_service.save_credential(auth_config, callback_context)
267268

268-
# Verify state contains the credential
269-
assert auth_config.credential_key in callback_context.state
269+
# Verify state contains the credential under secret: prefix
270+
secret_key = State.SECRET_PREFIX + auth_config.credential_key
271+
assert secret_key in callback_context.state
270272
assert (
271-
callback_context.state[auth_config.credential_key]
273+
callback_context.state[secret_key]
272274
== auth_config.exchanged_auth_credential
273275
)
274276

@@ -279,9 +281,9 @@ async def test_state_persistence_across_operations(
279281
assert result is not None
280282

281283
# Verify state still contains the credential
282-
assert auth_config.credential_key in callback_context.state
284+
assert secret_key in callback_context.state
283285
assert (
284-
callback_context.state[auth_config.credential_key]
286+
callback_context.state[secret_key]
285287
== auth_config.exchanged_auth_credential
286288
)
287289

@@ -300,7 +302,7 @@ async def test_state_persistence_across_operations(
300302
await credential_service.save_credential(auth_config, callback_context)
301303

302304
# Verify state was updated
303-
assert callback_context.state[auth_config.credential_key] == new_credential
305+
assert callback_context.state[secret_key] == new_credential
304306

305307
@pytest.mark.asyncio
306308
async def test_credential_key_uniqueness(
@@ -344,13 +346,12 @@ async def test_credential_key_uniqueness(
344346
await credential_service.save_credential(auth_config1, callback_context)
345347
await credential_service.save_credential(auth_config2, callback_context)
346348

347-
# Verify both exist in state with different keys
348-
assert "unique_key_1" in callback_context.state
349-
assert "unique_key_2" in callback_context.state
350-
assert (
351-
callback_context.state["unique_key_1"]
352-
!= callback_context.state["unique_key_2"]
353-
)
349+
# Verify both exist in state with secret-prefixed keys
350+
sk1 = State.SECRET_PREFIX + "unique_key_1"
351+
sk2 = State.SECRET_PREFIX + "unique_key_2"
352+
assert sk1 in callback_context.state
353+
assert sk2 in callback_context.state
354+
assert callback_context.state[sk1] != callback_context.state[sk2]
354355

355356
# Load and verify both credentials
356357
result1 = await credential_service.load_credential(
@@ -379,10 +380,62 @@ async def test_direct_state_access(
379380
redirect_uri="https://direct.com/callback",
380381
),
381382
)
382-
callback_context.state[auth_config.credential_key] = test_credential
383+
callback_context.state[State.SECRET_PREFIX + auth_config.credential_key] = (
384+
test_credential
385+
)
383386

384387
# Load using the service
385388
result = await credential_service.load_credential(
386389
auth_config, callback_context
387390
)
388391
assert result == test_credential
392+
393+
@pytest.mark.asyncio
394+
async def test_load_falls_back_to_legacy_unprefixed_key(
395+
self, credential_service, auth_config, callback_context
396+
):
397+
"""Credentials stored under the old unprefixed key are still found."""
398+
legacy_cred = AuthCredential(
399+
auth_type=AuthCredentialTypes.OAUTH2,
400+
oauth2=OAuth2Auth(
401+
client_id="legacy_client",
402+
client_secret="legacy_secret",
403+
),
404+
)
405+
# Simulate a session persisted before the secret: migration
406+
callback_context.state[auth_config.credential_key] = legacy_cred
407+
408+
result = await credential_service.load_credential(
409+
auth_config, callback_context
410+
)
411+
assert result is not None
412+
assert result.oauth2.client_id == "legacy_client"
413+
414+
@pytest.mark.asyncio
415+
async def test_secret_key_takes_precedence_over_legacy(
416+
self, credential_service, auth_config, callback_context
417+
):
418+
"""When both keys exist, the secret-prefixed key wins."""
419+
old_cred = AuthCredential(
420+
auth_type=AuthCredentialTypes.OAUTH2,
421+
oauth2=OAuth2Auth(
422+
client_id="old_client",
423+
client_secret="old_secret",
424+
),
425+
)
426+
new_cred = AuthCredential(
427+
auth_type=AuthCredentialTypes.OAUTH2,
428+
oauth2=OAuth2Auth(
429+
client_id="new_client",
430+
client_secret="new_secret",
431+
),
432+
)
433+
callback_context.state[auth_config.credential_key] = old_cred
434+
callback_context.state[State.SECRET_PREFIX + auth_config.credential_key] = (
435+
new_cred
436+
)
437+
438+
result = await credential_service.load_credential(
439+
auth_config, callback_context
440+
)
441+
assert result.oauth2.client_id == "new_client"

0 commit comments

Comments
 (0)