Skip to content

Commit 8a68cd8

Browse files
feat(cdk): Add configurable OAuth scope property name (scopes_name)
This change adds a new 'scopes_name' parameter to OAuth authenticators, allowing connector developers to configure the property name used for scopes in token refresh requests. Per RFC 6749, the default is 'scope' (singular), but some APIs require 'scopes' (plural) or other custom names. Changes: - Add abstract get_scopes_name() method to AbstractOauth2Authenticator - Add scopes_name parameter to Oauth2Authenticator (default: 'scope') - Add scopes_name parameter to SingleUseRefreshTokenOauth2Authenticator - Add scopes_name field to DeclarativeOauth2Authenticator - Update declarative_component_schema.yaml with scopes_name property - Update model_to_component_factory.py to pass scopes_name - Update tests to reflect new default behavior Resolves: airbytehq/oncall#11127 Related: airbytehq/airbyte#54137 Co-Authored-By: unknown <>
1 parent efad73e commit 8a68cd8

8 files changed

Lines changed: 128 additions & 67 deletions

File tree

airbyte_cdk/sources/declarative/auth/oauth.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ class DeclarativeOauth2Authenticator(AbstractOauth2Authenticator, DeclarativeAut
6868
client_secret_name: Union[InterpolatedString, str] = "client_secret"
6969
expires_in_name: Union[InterpolatedString, str] = "expires_in"
7070
refresh_token_name: Union[InterpolatedString, str] = "refresh_token"
71+
scopes_name: Union[InterpolatedString, str] = "scope"
7172
refresh_request_body: Optional[Mapping[str, Any]] = None
7273
refresh_request_headers: Optional[Mapping[str, Any]] = None
7374
grant_type_name: Union[InterpolatedString, str] = "grant_type"
@@ -108,6 +109,7 @@ def __post_init__(self, parameters: Mapping[str, Any]) -> None:
108109
self._refresh_token_name = InterpolatedString.create(
109110
self.refresh_token_name, parameters=parameters
110111
)
112+
self._scopes_name = InterpolatedString.create(self.scopes_name, parameters=parameters)
111113
if self.refresh_token is not None:
112114
self._refresh_token: Optional[InterpolatedString] = InterpolatedString.create(
113115
self.refresh_token, parameters=parameters
@@ -229,6 +231,9 @@ def get_refresh_token(self) -> Optional[str]:
229231
def get_scopes(self) -> List[str]:
230232
return self.scopes or []
231233

234+
def get_scopes_name(self) -> str:
235+
return self._scopes_name.eval(self.config) # type: ignore # eval returns a string in this context
236+
232237
def get_access_token_name(self) -> str:
233238
return self.access_token_name.eval(self.config) # type: ignore # eval returns a string in this context
234239

airbyte_cdk/sources/declarative/declarative_component_schema.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1429,6 +1429,14 @@ definitions:
14291429
"crm.objects.contacts.read",
14301430
"crm.schema.contacts.read",
14311431
]
1432+
scopes_name:
1433+
title: Scopes Property Name
1434+
description: The name of the property to use for scopes in the token refresh request body. Per RFC 6749, the default is "scope" (singular).
1435+
type: string
1436+
default: "scope"
1437+
examples:
1438+
- scope
1439+
- scopes
14321440
token_expiry_date:
14331441
title: Token Expiry Date
14341442
description: The access token expiry date.

airbyte_cdk/sources/declarative/models/declarative_component_schema.py

Lines changed: 92 additions & 60 deletions
Large diffs are not rendered by default.

airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2818,6 +2818,9 @@ def create_oauth_authenticator(
28182818
refresh_request_headers=InterpolatedMapping(
28192819
model.refresh_request_headers or {}, parameters=model.parameters or {}
28202820
).eval(config),
2821+
scopes_name=InterpolatedString.create(
2822+
model.scopes_name or "scope", parameters=model.parameters or {}
2823+
).eval(config),
28212824
scopes=model.scopes,
28222825
token_expiry_date_format=model.token_expiry_date_format,
28232826
token_expiry_is_time_of_expiration=bool(model.token_expiry_date_format),
@@ -2841,6 +2844,7 @@ def create_oauth_authenticator(
28412844
refresh_request_headers=model.refresh_request_headers,
28422845
refresh_token_name=model.refresh_token_name or "refresh_token",
28432846
refresh_token=model.refresh_token,
2847+
scopes_name=model.scopes_name or "scope",
28442848
scopes=model.scopes,
28452849
token_expiry_date=model.token_expiry_date,
28462850
token_expiry_date_format=model.token_expiry_date_format,

airbyte_cdk/sources/streams/http/requests_native_auth/abstract_oauth.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ def build_refresh_request_body(self) -> Mapping[str, Any]:
128128
payload[self.get_refresh_token_name()] = self.get_refresh_token()
129129

130130
if self.get_scopes():
131-
payload["scopes"] = self.get_scopes()
131+
payload[self.get_scopes_name()] = self.get_scopes()
132132

133133
if self.get_refresh_request_body():
134134
for key, val in self.get_refresh_request_body().items():
@@ -484,6 +484,10 @@ def get_refresh_token(self) -> Optional[str]:
484484
def get_scopes(self) -> List[str]:
485485
"""List of requested scopes"""
486486

487+
@abstractmethod
488+
def get_scopes_name(self) -> str:
489+
"""The name of the property to use for scopes in the token request"""
490+
487491
@abstractmethod
488492
def get_token_expiry_date(self) -> AirbyteDateTime:
489493
"""Expiration date of the access token"""

airbyte_cdk/sources/streams/http/requests_native_auth/oauth.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ def __init__(
3838
client_id_name: str = "client_id",
3939
client_secret_name: str = "client_secret",
4040
refresh_token_name: str = "refresh_token",
41+
scopes_name: str = "scope",
4142
scopes: List[str] | None = None,
4243
token_expiry_date: AirbyteDateTime | None = None,
4344
token_expiry_date_format: str | None = None,
@@ -59,6 +60,7 @@ def __init__(
5960
self._client_id = client_id
6061
self._refresh_token_name = refresh_token_name
6162
self._refresh_token = refresh_token
63+
self._scopes_name = scopes_name
6264
self._scopes = scopes
6365
self._access_token_name = access_token_name
6466
self._expires_in_name = expires_in_name
@@ -102,6 +104,9 @@ def get_access_token_name(self) -> str:
102104
def get_scopes(self) -> list[str]:
103105
return self._scopes # type: ignore[return-value]
104106

107+
def get_scopes_name(self) -> str:
108+
return self._scopes_name
109+
105110
def get_expires_in_name(self) -> str:
106111
return self._expires_in_name
107112

@@ -154,6 +159,7 @@ def __init__(
154159
self,
155160
connector_config: Mapping[str, Any],
156161
token_refresh_endpoint: str,
162+
scopes_name: str = "scope",
157163
scopes: List[str] | None = None,
158164
access_token_name: str = "access_token",
159165
expires_in_name: str = "expires_in",
@@ -221,6 +227,7 @@ def __init__(
221227
client_secret=self._client_secret,
222228
refresh_token=self.get_refresh_token(),
223229
refresh_token_name=self._refresh_token_name,
230+
scopes_name=scopes_name,
224231
scopes=scopes,
225232
token_expiry_date=self.get_token_expiry_date(),
226233
access_token_name=access_token_name,

unit_tests/sources/declarative/auth/test_oauth.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def test_refresh_request_body(self):
5858
refresh_request_body={
5959
"custom_field": "{{ config['custom_field'] }}",
6060
"another_field": "{{ config['another_field'] }}",
61-
"scopes": ["no_override"],
61+
"scope": ["no_override"],
6262
},
6363
parameters=parameters,
6464
grant_type="{{ config['grant_type'] }}",
@@ -69,7 +69,7 @@ def test_refresh_request_body(self):
6969
"client_id": "some_client_id",
7070
"client_secret": "some_client_secret",
7171
"refresh_token": "some_refresh_token",
72-
"scopes": scopes,
72+
"scope": scopes,
7373
"custom_field": "in_outbound_request",
7474
"another_field": "exists_in_body",
7575
}

unit_tests/sources/streams/http/requests_native_auth/test_requests_native_auth.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ def test_refresh_request_body(self):
156156
refresh_request_body={
157157
"custom_field": "in_outbound_request",
158158
"another_field": "exists_in_body",
159-
"scopes": ["no_override"],
159+
"scope": ["no_override"],
160160
},
161161
)
162162
body = oauth.build_refresh_request_body()
@@ -165,7 +165,7 @@ def test_refresh_request_body(self):
165165
"client_id": "some_client_id",
166166
"client_secret": "some_client_secret",
167167
"refresh_token": "some_refresh_token",
168-
"scopes": scopes,
168+
"scope": scopes,
169169
"custom_field": "in_outbound_request",
170170
"another_field": "exists_in_body",
171171
}
@@ -216,14 +216,15 @@ def test_refresh_request_body_with_keys_override(self):
216216
client_secret="some_client_secret",
217217
refresh_token_name="custom_refresh_token_key",
218218
refresh_token="some_refresh_token",
219+
scopes_name="custom_scopes_key",
219220
scopes=["scope1", "scope2"],
220221
token_expiry_date=ab_datetime_now() + timedelta(days=3),
221222
grant_type_name="custom_grant_type",
222223
grant_type="some_grant_type",
223224
refresh_request_body={
224225
"custom_field": "in_outbound_request",
225226
"another_field": "exists_in_body",
226-
"scopes": ["no_override"],
227+
"custom_scopes_key": ["no_override"],
227228
},
228229
)
229230
body = oauth.build_refresh_request_body()
@@ -232,7 +233,7 @@ def test_refresh_request_body_with_keys_override(self):
232233
"custom_client_id_key": "some_client_id",
233234
"custom_client_secret_key": "some_client_secret",
234235
"custom_refresh_token_key": "some_refresh_token",
235-
"scopes": scopes,
236+
"custom_scopes_key": scopes,
236237
"custom_field": "in_outbound_request",
237238
"another_field": "exists_in_body",
238239
}

0 commit comments

Comments
 (0)