Skip to content

Commit 3d00259

Browse files
Patrick Mageeclaude
andauthored
[CU-86b7bxfjk] Filter unsupported OAuth grant types in CLI (#129)
The CLI now filters out authentication methods with unsupported grant types (e.g., authorization_code) from service registry responses. This prevents OAuth2MisconfigurationError when services include client configurations intended for other use cases like bridge-service. Supported grant types: device_code, client_credentials, token_exchange 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1f69e58 commit 3d00259

3 files changed

Lines changed: 151 additions & 1 deletion

File tree

dnastack/http/authenticators/factory.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
from typing import List, Any, Dict, Optional
2+
import logging
23

34
from dnastack.client.models import ServiceEndpoint
45
from dnastack.common.model_mixin import JsonModelMixin
56
from dnastack.http.authenticators.abstract import Authenticator
67
from dnastack.http.authenticators.oauth2 import OAuth2Authenticator
7-
from dnastack.http.authenticators.oauth2_adapter.models import OAuth2Authentication
8+
from dnastack.http.authenticators.oauth2_adapter.models import OAuth2Authentication, SUPPORTED_GRANT_TYPES
89

910

1011
class UnsupportedAuthenticationInformationError(RuntimeError):
@@ -43,10 +44,21 @@ def _parse_auth_info(auth_info: Dict[str, Any]) -> Dict[str, Any]:
4344

4445
@staticmethod
4546
def get_unique_auth_info_list(endpoints: List[ServiceEndpoint]) -> List[Dict[str, Any]]:
47+
logger = logging.getLogger(__name__)
4648
unique_auth_info_map: Dict[str, Dict[str, Any]] = {}
4749

4850
for endpoint in endpoints:
4951
for auth_info in endpoint.get_authentications():
52+
grant_type = auth_info.get('grant_type')
53+
54+
# Skip authentication methods with unsupported grant types
55+
if grant_type and grant_type not in SUPPORTED_GRANT_TYPES:
56+
logger.debug("Skipping authentication with unsupported grant type '{}' for resource '{}'".format(
57+
grant_type,
58+
auth_info.get('resource_url', 'unknown')
59+
))
60+
continue
61+
5062
unique_auth_info_map[JsonModelMixin.hash(auth_info)] = auth_info
5163

5264
return sorted(unique_auth_info_map.values(), key=lambda a: a.get('resource_url') or a.get('type'))

dnastack/http/authenticators/oauth2_adapter/models.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@
66

77

88
GRANT_TYPE_TOKEN_EXCHANGE = 'urn:ietf:params:oauth:grant-type:token-exchange'
9+
GRANT_TYPE_DEVICE_CODE = 'urn:ietf:params:oauth:grant-type:device_code'
10+
GRANT_TYPE_CLIENT_CREDENTIALS = 'client_credentials'
11+
12+
# List of grant types supported by the CLI
13+
SUPPORTED_GRANT_TYPES = [
14+
GRANT_TYPE_DEVICE_CODE,
15+
GRANT_TYPE_CLIENT_CREDENTIALS,
16+
GRANT_TYPE_TOKEN_EXCHANGE,
17+
]
918

1019

1120
class OAuth2Authentication(BaseModel, HashableModel):
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
from unittest import TestCase
2+
3+
from dnastack import ServiceEndpoint
4+
from dnastack.http.authenticators.factory import HttpAuthenticatorFactory
5+
6+
7+
class TestHttpAuthenticatorFactory(TestCase):
8+
def test_filter_unsupported_grant_types(self):
9+
"""
10+
Test that HttpAuthenticatorFactory filters out authentication methods
11+
with unsupported grant types like 'authorization_code'
12+
"""
13+
# Create endpoints with different grant types
14+
device_code_endpoint = ServiceEndpoint(
15+
url='https://dc.faux.dnastack.com',
16+
authentication={
17+
'type': 'oauth2',
18+
'grant_type': 'urn:ietf:params:oauth:grant-type:device_code',
19+
'client_id': 'device-client',
20+
'device_code_endpoint': 'https://auth.faux.dnastack.com/oauth/device/code',
21+
'token_endpoint': 'https://auth.faux.dnastack.com/oauth/token',
22+
'resource_url': 'https://dc.faux.dnastack.com'
23+
}
24+
)
25+
26+
authorization_code_endpoint = ServiceEndpoint(
27+
url='https://workbench.faux.dnastack.com',
28+
authentication={
29+
'type': 'oauth2',
30+
'grant_type': 'authorization_code',
31+
'client_id': 'workbench-client',
32+
'token_endpoint': 'https://auth.faux.dnastack.com/oauth/token',
33+
'resource_url': 'https://workbench.faux.dnastack.com'
34+
}
35+
)
36+
37+
client_credentials_endpoint = ServiceEndpoint(
38+
url='https://api.faux.dnastack.com',
39+
authentication={
40+
'type': 'oauth2',
41+
'grant_type': 'client_credentials',
42+
'client_id': 'api-client',
43+
'client_secret': 'secret123',
44+
'token_endpoint': 'https://auth.faux.dnastack.com/oauth/token',
45+
'resource_url': 'https://api.faux.dnastack.com'
46+
}
47+
)
48+
49+
# Create authenticators - should only include device_code and client_credentials
50+
authenticators = HttpAuthenticatorFactory.create_multiple_from(
51+
endpoints=[
52+
device_code_endpoint,
53+
authorization_code_endpoint,
54+
client_credentials_endpoint
55+
]
56+
)
57+
58+
# Verify that only 2 authenticators were created (device_code and client_credentials)
59+
# The authorization_code endpoint should have been filtered out
60+
self.assertEqual(len(authenticators), 2,
61+
"Should only create authenticators for supported grant types")
62+
63+
# Verify the authenticators are for the correct endpoints
64+
auth_resource_urls = {auth._auth_info['resource_url'] for auth in authenticators}
65+
self.assertIn('https://dc.faux.dnastack.com', auth_resource_urls,
66+
"Should include device_code endpoint")
67+
self.assertIn('https://api.faux.dnastack.com', auth_resource_urls,
68+
"Should include client_credentials endpoint")
69+
self.assertNotIn('https://workbench.faux.dnastack.com', auth_resource_urls,
70+
"Should NOT include authorization_code endpoint")
71+
72+
def test_filter_all_supported_grant_types_pass_through(self):
73+
"""
74+
Test that all supported grant types (device_code, client_credentials, token_exchange)
75+
are correctly passed through and not filtered
76+
"""
77+
device_code_endpoint = ServiceEndpoint(
78+
url='https://dc.faux.dnastack.com',
79+
authentication={
80+
'type': 'oauth2',
81+
'grant_type': 'urn:ietf:params:oauth:grant-type:device_code',
82+
'client_id': 'device-client',
83+
'device_code_endpoint': 'https://auth.faux.dnastack.com/oauth/device/code',
84+
'token_endpoint': 'https://auth.faux.dnastack.com/oauth/token',
85+
'resource_url': 'https://dc.faux.dnastack.com'
86+
}
87+
)
88+
89+
client_credentials_endpoint = ServiceEndpoint(
90+
url='https://api.faux.dnastack.com',
91+
authentication={
92+
'type': 'oauth2',
93+
'grant_type': 'client_credentials',
94+
'client_id': 'api-client',
95+
'client_secret': 'secret123',
96+
'token_endpoint': 'https://auth.faux.dnastack.com/oauth/token',
97+
'resource_url': 'https://api.faux.dnastack.com'
98+
}
99+
)
100+
101+
token_exchange_endpoint = ServiceEndpoint(
102+
url='https://exchange.faux.dnastack.com',
103+
authentication={
104+
'type': 'oauth2',
105+
'grant_type': 'urn:ietf:params:oauth:grant-type:token-exchange',
106+
'client_id': 'exchange-client',
107+
'token_endpoint': 'https://auth.faux.dnastack.com/oauth/token',
108+
'resource_url': 'https://exchange.faux.dnastack.com'
109+
}
110+
)
111+
112+
# Create authenticators - should include all 3
113+
authenticators = HttpAuthenticatorFactory.create_multiple_from(
114+
endpoints=[
115+
device_code_endpoint,
116+
client_credentials_endpoint,
117+
token_exchange_endpoint
118+
]
119+
)
120+
121+
# Verify all 3 authenticators were created
122+
self.assertEqual(len(authenticators), 3,
123+
"Should create authenticators for all supported grant types")
124+
125+
# Verify the authenticators are for the correct endpoints
126+
auth_resource_urls = {auth._auth_info['resource_url'] for auth in authenticators}
127+
self.assertIn('https://dc.faux.dnastack.com', auth_resource_urls)
128+
self.assertIn('https://api.faux.dnastack.com', auth_resource_urls)
129+
self.assertIn('https://exchange.faux.dnastack.com', auth_resource_urls)

0 commit comments

Comments
 (0)