Skip to content

refactor: Refactor oauth2_credential_exchanger to exchanger and refresher separately #1418

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions src/google/adk/auth/auth_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from .auth_schemes import AuthSchemeType
from .auth_schemes import OpenIdConnectWithConfig
from .auth_tool import AuthConfig
from .oauth2_credential_fetcher import OAuth2CredentialFetcher
from .exchanger.oauth2_credential_exchanger import OAuth2CredentialExchanger

if TYPE_CHECKING:
from ..sessions.state import State
Expand All @@ -36,18 +36,23 @@


class AuthHandler:
"""A handler that handles the auth flow in Agent Development Kit to help
orchestrate the credential request and response flow (e.g. OAuth flow)
This class should only be used by Agent Development Kit.
"""

def __init__(self, auth_config: AuthConfig):
self.auth_config = auth_config

def exchange_auth_token(
async def exchange_auth_token(
self,
) -> AuthCredential:
return OAuth2CredentialFetcher(
self.auth_config.auth_scheme, self.auth_config.exchanged_auth_credential
).exchange()
exchanger = OAuth2CredentialExchanger()
return await exchanger.exchange(
self.auth_config.exchanged_auth_credential, self.auth_config.auth_scheme
)

def parse_and_store_auth_response(self, state: State) -> None:
async def parse_and_store_auth_response(self, state: State) -> None:

credential_key = "temp:" + self.auth_config.credential_key

Expand All @@ -60,7 +65,7 @@ def parse_and_store_auth_response(self, state: State) -> None:
):
return

state[credential_key] = self.exchange_auth_token()
state[credential_key] = await self.exchange_auth_token()

def _validate(self) -> None:
if not self.auth_scheme:
Expand Down
6 changes: 3 additions & 3 deletions src/google/adk/auth/auth_preprocessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ async def run_async(
# function call
request_euc_function_call_ids.add(function_call_response.id)
auth_config = AuthConfig.model_validate(function_call_response.response)
AuthHandler(auth_config=auth_config).parse_and_store_auth_response(
state=invocation_context.session.state
)
await AuthHandler(
auth_config=auth_config
).parse_and_store_auth_response(state=invocation_context.session.state)
break

if not request_euc_function_call_ids:
Expand Down
104 changes: 104 additions & 0 deletions src/google/adk/auth/exchanger/oauth2_credential_exchanger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Copyright 2025 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.

"""OAuth2 credential exchanger implementation."""

from __future__ import annotations

import logging
from typing import Optional

from google.adk.auth.auth_credential import AuthCredential
from google.adk.auth.auth_schemes import AuthScheme
from google.adk.auth.auth_schemes import OAuthGrantType
from google.adk.auth.oauth2_credential_util import create_oauth2_session
from google.adk.auth.oauth2_credential_util import update_credential_with_tokens
from google.adk.utils.feature_decorator import experimental
from typing_extensions import override

from .base_credential_exchanger import BaseCredentialExchanger
from .base_credential_exchanger import CredentialExchangError

try:
from authlib.integrations.requests_client import OAuth2Session

AUTHLIB_AVIALABLE = True
except ImportError:
AUTHLIB_AVIALABLE = False

logger = logging.getLogger("google_adk." + __name__)


@experimental
class OAuth2CredentialExchanger(BaseCredentialExchanger):
"""Exchanges OAuth2 credentials from authorization responses."""

@override
async def exchange(
self,
auth_credential: AuthCredential,
auth_scheme: Optional[AuthScheme] = None,
) -> AuthCredential:
"""Exchange OAuth2 credential from authorization response.
if credential exchange failed, the original credential will be returned.

Args:
auth_credential: The OAuth2 credential to exchange.
auth_scheme: The OAuth2 authentication scheme.

Returns:
The exchanged credential with access token.

Raises:
CredentialExchangError: If auth_scheme is missing.
"""
if not auth_scheme:
raise CredentialExchangError(
"auth_scheme is required for OAuth2 credential exchange"
)

if not AUTHLIB_AVIALABLE:
# If authlib is not available, we cannot exchange the credential.
# We return the original credential without exchange.
# The client using this tool can decide to exchange the credential
# themselves using other lib.
logger.warning(
"authlib is not available, skipping OAuth2 credential exchange."
)
return auth_credential

if auth_credential.oauth2 and auth_credential.oauth2.access_token:
return auth_credential

client, token_endpoint = create_oauth2_session(auth_scheme, auth_credential)
if not client:
logger.warning("Could not create OAuth2 session for token exchange")
return auth_credential

try:
tokens = client.fetch_token(
token_endpoint,
authorization_response=auth_credential.oauth2.auth_response_uri,
code=auth_credential.oauth2.auth_code,
grant_type=OAuthGrantType.AUTHORIZATION_CODE,
)
update_credential_with_tokens(auth_credential, tokens)
logger.debug("Successfully exchanged OAuth2 tokens")
except Exception as e:
# TODO reconsider whether we should raise errors in this case
logger.error("Failed to exchange OAuth2 tokens: %s", e)
# Return original credential on failure
return auth_credential

return auth_credential
132 changes: 0 additions & 132 deletions src/google/adk/auth/oauth2_credential_fetcher.py

This file was deleted.

Loading