feat: add token_endpoint_auth_method support to OAuth2 credentials

Merge https://github.com/google/adk-python/pull/2870

## Summary

  Add `token_endpoint_auth_method` field to OAuth2Auth class to allow configuring OAuth2 token endpoint authentication methods. This enables users to specify how the client should authenticate with the authorization server's token
  endpoint.

  • Add `token_endpoint_auth_method` field to `OAuth2Auth` with default value `"client_secret_basic"`
  • Update `create_oauth2_session()` to pass the authentication method to `OAuth2Session`
  • Maintain backward compatibility with existing OAuth2 configurations

  ## Unit Tests
  Added unit test coverage with 3 new test methods:

  1. `test_create_oauth2_session_with_token_endpoint_auth_method()` - Tests explicit auth method setting (`client_secret_post`)
  2. `test_create_oauth2_session_with_default_token_endpoint_auth_method()` - Tests default behavior (`client_secret_basic`)
  3. `test_create_oauth2_session_oauth2_scheme_with_token_endpoint_auth_method()` - Tests with OAuth2 scheme using `client_secret_jwt`

  **Test Results:**
   16/16 OAuth2 credential utility tests passed
   240/240 auth module tests passed (no regressions)
   Tests cover both GOOGLE_AI and VERTEX variants
   Pylint score: 9.41/10

  ## Changes Made

  **src/google/adk/auth/auth_credential.py**
  - Added `token_endpoint_auth_method: Optional[str] = "client_secret_basic"` to `OAuth2Auth` class

  **src/google/adk/auth/oauth2_credential_util.py**
  - Updated `create_oauth2_session()` to pass `token_endpoint_auth_method` parameter to `OAuth2Session`

  **tests/unittests/auth/test_oauth2_credential_util.py**
  - Added 3 comprehensive test methods covering different authentication scenarios

  ## Backward Compatibility

   **Non-breaking change** - All existing OAuth2 configurations continue to work unchanged with the default `client_secret_basic` authentication method.

  ## Supported Authentication Methods

  - `client_secret_basic` (default) - Client credentials in Authorization header
  - `client_secret_post` - Client credentials in request body
  - `client_secret_jwt` - JWT with client secret
  - `private_key_jwt` - JWT with private key

Co-authored-by: Xiang (Sean) Zhou <seanzhougoogle@google.com>
COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/2870 from sully90:feat/oauth2-token-endpoint-auth-method 04fe8244598f96b4e3366f0fc79382628382e9c2
PiperOrigin-RevId: 843739984
This commit is contained in:
David Sullivan
2025-12-12 10:16:36 -08:00
committed by Copybara-Service
parent 29c1115959
commit 8782a69503
3 changed files with 103 additions and 8 deletions
@@ -13,6 +13,7 @@
# limitations under the License.
import time
from typing import Optional
from unittest.mock import Mock
from authlib.oauth2.rfc6749 import OAuth2Token
@@ -25,6 +26,39 @@ from google.adk.auth.auth_credential import OAuth2Auth
from google.adk.auth.auth_schemes import OpenIdConnectWithConfig
from google.adk.auth.oauth2_credential_util import create_oauth2_session
from google.adk.auth.oauth2_credential_util import update_credential_with_tokens
import pytest
@pytest.fixture
def openid_connect_scheme() -> OpenIdConnectWithConfig:
"""Fixture providing a standard OpenIdConnectWithConfig scheme."""
return OpenIdConnectWithConfig(
type_="openIdConnect",
openId_connect_url="https://example.com/.well-known/openid_configuration",
authorization_endpoint="https://example.com/auth",
token_endpoint="https://example.com/token",
scopes=["openid", "profile"],
)
def create_oauth2_auth_credential(
auth_type=AuthCredentialTypes.OPEN_ID_CONNECT,
token_endpoint_auth_method: Optional[str] = None,
):
"""Helper function to create OAuth2Auth credential with optional token_endpoint_auth_method."""
oauth2_auth = OAuth2Auth(
client_id="test_client_id",
client_secret="test_client_secret",
redirect_uri="https://example.com/callback",
state="test_state",
)
if token_endpoint_auth_method is not None:
oauth2_auth.token_endpoint_auth_method = token_endpoint_auth_method
return AuthCredential(
auth_type=auth_type,
oauth2=oauth2_auth,
)
class TestOAuth2CredentialUtil:
@@ -41,14 +75,9 @@ class TestOAuth2CredentialUtil:
token_endpoint="https://example.com/token",
scopes=["openid", "profile"],
)
credential = AuthCredential(
auth_type=AuthCredentialTypes.OPEN_ID_CONNECT,
oauth2=OAuth2Auth(
client_id="test_client_id",
client_secret="test_client_secret",
redirect_uri="https://example.com/callback",
state="test_state",
),
credential = create_oauth2_auth_credential(
auth_type=AuthCredentialTypes.OAUTH2,
token_endpoint_auth_method="client_secret_jwt",
)
client, token_endpoint = create_oauth2_session(scheme, credential)
@@ -122,6 +151,62 @@ class TestOAuth2CredentialUtil:
assert client is None
assert token_endpoint is None
@pytest.mark.parametrize(
"token_endpoint_auth_method, expected_auth_method",
[
("client_secret_post", "client_secret_post"),
(None, "client_secret_basic"),
],
)
def test_create_oauth2_session_with_token_endpoint_auth_method(
self,
openid_connect_scheme,
token_endpoint_auth_method,
expected_auth_method,
):
"""Test create_oauth2_session with various token_endpoint_auth_method settings."""
credential = create_oauth2_auth_credential(
token_endpoint_auth_method=token_endpoint_auth_method
)
client, token_endpoint = create_oauth2_session(
openid_connect_scheme, credential
)
assert client is not None
assert token_endpoint == "https://example.com/token"
assert client.client_id == "test_client_id"
assert client.client_secret == "test_client_secret"
assert client.token_endpoint_auth_method == expected_auth_method
def test_create_oauth2_session_oauth2_scheme_with_token_endpoint_auth_method(
self,
):
"""Test create_oauth2_session with OAuth2 scheme and token_endpoint_auth_method."""
flows = OAuthFlows(
authorizationCode=OAuthFlowAuthorizationCode(
authorizationUrl="https://example.com/auth",
tokenUrl="https://example.com/token",
scopes={"read": "Read access", "write": "Write access"},
)
)
scheme = OAuth2(type_="oauth2", flows=flows)
credential = AuthCredential(
auth_type=AuthCredentialTypes.OAUTH2,
oauth2=OAuth2Auth(
client_id="test_client_id",
client_secret="test_client_secret",
redirect_uri="https://example.com/callback",
token_endpoint_auth_method="client_secret_jwt",
),
)
client, token_endpoint = create_oauth2_session(scheme, credential)
assert client is not None
assert token_endpoint == "https://example.com/token"
assert client.token_endpoint_auth_method == "client_secret_jwt"
def test_update_credential_with_tokens(self):
"""Test update_credential_with_tokens function."""
credential = AuthCredential(