refactor: Move the IamConnectorCredential service depedency to a seperate file

PiperOrigin-RevId: 931088283
This commit is contained in:
Google Team Member
2026-06-12 05:02:14 -07:00
committed by Copybara-Service
parent 57bdecfcb1
commit c423fcd987
6 changed files with 775 additions and 658 deletions
@@ -0,0 +1,272 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import asyncio
import logging
import os
import time
from google.adk.agents.callback_context import CallbackContext
from google.adk.auth.auth_credential import AuthCredential
from google.adk.auth.auth_credential import AuthCredentialTypes
from google.adk.auth.auth_credential import HttpAuth
from google.adk.auth.auth_credential import HttpCredentials
from google.adk.auth.auth_credential import OAuth2Auth
from google.adk.flows.llm_flows.functions import REQUEST_EUC_FUNCTION_CALL_NAME
from google.api_core.client_options import ClientOptions
try:
from google.cloud.iamconnectorcredentials_v1alpha import IAMConnectorCredentialsServiceClient as Client
from google.cloud.iamconnectorcredentials_v1alpha import RetrieveCredentialsMetadata
from google.cloud.iamconnectorcredentials_v1alpha import RetrieveCredentialsRequest
from google.cloud.iamconnectorcredentials_v1alpha import RetrieveCredentialsResponse
except ImportError as e:
raise ImportError(
"Missing required dependencies for Agent Identity Auth Manager. "
'Please install with: pip install "google-adk[agent-identity]"'
) from e
from google.longrunning.operations_pb2 import Operation
from .gcp_auth_provider_scheme import GcpAuthProviderScheme
# Notes on the current IAM Connector Credentials service implementation:
# 1. The service does not yet support LROs, so even though the
# retrieve_credentials method returns an Operation object, the methods like
# operation.done() and operation.result() will not work yet.
# 2. For API key flows, the returned Operation contains the credentials.
# 3. For 2-legged OAuth flows, the returned Operation contains pending status,
# client needs to retry the request until response with credentials is
# returned or timeout occurs.
# 4. For 3-legged OAuth flows, the returned Operation contains consent pending
# status along with the authorization URI.
# TODO: Catch specific exceptions instead of generic ones.
logger = logging.getLogger("google_adk." + __name__)
NON_INTERACTIVE_TOKEN_POLL_INTERVAL_SEC: float = 1.0
NON_INTERACTIVE_TOKEN_POLL_TIMEOUT_SEC: float = 10.0
def _construct_auth_credential(
response: RetrieveCredentialsResponse,
) -> AuthCredential:
"""Constructs a simplified HTTP auth credential from the header-token tuple returned by the upstream service."""
if not response.header or not response.token:
raise ValueError(
"Received either empty header or token from IAM Connector Credentials"
" service."
)
header_name, _, header_value = response.header.partition(":")
if (
header_name.strip().lower() == "authorization"
and header_value.strip().lower().startswith("bearer")
):
return AuthCredential(
auth_type=AuthCredentialTypes.HTTP,
http=HttpAuth(
scheme="Bearer",
credentials=HttpCredentials(token=response.token),
),
)
# Handle custom header.
return AuthCredential(
auth_type=AuthCredentialTypes.HTTP,
http=HttpAuth(
# For custom headers, scheme and credentials fields are not used.
scheme="",
credentials=HttpCredentials(),
additional_headers={
response.header: response.token,
"X-GOOG-API-KEY": response.token,
},
),
)
class _IamConnectorCredentialsProvider:
"""Implementation for auth provider using IAM Connector credentials service."""
_client: Client | None = None
def __init__(self, client: Client | None = None):
self._client = client
def _get_client(self) -> Client:
"""Lazy loads the client to avoid unnecessary setup on startup."""
if self._client is None:
client_options = None
if host := os.environ.get("IAM_CONNECTOR_CREDENTIALS_TARGET_HOST"):
client_options = ClientOptions(api_endpoint=host)
self._client = Client(client_options=client_options, transport="rest")
return self._client
async def _retrieve_credentials(
self,
user_id: str,
auth_scheme: GcpAuthProviderScheme,
) -> Operation:
request = RetrieveCredentialsRequest(
connector=auth_scheme.name,
user_id=user_id,
scopes=auth_scheme.scopes,
continue_uri=auth_scheme.continue_uri or "",
force_refresh=False,
)
# TODO: Use async client once available. Temporarily using threading to
# prevent blocking the event loop.
operation = await asyncio.to_thread(
self._get_client().retrieve_credentials, request
)
return operation.operation
def _unpack_operation(
self, operation: Operation
) -> tuple[
RetrieveCredentialsResponse | None, RetrieveCredentialsMetadata | None
]:
"""Deserializes the response and metadata from the operation."""
response = None
metadata = None
if operation.response:
response = RetrieveCredentialsResponse.deserialize(
operation.response.value
)
if operation.metadata:
metadata = RetrieveCredentialsMetadata.deserialize(
operation.metadata.value
)
return response, metadata
async def _poll_credentials(
self, user_id: str, auth_scheme: GcpAuthProviderScheme, timeout: float
) -> Operation:
end_time = time.time() + timeout
while time.time() < end_time:
operation = await self._retrieve_credentials(user_id, auth_scheme)
if operation.done:
return operation
await asyncio.sleep(NON_INTERACTIVE_TOKEN_POLL_INTERVAL_SEC)
raise TimeoutError("Timeout waiting for credentials.")
@staticmethod
def _is_consent_completed(context: CallbackContext) -> bool:
"""Checks if the user consent flow is completed for the current function call."""
if not context.function_call_id:
return False
if not context.session:
return False
events = context.session.events
target_tool_call_id = context.function_call_id
# Find all relevant function calls and responses
euc_calls = {}
euc_responses = {}
for event in events:
for call in event.get_function_calls():
if call.name == REQUEST_EUC_FUNCTION_CALL_NAME:
euc_calls[call.id] = call
for response in event.get_function_responses():
if response.name == REQUEST_EUC_FUNCTION_CALL_NAME:
euc_responses[response.id] = response
# Check for a response that matches a call for the current tool invocation
for call_id, _ in euc_responses.items():
if call_id in euc_calls:
call = euc_calls[call_id]
if call.args and call.args.get("functionCallId") == target_tool_call_id:
return True
return False
async def get_auth_credential(
self,
auth_scheme: GcpAuthProviderScheme,
context: CallbackContext | None = None,
) -> AuthCredential:
"""Retrieves credentials using the IAM Connector Credentials service.
Args:
auth_scheme: The GcpAuthProviderScheme.
context: Optional context for the callback.
Returns:
An AuthCredential instance.
Raises:
RuntimeError: If credential retrieval or polling fails.
"""
if context is None or context.user_id is None:
raise ValueError(
"GcpAuthProvider requires a context with a valid user_id."
)
user_id = context.user_id
try:
operation = await self._retrieve_credentials(user_id, auth_scheme)
except Exception as e:
raise RuntimeError(
f"Failed to retrieve credential for user '{user_id}' on connector"
f" '{auth_scheme.name}'."
) from e
response, metadata = self._unpack_operation(operation)
if operation.HasField("error"):
raise RuntimeError(f"Operation failed: {operation.error.message}")
if operation.done:
logger.debug("Auth credential obtained immediately.")
return _construct_auth_credential(response)
if metadata and metadata.consent_pending:
# Get 2-legged OAuth token. Allow enough time for token exchange.
try:
operation = await self._poll_credentials(
user_id,
auth_scheme,
timeout=NON_INTERACTIVE_TOKEN_POLL_TIMEOUT_SEC,
)
if operation.HasField("error"):
raise RuntimeError(f"Operation failed: {operation.error.message}")
if operation.done:
logger.debug("Auth credential obtained after polling.")
response, _ = self._unpack_operation(operation)
return _construct_auth_credential(response)
except Exception as e:
raise RuntimeError(
f"Failed to retrieve credential for user '{user_id}' on connector"
f" '{auth_scheme.name}'."
) from e
if metadata is not None and metadata.uri_consent_required:
if self._is_consent_completed(context):
raise RuntimeError("Failed to retrieve consent based credential.")
# Return AuthCredential with only auth_uri to trigger user consent flow.
return AuthCredential(
auth_type=AuthCredentialTypes.OAUTH2,
oauth2=OAuth2Auth(
auth_uri=metadata.uri_consent_required.authorization_uri,
nonce=metadata.uri_consent_required.consent_nonce,
),
)
@@ -12,198 +12,32 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""Authentication provider using Google Cloud Agent Identity Credentials service."""
from __future__ import annotations
import asyncio
import logging
import os
import time
from google.adk.agents.callback_context import CallbackContext
from google.adk.auth.auth_credential import AuthCredential
from google.adk.auth.auth_credential import AuthCredentialTypes
from google.adk.auth.auth_credential import HttpAuth
from google.adk.auth.auth_credential import HttpCredentials
from google.adk.auth.auth_credential import OAuth2Auth
from google.adk.auth.auth_tool import AuthConfig
from google.adk.auth.base_auth_provider import BaseAuthProvider
from google.adk.flows.llm_flows.functions import REQUEST_EUC_FUNCTION_CALL_NAME
from google.api_core.client_options import ClientOptions
try:
from google.cloud.iamconnectorcredentials_v1alpha import IAMConnectorCredentialsServiceClient as Client
from google.cloud.iamconnectorcredentials_v1alpha import RetrieveCredentialsMetadata
from google.cloud.iamconnectorcredentials_v1alpha import RetrieveCredentialsRequest
from google.cloud.iamconnectorcredentials_v1alpha import RetrieveCredentialsResponse
except ImportError as e:
raise ImportError(
"Missing required dependencies for Agent Identity Auth Manager. "
'Please install with: pip install "google-adk[agent-identity]"'
) from e
from google.longrunning.operations_pb2 import Operation
from typing_extensions import override
from ._iam_connector_credentials_provider import _IamConnectorCredentialsProvider
from .gcp_auth_provider_scheme import GcpAuthProviderScheme
# Notes on the current Agent Identity Credentials service implementation:
# 1. The service does not yet support LROs, so even though the
# retrieve_credentials method returns an Operation object, the methods like
# operation.done() and operation.result() will not work yet.
# 2. For API key flows, the returned Operation contains the credentials.
# 3. For 2-legged OAuth flows, the returned Operation contains pending status,
# client needs to retry the request until response with credentials is
# returned or timeout occurs.
# 4. For 3-legged OAuth flows, the returned Operation contains consent pending
# status along with the authorization URI.
# TODO: Catch specific exceptions instead of generic ones.
logger = logging.getLogger("google_adk." + __name__)
NON_INTERACTIVE_TOKEN_POLL_INTERVAL_SEC: float = 1.0
NON_INTERACTIVE_TOKEN_POLL_TIMEOUT_SEC: float = 10.0
def _construct_auth_credential(
response: RetrieveCredentialsResponse,
) -> AuthCredential:
"""Constructs a simplified HTTP auth credential from the header-token tuple returned by the upstream service."""
if not response.header or not response.token:
raise ValueError(
"Received either empty header or token from Agent Identity Credentials"
" service."
)
header_name, _, header_value = response.header.partition(":")
if (
header_name.strip().lower() == "authorization"
and header_value.strip().lower().startswith("bearer")
):
return AuthCredential(
auth_type=AuthCredentialTypes.HTTP,
http=HttpAuth(
scheme="Bearer",
credentials=HttpCredentials(token=response.token),
),
)
# Handle custom header.
return AuthCredential(
auth_type=AuthCredentialTypes.HTTP,
http=HttpAuth(
# For custom headers, scheme and credentials fields are not used.
scheme="",
credentials=HttpCredentials(),
additional_headers={
response.header: response.token,
"X-GOOG-API-KEY": response.token,
},
),
)
class GcpAuthProvider(BaseAuthProvider):
"""An auth provider that uses the Agent Identity Credentials service to generate access tokens."""
_client: Client | None = None
def __init__(self, client: Client | None = None):
self._client = client
def __init__(self):
self._iam_connector_provider = _IamConnectorCredentialsProvider()
@property
@override
def supported_auth_schemes(self) -> tuple[type[GcpAuthProviderScheme], ...]:
return (GcpAuthProviderScheme,)
def _get_client(self) -> Client:
"""Lazy loads the client to avoid unnecessary setup on startup."""
if self._client is None:
client_options = None
if host := os.environ.get("IAM_CONNECTOR_CREDENTIALS_TARGET_HOST"):
client_options = ClientOptions(api_endpoint=host)
self._client = Client(client_options=client_options, transport="rest")
return self._client
async def _retrieve_credentials(
self,
user_id: str,
auth_scheme: GcpAuthProviderScheme,
) -> Operation:
request = RetrieveCredentialsRequest(
connector=auth_scheme.name,
user_id=user_id,
scopes=auth_scheme.scopes,
continue_uri=auth_scheme.continue_uri or "",
force_refresh=False,
)
# TODO: Use async client once available. Temporarily using threading to
# prevent blocking the event loop.
operation = await asyncio.to_thread(
self._get_client().retrieve_credentials, request
)
return operation.operation
def _unpack_operation(
self, operation: Operation
) -> tuple[
RetrieveCredentialsResponse | None, RetrieveCredentialsMetadata | None
]:
"""Deserializes the response and metadata from the operation."""
response = None
metadata = None
if operation.response:
response = RetrieveCredentialsResponse.deserialize(
operation.response.value
)
if operation.metadata:
metadata = RetrieveCredentialsMetadata.deserialize(
operation.metadata.value
)
return response, metadata
async def _poll_credentials(
self, user_id: str, auth_scheme: GcpAuthProviderScheme, timeout: float
) -> Operation:
end_time = time.time() + timeout
while time.time() < end_time:
operation = await self._retrieve_credentials(user_id, auth_scheme)
if operation.done:
return operation
await asyncio.sleep(NON_INTERACTIVE_TOKEN_POLL_INTERVAL_SEC)
raise TimeoutError("Timeout waiting for credentials.")
@staticmethod
def _is_consent_completed(context: CallbackContext) -> bool:
"""Checks if the user consent flow is completed for the current function call."""
if not context.function_call_id:
return False
if not context.session:
return False
events = context.session.events
target_tool_call_id = context.function_call_id
# Find all relevant function calls and responses
euc_calls = {}
euc_responses = {}
for event in events:
for call in event.get_function_calls():
if call.name == REQUEST_EUC_FUNCTION_CALL_NAME:
euc_calls[call.id] = call
for response in event.get_function_responses():
if response.name == REQUEST_EUC_FUNCTION_CALL_NAME:
euc_responses[response.id] = response
# Check for a response that matches a call for the current tool invocation
for call_id, _ in euc_responses.items():
if call_id in euc_calls:
call = euc_calls[call_id]
if call.args and call.args.get("functionCallId") == target_tool_call_id:
return True
return False
@override
async def get_auth_credential(
self,
@@ -221,68 +55,13 @@ class GcpAuthProvider(BaseAuthProvider):
Raises:
ValueError: If auth_scheme is not a GcpAuthProviderScheme.
RuntimeError: If credential retrieval or polling fails.
"""
auth_scheme = auth_config.auth_scheme
if not isinstance(auth_scheme, GcpAuthProviderScheme):
raise ValueError(
f"Expected GcpAuthProviderScheme, got {type(auth_scheme)}"
)
if context is None or context.user_id is None:
raise ValueError(
"GcpAuthProvider requires a context with a valid user_id."
)
user_id = context.user_id
try:
operation = await self._retrieve_credentials(user_id, auth_scheme)
except Exception as e:
raise RuntimeError(
f"Failed to retrieve credential for user '{user_id}' on connector"
f" '{auth_scheme.name}'."
) from e
response, metadata = self._unpack_operation(operation)
if operation.HasField("error"):
raise RuntimeError(f"Operation failed: {operation.error.message}")
if operation.done:
logger.debug("Auth credential obtained immediately.")
return _construct_auth_credential(response)
if metadata and metadata.consent_pending:
# Get 2-legged OAuth token. Allow enough time for token exchange.
try:
operation = await self._poll_credentials(
user_id,
auth_scheme,
timeout=NON_INTERACTIVE_TOKEN_POLL_TIMEOUT_SEC,
)
if operation.HasField("error"):
raise RuntimeError(f"Operation failed: {operation.error.message}")
if operation.done:
logger.debug("Auth credential obtained after polling.")
response, _ = self._unpack_operation(operation)
return _construct_auth_credential(response)
except Exception as e:
raise RuntimeError(
f"Failed to retrieve credential for user '{user_id}' on connector"
f" '{auth_scheme.name}'."
) from e
if metadata is not None and metadata.uri_consent_required:
if self._is_consent_completed(context):
raise RuntimeError("Failed to retrieve consent based credential.")
# Return AuthCredential with only auth_uri to trigger user consent flow.
return AuthCredential(
auth_type=AuthCredentialTypes.OAUTH2,
oauth2=OAuth2Auth(
auth_uri=metadata.uri_consent_required.authorization_uri,
nonce=metadata.uri_consent_required.consent_nonce,
),
)
return await self._iam_connector_provider.get_auth_credential(
auth_scheme=auth_scheme, context=context
)
@@ -22,7 +22,7 @@ from google.adk import Agent
from google.adk import Runner
from google.adk.auth.auth_tool import AuthConfig
from google.adk.auth.credential_manager import CredentialManager
from google.adk.integrations.agent_identity import gcp_auth_provider
from google.adk.integrations.agent_identity import _iam_connector_credentials_provider
from google.adk.integrations.agent_identity import GcpAuthProvider
from google.adk.integrations.agent_identity import GcpAuthProviderScheme
from google.adk.sessions.in_memory_session_service import InMemorySessionService
@@ -103,7 +103,7 @@ async def test_gcp_agent_identity_2lo_gets_token() -> None:
# 1. Setup mocked GCP Client to return the fake Bearer token
with mock.patch.object(
gcp_auth_provider,
_iam_connector_credentials_provider,
"Client",
autospec=True,
) as mock_client_cls:
@@ -209,7 +209,7 @@ async def test_gcp_agent_identity_2lo_sends_authorization_header_to_mcp_session(
mock_operation = _DummyOperation()
with mock.patch.object(
gcp_auth_provider, "Client", autospec=True
_iam_connector_credentials_provider, "Client", autospec=True
) as mock_gcp:
mock_gcp.return_value.retrieve_credentials.return_value = mock_operation
@@ -22,7 +22,7 @@ from google.adk import Agent
from google.adk import Runner
from google.adk.auth.auth_tool import AuthConfig
from google.adk.auth.credential_manager import CredentialManager
from google.adk.integrations.agent_identity import gcp_auth_provider
from google.adk.integrations.agent_identity import _iam_connector_credentials_provider
from google.adk.integrations.agent_identity import GcpAuthProvider
from google.adk.integrations.agent_identity import GcpAuthProviderScheme
from google.adk.sessions.in_memory_session_service import InMemorySessionService
@@ -146,7 +146,7 @@ async def test_gcp_agent_identity_3lo_user_consent_flow() -> None:
mock_gcp_client = MockGcpClient()
with mock.patch.object(
gcp_auth_provider,
_iam_connector_credentials_provider,
"Client",
autospec=True,
) as mock_client_cls:
@@ -12,40 +12,17 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from unittest.mock import AsyncMock
from unittest.mock import Mock
from unittest.mock import patch
import pytest
pytest.importorskip(
"google.cloud.iamconnectorcredentials_v1alpha",
reason="Requires google-cloud-iamconnectorcredentials",
)
from google.adk.agents.callback_context import CallbackContext
from google.adk.auth.auth_credential import AuthCredentialTypes
from google.adk.auth.auth_credential import AuthCredential
from google.adk.auth.auth_tool import AuthConfig
from google.adk.auth.auth_tool import AuthToolArguments
from google.adk.flows.llm_flows.functions import REQUEST_EUC_FUNCTION_CALL_NAME
from google.adk.integrations.agent_identity import gcp_auth_provider
from google.adk.integrations.agent_identity import GcpAuthProvider
from google.adk.integrations.agent_identity import GcpAuthProviderScheme
from google.adk.sessions.session import Session
from google.cloud.iamconnectorcredentials_v1alpha import RetrieveCredentialsMetadata
from google.cloud.iamconnectorcredentials_v1alpha import RetrieveCredentialsResponse
from google.longrunning.operations_pb2 import Operation
from google.protobuf.any_pb2 import Any
from google.rpc.status_pb2 import Status
@pytest.fixture
def mock_client():
return Mock(spec=gcp_auth_provider.Client)
@pytest.fixture
def provider(mock_client):
return GcpAuthProvider(client=mock_client)
from google.adk.integrations.agent_identity._iam_connector_credentials_provider import _IamConnectorCredentialsProvider
import pytest
@pytest.fixture
@@ -58,420 +35,43 @@ def auth_config():
return Mock(spec=AuthConfig, auth_scheme=scheme)
@pytest.fixture
def mock_operation(mocker, mock_client):
op = Operation(done=True)
class DummyCall:
def __init__(self, operation):
self.operation = operation
mock_client.retrieve_credentials.return_value = DummyCall(op)
return op
@pytest.fixture
def context():
context = Mock(spec=CallbackContext)
context.user_id = "user"
context.function_call_id = "call_123"
session = Mock(spec=Session)
session.events = []
context.session = session
return context
@pytest.fixture
def provider():
return GcpAuthProvider()
@patch.dict(gcp_auth_provider.os.environ, clear=True)
@patch.object(gcp_auth_provider, "Client")
def test_get_client_uses_rest_transport(mock_client_class):
def test_supported_auth_schemes(provider):
"""Verify the provider supports the correct auth scheme."""
assert GcpAuthProviderScheme in provider.supported_auth_schemes
@patch("google.adk.integrations.agent_identity.gcp_auth_provider._IamConnectorCredentialsProvider")
async def test_gcp_auth_provider_delegates_get_auth_credential(mock_provider_class, auth_config, context):
"""Test that get_auth_credential delegates to the internal provider."""
provider = GcpAuthProvider()
provider._get_client()
mock_client_class.assert_called_once()
_, kwargs = mock_client_class.call_args
assert kwargs.get("transport") == "rest"
mock_credential = Mock(spec=AuthCredential)
mock_provider_instance = mock_provider_class.return_value
mock_provider_instance.get_auth_credential = AsyncMock(return_value=mock_credential)
result = await provider.get_auth_credential(auth_config, context)
@patch.dict(
gcp_auth_provider.os.environ,
{"IAM_CONNECTOR_CREDENTIALS_TARGET_HOST": "some-host"},
)
@patch.object(gcp_auth_provider, "Client")
@patch.object(gcp_auth_provider, "ClientOptions")
def test_get_client_with_env_var(mock_client_options_class, mock_client_class):
provider = GcpAuthProvider()
client = provider._get_client()
assert client == mock_client_class.return_value
mock_client_options_class.assert_called_once_with(api_endpoint="some-host")
mock_client_class.assert_called_once_with(
client_options=mock_client_options_class.return_value, transport="rest"
assert result == mock_credential
mock_provider_instance.get_auth_credential.assert_awaited_once_with(
auth_scheme=auth_config.auth_scheme, context=context
)
# ==============================================================================
# Non-interactive auth flows (API key and 2-legged OAuth)
# ==============================================================================
async def test_get_auth_credential_raises_error_for_invalid_auth_scheme(
provider, context
):
async def test_get_auth_credential_raises_error_for_invalid_auth_scheme(context):
"""Test get_auth_credential raises ValueError for invalid auth scheme."""
provider = GcpAuthProvider()
invalid_auth_config = Mock(spec=AuthConfig)
invalid_auth_config.auth_scheme = Mock() # Not GcpAuthProviderScheme
with pytest.raises(ValueError, match="Expected GcpAuthProviderScheme, got"):
await provider.get_auth_credential(invalid_auth_config, context)
async def test_get_auth_credential_raises_error_if_context_is_missing(
provider, auth_config
):
"""Test get_auth_credential raises ValueError if context is missing."""
with pytest.raises(
ValueError,
match="GcpAuthProvider requires a context with a valid user_id",
):
await provider.get_auth_credential(auth_config, context=None)
async def test_get_auth_credential_raises_error_if_user_id_is_missing(
provider, auth_config
):
"""Test get_auth_credential raises ValueError if user_id is missing."""
context = Mock(spec=CallbackContext)
context.user_id = None
with pytest.raises(
ValueError,
match="GcpAuthProvider requires a context with a valid user_id",
):
await provider.get_auth_credential(auth_config, context=context)
async def test_get_auth_credential_returns_credential_if_available_immediately(
mock_client,
mock_operation,
auth_config,
context,
provider,
):
"""Test get_auth_credential returns credential if available immediately."""
mock_credential = RetrieveCredentialsResponse(
header="Authorization: Bearer", token="test-token"
)
mock_operation.response.value = RetrieveCredentialsResponse.serialize(
mock_credential
)
auth_credential = await provider.get_auth_credential(auth_config, context)
assert auth_credential.auth_type == AuthCredentialTypes.HTTP
assert auth_credential.http.scheme == "Bearer"
assert auth_credential.http.credentials.token == "test-token"
mock_client.retrieve_credentials.assert_called_once()
async def test_get_auth_credential_raises_error_if_upstream_returns_empty_header(
mock_operation,
auth_config,
context,
provider,
):
"""Test get_auth_credential raises RuntimeError for empty header."""
mock_credential = RetrieveCredentialsResponse(header="", token="test-token")
mock_operation.response.value = RetrieveCredentialsResponse.serialize(
mock_credential
)
with pytest.raises(
ValueError,
match=(
"Received either empty header or token from Agent Identity"
" Credentials service."
),
):
await provider.get_auth_credential(auth_config, context)
async def test_get_auth_credential_raises_error_if_upstream_returns_empty_token(
mock_operation,
auth_config,
context,
provider,
):
"""Test get_auth_credential raises RuntimeError for empty token."""
mock_credential = RetrieveCredentialsResponse(
header="Authorization: Bearer", token=""
)
mock_operation.response.value = RetrieveCredentialsResponse.serialize(
mock_credential
)
with pytest.raises(
ValueError,
match=(
"Received either empty header or token from Agent Identity"
" Credentials service."
),
):
await provider.get_auth_credential(auth_config, context)
async def test_get_auth_credential_returns_credential_if_upstream_returns_custom_header(
mock_operation,
auth_config,
context,
provider,
):
"""Test get_auth_credential returns valid credential for custom header and sets X-GOOG-API-KEY header."""
mock_credential = RetrieveCredentialsResponse(
header="some-x-api-key", token="test-token"
)
mock_operation.response.value = RetrieveCredentialsResponse.serialize(
mock_credential
)
auth_credential = await provider.get_auth_credential(auth_config, context)
assert auth_credential.auth_type == AuthCredentialTypes.HTTP
assert not auth_credential.http.scheme
assert auth_credential.http.credentials.token is None
assert auth_credential.http.additional_headers == {
"some-x-api-key": "test-token",
"X-GOOG-API-KEY": "test-token",
}
async def test_get_auth_credential_raises_error_if_upstream_operation_errors(
mock_operation, auth_config, context, provider
):
"""Test get_auth_credential raises RuntimeError for failed operations."""
mock_operation.error.message = "OAuth server error"
mock_operation.done = False
with pytest.raises(
RuntimeError, match="Operation failed: OAuth server error"
):
await provider.get_auth_credential(auth_config, context)
async def test_get_auth_credential_raises_error_if_upstream_call_fails(
mock_client, auth_config, context, provider
):
"""Test get_auth_credential raises RuntimeError for failed calls."""
mock_client.retrieve_credentials.side_effect = Exception(
"API Quota Exhausted"
)
with pytest.raises(
RuntimeError,
match="Failed to retrieve credential for user 'user' on connector",
) as exc_info:
await provider.get_auth_credential(auth_config, context)
# Assert that the original Exception is the chained cause!
assert str(exc_info.value.__cause__) == "API Quota Exhausted"
@patch.object(gcp_auth_provider.time, "time")
async def test_get_auth_credential_raises_error_if_polling_times_out(
mock_time,
mock_operation,
auth_config,
context,
provider,
):
"""Test get_auth_credential raises RuntimeError if polling times out."""
# Force the operation into the polling loop state
meta_pb = RetrieveCredentialsMetadata.pb()()
meta_pb.consent_pending.SetInParent()
meta = RetrieveCredentialsMetadata.deserialize(meta_pb.SerializeToString())
mock_operation.metadata.value = RetrieveCredentialsMetadata.serialize(meta)
# First call sets start_time=0.0, second call checks time > timeout
# (20.0 > 10.0)
mock_time.side_effect = [0.0, 20.0]
mock_metadata = Mock(spec=RetrieveCredentialsMetadata)
mock_metadata.consent_pending = True
mock_metadata.uri_consent_required = False
mock_operation.done = True
mock_operation.ClearField("error")
mock_client = Mock(spec=gcp_auth_provider.Client)
mock_client.retrieve_credentials.side_effect = Exception(
"Timeout waiting for credentials."
)
provider._client = mock_client
with pytest.raises(
RuntimeError,
match="Failed to retrieve credential for user 'user' on connector",
) as exc_info:
await provider.get_auth_credential(auth_config, context)
assert "Timeout waiting for credentials." in str(exc_info.value.__cause__)
# ==============================================================================
# Interactive Auth Flows (3-legged OAuth for User Consents)
# ==============================================================================
async def test_get_auth_credential_initiates_user_consent(
mock_operation, auth_config, context, provider
):
# Explicitly set the mock behavior for this test
expected_uri = "https://example.com/auth"
expected_nonce = "sample-nonce-123"
meta = RetrieveCredentialsMetadata({
"uri_consent_required": {
"authorization_uri": expected_uri,
"consent_nonce": expected_nonce,
}
})
mock_operation.metadata.value = RetrieveCredentialsMetadata.serialize(meta)
mock_operation.done = False
# Assert that there is no prior user consent completion event
assert not context.session.events
credential = await provider.get_auth_credential(auth_config, context)
assert credential is not None
assert credential.auth_type == AuthCredentialTypes.OAUTH2
assert credential.oauth2.auth_uri == expected_uri
assert credential.oauth2.nonce == expected_nonce
async def test_get_auth_credential_returns_fresh_auth_uri_for_repeated_requests(
mock_client, mock_operation, auth_config, context, provider
):
"""Test that repeated calls fetch fresh auth URIs if consent is still pending."""
# Arrange: Explicit initial URI
initial_uri = "https://example.com/auth"
initial_nonce = "initial-nonce-123"
meta1 = RetrieveCredentialsMetadata({
"uri_consent_required": {
"authorization_uri": initial_uri,
"consent_nonce": initial_nonce,
}
})
mock_operation.metadata.value = RetrieveCredentialsMetadata.serialize(meta1)
mock_operation.done = False
credential1 = await provider.get_auth_credential(auth_config, context)
assert credential1.oauth2.auth_uri == initial_uri
assert credential1.oauth2.nonce == initial_nonce
# Arrange: Explicit new URI for the second call
fresh_auth_uri = "https://example.com/auth_new"
fresh_nonce = "fresh-nonce-456"
meta2 = RetrieveCredentialsMetadata({
"uri_consent_required": {
"authorization_uri": fresh_auth_uri,
"consent_nonce": fresh_nonce,
}
})
mock_operation.metadata.value = RetrieveCredentialsMetadata.serialize(meta2)
credential2 = await provider.get_auth_credential(auth_config, context)
assert mock_client.retrieve_credentials.call_count == 2
assert credential2.oauth2.auth_uri == fresh_auth_uri
assert credential2.oauth2.nonce == fresh_nonce
async def test_get_auth_credential_returns_token_if_consent_was_completed(
mock_operation, auth_config, context, provider
):
# Setup mock credential for successful credential retrieval
mock_credential = RetrieveCredentialsResponse(
header="Authorization: Bearer", token="test-token"
)
mock_operation.response.value = RetrieveCredentialsResponse.serialize(
mock_credential
)
# Create mock events
# 1. FunctionCall event for adk_request_credential
function_call = Mock()
function_call.id = "auth-req-1"
function_call.name = REQUEST_EUC_FUNCTION_CALL_NAME
function_call.args = AuthToolArguments(
function_call_id="call-123", auth_config=auth_config
).model_dump(by_alias=True, exclude_none=True)
event1 = Mock()
event1.get_function_calls.return_value = [function_call]
event1.get_function_responses.return_value = []
# 2. FunctionResponse event for adk_request_credential
function_response = Mock()
function_response.id = "auth-req-1"
function_response.name = REQUEST_EUC_FUNCTION_CALL_NAME
event2 = Mock()
event2.get_function_calls.return_value = []
event2.get_function_responses.return_value = [function_response]
# Setup tool context and event history (order of events matters)
context.session.events = [event1, event2]
context.function_call_id = "call-123"
# Also set uri_consent_required to True-ish so it enters the check block
meta = RetrieveCredentialsMetadata(
uri_consent_required=RetrieveCredentialsMetadata.UriConsentRequired()
)
mock_operation.metadata.value = RetrieveCredentialsMetadata.serialize(meta)
# Execute
auth_credential = await provider.get_auth_credential(auth_config, context)
# Verify
assert auth_credential is not None
assert auth_credential.auth_type == AuthCredentialTypes.HTTP
assert auth_credential.http.scheme == "Bearer"
assert auth_credential.http.credentials.token == "test-token"
async def test_get_auth_credential_raises_error_if_consent_canceled(
mock_operation, auth_config, context, provider
):
function_call = Mock()
function_call.id = "auth-req-1"
function_call.name = REQUEST_EUC_FUNCTION_CALL_NAME
function_call.args = AuthToolArguments(
function_call_id="call-123", auth_config=auth_config
).model_dump(by_alias=True, exclude_none=True)
event1 = Mock()
event1.get_function_calls.return_value = [function_call]
event1.get_function_responses.return_value = []
function_response = Mock()
function_response.id = "auth-req-1"
function_response.name = REQUEST_EUC_FUNCTION_CALL_NAME
event2 = Mock()
event2.get_function_calls.return_value = []
event2.get_function_responses.return_value = [function_response]
context.session.events = [event1, event2]
context.function_call_id = "call-123"
meta = RetrieveCredentialsMetadata({
"uri_consent_required": {
"authorization_uri": "https://example.com/auth",
"consent_nonce": "sample-nonce",
}
})
mock_operation.metadata.value = RetrieveCredentialsMetadata.serialize(meta)
mock_operation.done = False
with pytest.raises(
RuntimeError, match="Failed to retrieve consent based credential."
):
await provider.get_auth_credential(auth_config, context)
@@ -0,0 +1,466 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from unittest.mock import Mock
from unittest.mock import patch
import pytest
pytest.importorskip(
"google.cloud.iamconnectorcredentials_v1alpha",
reason="Requires google-cloud-iamconnectorcredentials",
)
from google.adk.agents.callback_context import CallbackContext
from google.adk.auth.auth_credential import AuthCredentialTypes
from google.adk.auth.auth_tool import AuthConfig
from google.adk.auth.auth_tool import AuthToolArguments
from google.adk.flows.llm_flows.functions import REQUEST_EUC_FUNCTION_CALL_NAME
from google.adk.integrations.agent_identity import _iam_connector_credentials_provider
from google.adk.integrations.agent_identity import GcpAuthProviderScheme
from google.adk.integrations.agent_identity._iam_connector_credentials_provider import _IamConnectorCredentialsProvider
from google.adk.integrations.agent_identity._iam_connector_credentials_provider import Client
from google.adk.sessions.session import Session
from google.cloud.iamconnectorcredentials_v1alpha import RetrieveCredentialsMetadata
from google.cloud.iamconnectorcredentials_v1alpha import RetrieveCredentialsResponse
from google.longrunning.operations_pb2 import Operation
@pytest.fixture
def mock_client():
return Mock(spec=Client)
@pytest.fixture
def provider(mock_client):
return _IamConnectorCredentialsProvider(client=mock_client)
@pytest.fixture
def auth_scheme():
scheme = GcpAuthProviderScheme(
name="projects/test-project/locations/global/connectors/test-connector",
scopes=["test-scope"],
continue_uri="https://example.com/continue",
)
return scheme
@pytest.fixture
def mock_operation(mock_client):
op = Operation(done=True)
class DummyCall:
def __init__(self, operation):
self.operation = operation
mock_client.retrieve_credentials.return_value = DummyCall(op)
return op
@pytest.fixture
def context():
context = Mock(spec=CallbackContext)
context.user_id = "user"
context.function_call_id = "call_123"
session = Mock(spec=Session)
session.events = []
context.session = session
return context
@patch.dict(_iam_connector_credentials_provider.os.environ, clear=True)
@patch.object(_iam_connector_credentials_provider, "Client")
def test_get_client_uses_rest_transport(mock_client_class):
provider = _iam_connector_credentials_provider._IamConnectorCredentialsProvider()
provider._get_client()
mock_client_class.assert_called_once()
_, kwargs = mock_client_class.call_args
assert kwargs.get("transport") == "rest"
@patch.dict(
_iam_connector_credentials_provider.os.environ,
{"IAM_CONNECTOR_CREDENTIALS_TARGET_HOST": "some-host"},
)
@patch.object(_iam_connector_credentials_provider, "Client")
@patch.object(_iam_connector_credentials_provider, "ClientOptions")
def test_get_client_with_env_var(mock_client_options_class, mock_client_class):
provider = _iam_connector_credentials_provider._IamConnectorCredentialsProvider()
client = provider._get_client()
assert client == mock_client_class.return_value
mock_client_options_class.assert_called_once_with(api_endpoint="some-host")
mock_client_class.assert_called_once_with(
client_options=mock_client_options_class.return_value, transport="rest"
)
# ==============================================================================
# Non-interactive auth flows (API key and 2-legged OAuth)
# ==============================================================================
async def test_get_auth_credential_raises_error_if_context_is_missing(
provider, auth_scheme
):
"""Test get_auth_credential raises ValueError if context is missing."""
with pytest.raises(
ValueError,
match="GcpAuthProvider requires a context with a valid user_id",
):
await provider.get_auth_credential(auth_scheme, context=None)
async def test_get_auth_credential_raises_error_if_user_id_is_missing(
provider, auth_scheme
):
"""Test get_auth_credential raises ValueError if user_id is missing."""
context = Mock(spec=CallbackContext)
context.user_id = None
with pytest.raises(
ValueError,
match="GcpAuthProvider requires a context with a valid user_id",
):
await provider.get_auth_credential(auth_scheme, context=context)
async def test_get_auth_credential_returns_credential_if_available_immediately(
mock_client,
mock_operation,
auth_scheme,
context,
provider,
):
"""Test get_auth_credential returns credential if available immediately."""
mock_credential = RetrieveCredentialsResponse(
header="Authorization: Bearer", token="test-token"
)
mock_operation.response.value = RetrieveCredentialsResponse.serialize(
mock_credential
)
auth_credential = await provider.get_auth_credential(auth_scheme, context)
assert auth_credential.auth_type == AuthCredentialTypes.HTTP
assert auth_credential.http.scheme == "Bearer"
assert auth_credential.http.credentials.token == "test-token"
mock_client.retrieve_credentials.assert_called_once()
async def test_get_auth_credential_raises_error_if_upstream_returns_empty_header(
mock_operation,
auth_scheme,
context,
provider,
):
"""Test get_auth_credential raises RuntimeError for empty header."""
mock_credential = RetrieveCredentialsResponse(header="", token="test-token")
mock_operation.response.value = RetrieveCredentialsResponse.serialize(
mock_credential
)
with pytest.raises(
ValueError,
match=(
"Received either empty header or token from IAM Connector"
" Credentials service."
),
):
await provider.get_auth_credential(auth_scheme, context)
async def test_get_auth_credential_raises_error_if_upstream_returns_empty_token(
mock_operation,
auth_scheme,
context,
provider,
):
"""Test get_auth_credential raises RuntimeError for empty token."""
mock_credential = RetrieveCredentialsResponse(
header="Authorization: Bearer", token=""
)
mock_operation.response.value = RetrieveCredentialsResponse.serialize(
mock_credential
)
with pytest.raises(
ValueError,
match=(
"Received either empty header or token from IAM Connector"
" Credentials service."
),
):
await provider.get_auth_credential(auth_scheme, context)
async def test_get_auth_credential_returns_credential_if_upstream_returns_custom_header(
mock_operation,
auth_scheme,
context,
provider,
):
"""Test get_auth_credential returns valid credential for custom header and sets X-GOOG-API-KEY header."""
mock_credential = RetrieveCredentialsResponse(
header="some-x-api-key", token="test-token"
)
mock_operation.response.value = RetrieveCredentialsResponse.serialize(
mock_credential
)
auth_credential = await provider.get_auth_credential(auth_scheme, context)
assert auth_credential.auth_type == AuthCredentialTypes.HTTP
assert not auth_credential.http.scheme
assert auth_credential.http.credentials.token is None
assert auth_credential.http.additional_headers == {
"some-x-api-key": "test-token",
"X-GOOG-API-KEY": "test-token",
}
async def test_get_auth_credential_raises_error_if_upstream_operation_errors(
mock_operation, auth_scheme, context, provider
):
"""Test get_auth_credential raises RuntimeError for failed operations."""
mock_operation.error.message = "OAuth server error"
mock_operation.done = False
with pytest.raises(
RuntimeError, match="Operation failed: OAuth server error"
):
await provider.get_auth_credential(auth_scheme, context)
async def test_get_auth_credential_raises_error_if_upstream_call_fails(
mock_client, auth_scheme, context, provider
):
"""Test get_auth_credential raises RuntimeError for failed calls."""
mock_client.retrieve_credentials.side_effect = Exception(
"API Quota Exhausted"
)
with pytest.raises(
RuntimeError,
match="Failed to retrieve credential for user 'user' on connector",
) as exc_info:
await provider.get_auth_credential(auth_scheme, context)
# Assert that the original Exception is the chained cause!
assert str(exc_info.value.__cause__) == "API Quota Exhausted"
@patch.object(_iam_connector_credentials_provider.time, "time")
async def test_get_auth_credential_raises_error_if_polling_times_out(
mock_time,
mock_operation,
auth_scheme,
context,
provider,
):
"""Test get_auth_credential raises RuntimeError if polling times out."""
# Force the operation into the polling loop state
meta_pb = RetrieveCredentialsMetadata.pb()()
meta_pb.consent_pending.SetInParent()
meta = RetrieveCredentialsMetadata.deserialize(meta_pb.SerializeToString())
mock_operation.metadata.value = RetrieveCredentialsMetadata.serialize(meta)
# First call sets start_time=0.0, second call checks time > timeout
# (20.0 > 10.0)
mock_time.side_effect = [0.0, 20.0]
mock_metadata = Mock(spec=RetrieveCredentialsMetadata)
mock_metadata.consent_pending = True
mock_metadata.uri_consent_required = False
mock_operation.done = True
mock_operation.ClearField("error")
mock_client = Mock(spec=Client)
mock_client.retrieve_credentials.side_effect = Exception(
"Timeout waiting for credentials."
)
provider._client = mock_client
with pytest.raises(
RuntimeError,
match="Failed to retrieve credential for user 'user' on connector",
) as exc_info:
await provider.get_auth_credential(auth_scheme, context)
assert "Timeout waiting for credentials." in str(exc_info.value.__cause__)
# ==============================================================================
# Interactive Auth Flows (3-legged OAuth for User Consents)
# ==============================================================================
async def test_get_auth_credential_initiates_user_consent(
mock_operation, auth_scheme, context, provider
):
# Explicitly set the mock behavior for this test
expected_uri = "https://example.com/auth"
expected_nonce = "sample-nonce-123"
meta = RetrieveCredentialsMetadata({
"uri_consent_required": {
"authorization_uri": expected_uri,
"consent_nonce": expected_nonce,
}
})
mock_operation.metadata.value = RetrieveCredentialsMetadata.serialize(meta)
mock_operation.done = False
# Assert that there is no prior user consent completion event
assert not context.session.events
credential = await provider.get_auth_credential(auth_scheme, context)
assert credential is not None
assert credential.auth_type == AuthCredentialTypes.OAUTH2
assert credential.oauth2.auth_uri == expected_uri
assert credential.oauth2.nonce == expected_nonce
async def test_get_auth_credential_returns_fresh_auth_uri_for_repeated_requests(
mock_client, mock_operation, auth_scheme, context, provider
):
"""Test that repeated calls fetch fresh auth URIs if consent is still pending."""
# Arrange: Explicit initial URI
initial_uri = "https://example.com/auth"
initial_nonce = "initial-nonce-123"
meta1 = RetrieveCredentialsMetadata({
"uri_consent_required": {
"authorization_uri": initial_uri,
"consent_nonce": initial_nonce,
}
})
mock_operation.metadata.value = RetrieveCredentialsMetadata.serialize(meta1)
mock_operation.done = False
credential1 = await provider.get_auth_credential(auth_scheme, context)
assert credential1.oauth2.auth_uri == initial_uri
assert credential1.oauth2.nonce == initial_nonce
# Arrange: Explicit new URI for the second call
fresh_auth_uri = "https://example.com/auth_new"
fresh_nonce = "fresh-nonce-456"
meta2 = RetrieveCredentialsMetadata({
"uri_consent_required": {
"authorization_uri": fresh_auth_uri,
"consent_nonce": fresh_nonce,
}
})
mock_operation.metadata.value = RetrieveCredentialsMetadata.serialize(meta2)
credential2 = await provider.get_auth_credential(auth_scheme, context)
assert mock_client.retrieve_credentials.call_count == 2
assert credential2.oauth2.auth_uri == fresh_auth_uri
assert credential2.oauth2.nonce == fresh_nonce
async def test_get_auth_credential_returns_token_if_consent_was_completed(
mock_operation, auth_scheme, context, provider
):
# Setup mock credential for successful credential retrieval
mock_credential = RetrieveCredentialsResponse(
header="Authorization: Bearer", token="test-token"
)
mock_operation.response.value = RetrieveCredentialsResponse.serialize(
mock_credential
)
# Create mock events
# 1. FunctionCall event for adk_request_credential
function_call = Mock()
function_call.id = "auth-req-1"
function_call.name = REQUEST_EUC_FUNCTION_CALL_NAME
function_call.args = AuthToolArguments(
function_call_id="call-123", auth_config=Mock(spec=AuthConfig, auth_scheme=auth_scheme)
).model_dump(by_alias=True, exclude_none=True)
event1 = Mock()
event1.get_function_calls.return_value = [function_call]
event1.get_function_responses.return_value = []
# 2. FunctionResponse event for adk_request_credential
function_response = Mock()
function_response.id = "auth-req-1"
function_response.name = REQUEST_EUC_FUNCTION_CALL_NAME
event2 = Mock()
event2.get_function_calls.return_value = []
event2.get_function_responses.return_value = [function_response]
# Setup tool context and event history (order of events matters)
context.session.events = [event1, event2]
context.function_call_id = "call-123"
# Also set uri_consent_required to True-ish so it enters the check block
meta = RetrieveCredentialsMetadata(
uri_consent_required=RetrieveCredentialsMetadata.UriConsentRequired()
)
mock_operation.metadata.value = RetrieveCredentialsMetadata.serialize(meta)
# Execute
auth_credential = await provider.get_auth_credential(auth_scheme, context)
# Verify
assert auth_credential is not None
assert auth_credential.auth_type == AuthCredentialTypes.HTTP
assert auth_credential.http.scheme == "Bearer"
assert auth_credential.http.credentials.token == "test-token"
async def test_get_auth_credential_raises_error_if_consent_canceled(
mock_operation, auth_scheme, context, provider
):
function_call = Mock()
function_call.id = "auth-req-1"
function_call.name = REQUEST_EUC_FUNCTION_CALL_NAME
function_call.args = AuthToolArguments(
function_call_id="call-123", auth_config=Mock(spec=AuthConfig, auth_scheme=auth_scheme)
).model_dump(by_alias=True, exclude_none=True)
event1 = Mock()
event1.get_function_calls.return_value = [function_call]
event1.get_function_responses.return_value = []
function_response = Mock()
function_response.id = "auth-req-1"
function_response.name = REQUEST_EUC_FUNCTION_CALL_NAME
event2 = Mock()
event2.get_function_calls.return_value = []
event2.get_function_responses.return_value = [function_response]
context.session.events = [event1, event2]
context.function_call_id = "call-123"
meta = RetrieveCredentialsMetadata({
"uri_consent_required": {
"authorization_uri": "https://example.com/auth",
"consent_nonce": "sample-nonce",
}
})
mock_operation.metadata.value = RetrieveCredentialsMetadata.serialize(meta)
mock_operation.done = False
with pytest.raises(
RuntimeError, match="Failed to retrieve consent based credential."
):
await provider.get_auth_credential(auth_scheme, context)