Skip to content

Commit 2c739ab

Browse files
seanzhougooglecopybara-github
authored andcommitted
chore: Add Credential Manager for managing tools credential (Experimental)
PiperOrigin-RevId: 772986051
1 parent 9a207cb commit 2c739ab

File tree

2 files changed

+824
-0
lines changed

2 files changed

+824
-0
lines changed
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
from typing import Optional
18+
19+
from ..tools.tool_context import ToolContext
20+
from ..utils.feature_decorator import experimental
21+
from .auth_credential import AuthCredential
22+
from .auth_credential import AuthCredentialTypes
23+
from .auth_schemes import AuthSchemeType
24+
from .auth_tool import AuthConfig
25+
from .exchanger.base_credential_exchanger import BaseCredentialExchanger
26+
from .exchanger.credential_exchanger_registry import CredentialExchangerRegistry
27+
from .refresher.base_credential_refresher import BaseCredentialRefresher
28+
from .refresher.credential_refresher_registry import CredentialRefresherRegistry
29+
30+
31+
@experimental
32+
class CredentialManager:
33+
"""Manages authentication credentials through a structured workflow.
34+
35+
The CredentialManager orchestrates the complete lifecycle of authentication
36+
credentials, from initial loading to final preparation for use. It provides
37+
a centralized interface for handling various credential types and authentication
38+
schemes while maintaining proper credential hygiene (refresh, exchange, caching).
39+
40+
This class is only for use by Agent Development Kit.
41+
42+
Args:
43+
auth_config: Configuration containing authentication scheme and credentials
44+
45+
Example:
46+
```python
47+
auth_config = AuthConfig(
48+
auth_scheme=oauth2_scheme,
49+
raw_auth_credential=service_account_credential
50+
)
51+
manager = CredentialManager(auth_config)
52+
53+
# Register custom exchanger if needed
54+
manager.register_credential_exchanger(
55+
AuthCredentialTypes.CUSTOM_TYPE,
56+
CustomCredentialExchanger()
57+
)
58+
59+
# Register custom refresher if needed
60+
manager.register_credential_refresher(
61+
AuthCredentialTypes.CUSTOM_TYPE,
62+
CustomCredentialRefresher()
63+
)
64+
65+
# Load and prepare credential
66+
credential = await manager.load_auth_credential(tool_context)
67+
```
68+
"""
69+
70+
def __init__(
71+
self,
72+
auth_config: AuthConfig,
73+
):
74+
self._auth_config = auth_config
75+
self._exchanger_registry = CredentialExchangerRegistry()
76+
self._refresher_registry = CredentialRefresherRegistry()
77+
78+
# Register default exchangers and refreshers
79+
from .exchanger.service_account_credential_exchanger import ServiceAccountCredentialExchanger
80+
81+
self._exchanger_registry.register(
82+
AuthCredentialTypes.SERVICE_ACCOUNT, ServiceAccountCredentialExchanger()
83+
)
84+
from .refresher.oauth2_credential_refresher import OAuth2CredentialRefresher
85+
86+
oauth2_refresher = OAuth2CredentialRefresher()
87+
self._refresher_registry.register(
88+
AuthCredentialTypes.OAUTH2, oauth2_refresher
89+
)
90+
self._refresher_registry.register(
91+
AuthCredentialTypes.OPEN_ID_CONNECT, oauth2_refresher
92+
)
93+
94+
def register_credential_exchanger(
95+
self,
96+
credential_type: AuthCredentialTypes,
97+
exchanger_instance: BaseCredentialExchanger,
98+
) -> None:
99+
"""Register a credential exchanger for a credential type.
100+
101+
Args:
102+
credential_type: The credential type to register for.
103+
exchanger_instance: The exchanger instance to register.
104+
"""
105+
self._exchanger_registry.register(credential_type, exchanger_instance)
106+
107+
async def request_credential(self, tool_context: ToolContext) -> None:
108+
tool_context.request_credential(self._auth_config)
109+
110+
async def get_auth_credential(
111+
self, tool_context: ToolContext
112+
) -> Optional[AuthCredential]:
113+
"""Load and prepare authentication credential through a structured workflow."""
114+
115+
# Step 1: Validate credential configuration
116+
await self._validate_credential()
117+
118+
# Step 2: Check if credential is already ready (no processing needed)
119+
if self._is_credential_ready():
120+
return self._auth_config.raw_auth_credential
121+
122+
# Step 3: Try to load existing processed credential
123+
credential = await self._load_existing_credential(tool_context)
124+
125+
# Step 4: If no existing credential, load from auth response
126+
# TODO instead of load from auth response, we can store auth response in
127+
# credential service.
128+
was_from_auth_response = False
129+
if not credential:
130+
credential = await self._load_from_auth_response(tool_context)
131+
was_from_auth_response = True
132+
133+
# Step 5: If still no credential available, return None
134+
if not credential:
135+
return None
136+
137+
# Step 6: Exchange credential if needed (e.g., service account to access token)
138+
credential, was_exchanged = await self._exchange_credential(credential)
139+
140+
# Step 7: Refresh credential if expired
141+
if not was_exchanged:
142+
credential, was_refreshed = await self._refresh_credential(credential)
143+
144+
# Step 8: Save credential if it was modified
145+
if was_from_auth_response or was_exchanged or was_refreshed:
146+
await self._save_credential(tool_context, credential)
147+
148+
return credential
149+
150+
async def _load_existing_credential(
151+
self, tool_context: ToolContext
152+
) -> Optional[AuthCredential]:
153+
"""Load existing credential from credential service or cached exchanged credential."""
154+
155+
# Try loading from credential service first
156+
credential = await self._load_from_credential_service(tool_context)
157+
if credential:
158+
return credential
159+
160+
# Check if we have a cached exchanged credential
161+
if self._auth_config.exchanged_auth_credential:
162+
return self._auth_config.exchanged_auth_credential
163+
164+
return None
165+
166+
async def _load_from_credential_service(
167+
self, tool_context: ToolContext
168+
) -> Optional[AuthCredential]:
169+
"""Load credential from credential service if available."""
170+
credential_service = tool_context._invocation_context.credential_service
171+
if credential_service:
172+
# Note: This should be made async in a future refactor
173+
# For now, assuming synchronous operation
174+
return await credential_service.load_credential(
175+
self._auth_config, tool_context
176+
)
177+
return None
178+
179+
async def _load_from_auth_response(
180+
self, tool_context: ToolContext
181+
) -> Optional[AuthCredential]:
182+
"""Load credential from auth response in tool context."""
183+
return tool_context.get_auth_response(self._auth_config)
184+
185+
async def _exchange_credential(
186+
self, credential: AuthCredential
187+
) -> tuple[AuthCredential, bool]:
188+
"""Exchange credential if needed and return the credential and whether it was exchanged."""
189+
exchanger = self._exchanger_registry.get_exchanger(credential.auth_type)
190+
if not exchanger:
191+
return credential, False
192+
193+
exchanged_credential = await exchanger.exchange(
194+
credential, self._auth_config.auth_scheme
195+
)
196+
return exchanged_credential, True
197+
198+
async def _refresh_credential(
199+
self, credential: AuthCredential
200+
) -> tuple[AuthCredential, bool]:
201+
"""Refresh credential if expired and return the credential and whether it was refreshed."""
202+
refresher = self._refresher_registry.get_refresher(credential.auth_type)
203+
if not refresher:
204+
return credential, False
205+
206+
if await refresher.is_refresh_needed(
207+
credential, self._auth_config.auth_scheme
208+
):
209+
refreshed_credential = await refresher.refresh(
210+
credential, self._auth_config.auth_scheme
211+
)
212+
return refreshed_credential, True
213+
214+
return credential, False
215+
216+
def _is_credential_ready(self) -> bool:
217+
"""Check if credential is ready to use without further processing."""
218+
raw_credential = self._auth_config.raw_auth_credential
219+
if not raw_credential:
220+
return False
221+
222+
# Simple credentials that don't need exchange or refresh
223+
return raw_credential.auth_type in (
224+
AuthCredentialTypes.API_KEY,
225+
AuthCredentialTypes.HTTP,
226+
# Add other simple auth types as needed
227+
)
228+
229+
async def _validate_credential(self) -> None:
230+
"""Validate credential configuration and raise errors if invalid."""
231+
if not self._auth_config.raw_auth_credential:
232+
if self._auth_config.auth_scheme.type_ in (
233+
AuthSchemeType.oauth2,
234+
AuthSchemeType.openIdConnect,
235+
):
236+
raise ValueError(
237+
"raw_auth_credential is required for auth_scheme type "
238+
f"{self._auth_config.auth_scheme.type_}"
239+
)
240+
241+
raw_credential = self._auth_config.raw_auth_credential
242+
if raw_credential:
243+
if (
244+
raw_credential.auth_type
245+
in (
246+
AuthCredentialTypes.OAUTH2,
247+
AuthCredentialTypes.OPEN_ID_CONNECT,
248+
)
249+
and not raw_credential.oauth2
250+
):
251+
raise ValueError(
252+
"auth_config.raw_credential.oauth2 required for credential type "
253+
f"{raw_credential.auth_type}"
254+
)
255+
# Additional validation can be added here
256+
257+
async def _save_credential(
258+
self, tool_context: ToolContext, credential: AuthCredential
259+
) -> None:
260+
"""Save credential to credential service if available."""
261+
credential_service = tool_context._invocation_context.credential_service
262+
if credential_service:
263+
# Update the exchanged credential in config
264+
self._auth_config.exchanged_auth_credential = credential
265+
await credential_service.save_credential(self._auth_config, tool_context)

0 commit comments

Comments
 (0)