Skip to content

Commit a546bcf

Browse files
sqsgeharanrk
authored andcommitted
fix(auth): handle missing client-credentials scopes safely
Merge #5348 ## Summary - Normalize OAuth scopes so the client-credentials/M2M flow no longer crashes with `AttributeError: 'NoneType' object has no attribute 'keys'` when scopes are absent. - Add a regression test for the client-credentials flow with missing scopes. Fixes #5345 Co-authored-by: Haran Rajkumar <haranrk@google.com> COPYBARA_INTEGRATE_REVIEW=#5348 from sqsge:codex/fix-openapi-m2m-scopes f73e347 PiperOrigin-RevId: 933815068
1 parent 4340208 commit a546bcf

2 files changed

Lines changed: 56 additions & 12 deletions

File tree

src/google/adk/auth/auth_handler.py

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,17 @@
3636
AUTHLIB_AVAILABLE = False
3737

3838

39+
def _normalize_oauth_scopes(
40+
scopes: dict[str, str] | list[str] | None,
41+
) -> list[str]:
42+
"""Normalize OAuth scopes into the list shape expected by authlib."""
43+
if not scopes:
44+
return []
45+
if isinstance(scopes, dict):
46+
return list(scopes.keys())
47+
return list(scopes)
48+
49+
3950
class AuthHandler:
4051
"""A handler that handles the auth flow in Agent Development Kit to help
4152
orchestrate the credential request and response flow (e.g. OAuth flow)
@@ -164,7 +175,7 @@ def generate_auth_uri(
164175

165176
if isinstance(auth_scheme, OpenIdConnectWithConfig):
166177
authorization_endpoint = auth_scheme.authorization_endpoint
167-
scopes = auth_scheme.scopes
178+
scopes = _normalize_oauth_scopes(auth_scheme.scopes)
168179
else:
169180
authorization_endpoint = (
170181
auth_scheme.flows.implicit
@@ -176,17 +187,20 @@ def generate_auth_uri(
176187
or auth_scheme.flows.password
177188
and auth_scheme.flows.password.tokenUrl
178189
)
179-
scopes = (
180-
auth_scheme.flows.implicit
181-
and auth_scheme.flows.implicit.scopes
182-
or auth_scheme.flows.authorizationCode
183-
and auth_scheme.flows.authorizationCode.scopes
184-
or auth_scheme.flows.clientCredentials
185-
and auth_scheme.flows.clientCredentials.scopes
186-
or auth_scheme.flows.password
187-
and auth_scheme.flows.password.scopes
188-
)
189-
scopes = list(scopes.keys())
190+
if auth_scheme.flows.implicit:
191+
scopes = _normalize_oauth_scopes(auth_scheme.flows.implicit.scopes)
192+
elif auth_scheme.flows.authorizationCode:
193+
scopes = _normalize_oauth_scopes(
194+
auth_scheme.flows.authorizationCode.scopes
195+
)
196+
elif auth_scheme.flows.clientCredentials:
197+
scopes = _normalize_oauth_scopes(
198+
auth_scheme.flows.clientCredentials.scopes
199+
)
200+
elif auth_scheme.flows.password:
201+
scopes = _normalize_oauth_scopes(auth_scheme.flows.password.scopes)
202+
else:
203+
scopes = []
190204

191205
client = OAuth2Session(
192206
auth_credential.oauth2.client_id,

tests/unittests/auth/test_auth_handler.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from fastapi.openapi.models import APIKeyIn
2323
from fastapi.openapi.models import OAuth2
2424
from fastapi.openapi.models import OAuthFlowAuthorizationCode
25+
from fastapi.openapi.models import OAuthFlowClientCredentials
2526
from fastapi.openapi.models import OAuthFlows
2627
from google.adk.auth.auth_credential import AuthCredential
2728
from google.adk.auth.auth_credential import AuthCredentialTypes
@@ -273,6 +274,35 @@ def test_generate_auth_uri_openid(
273274
assert "client_id=mock_client_id" in result.oauth2.auth_uri
274275
assert result.oauth2.state == "mock_state"
275276

277+
@patch("google.adk.auth.auth_handler.OAuth2Session", MockOAuth2Session)
278+
def test_generate_auth_uri_client_credentials_with_missing_scopes(
279+
self, oauth2_credentials
280+
):
281+
"""Test client credentials flow tolerates missing scopes."""
282+
auth_scheme = OAuth2(
283+
flows=OAuthFlows(
284+
clientCredentials=OAuthFlowClientCredentials(
285+
tokenUrl="https://example.com/oauth2/token"
286+
)
287+
)
288+
)
289+
auth_scheme.flows.clientCredentials.scopes = None
290+
291+
config = AuthConfig(
292+
auth_scheme=auth_scheme,
293+
raw_auth_credential=oauth2_credentials,
294+
exchanged_auth_credential=oauth2_credentials.model_copy(deep=True),
295+
)
296+
297+
handler = AuthHandler(config)
298+
result = handler.generate_auth_uri()
299+
300+
assert (
301+
result.oauth2.auth_uri
302+
== "https://example.com/oauth2/token?client_id=mock_client_id&scope="
303+
)
304+
assert result.oauth2.state == "mock_state"
305+
276306
@patch("google.adk.auth.auth_handler.OAuth2Session")
277307
def test_generate_auth_uri_pkce(
278308
self, mock_oauth2_session, oauth2_auth_scheme, oauth2_credentials

0 commit comments

Comments
 (0)