Skip to content

Commit fe97c73

Browse files
feat(oauth): add configurable token_refresh_request_type for GET-based OAuth APIs
Add token_refresh_request_type property to OAuthAuthenticator supporting: - body_data (default): POST with form-encoded body (standard OAuth2) - body_json: POST with JSON body - query_params: GET with query parameters (e.g., Marketo) This unblocks Connector Builder users from building connectors for APIs like Marketo that require GET requests to their OAuth token endpoint. Changes: - AbstractOauth2Authenticator: add get_token_refresh_request_type() and update _make_handled_request() to dispatch on request type - DeclarativeOauth2Authenticator: thread through token_refresh_request_type - declarative_component_schema.yaml: add token_refresh_request_type enum - declarative_component_schema.py: add Pydantic field - model_to_component_factory.py: wire through the new property Co-Authored-By: unknown <>
1 parent 7f41401 commit fe97c73

5 files changed

Lines changed: 62 additions & 9 deletions

File tree

airbyte_cdk/sources/declarative/auth/oauth.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ class DeclarativeOauth2Authenticator(AbstractOauth2Authenticator, DeclarativeAut
7272
refresh_request_headers: Optional[Mapping[str, Any]] = None
7373
grant_type_name: Union[InterpolatedString, str] = "grant_type"
7474
grant_type: Union[InterpolatedString, str] = "refresh_token"
75+
token_refresh_request_type: str = "body_data"
7576
message_repository: MessageRepository = NoopMessageRepository()
7677
profile_assertion: Optional[DeclarativeAuthenticator] = None
7778
use_profile_assertion: Optional[Union[InterpolatedBoolean, str, bool]] = False
@@ -247,6 +248,9 @@ def get_refresh_request_body(self) -> Mapping[str, Any]:
247248
def get_refresh_request_headers(self) -> Mapping[str, Any]:
248249
return self._refresh_request_headers.eval(self.config)
249250

251+
def get_token_refresh_request_type(self) -> str:
252+
return self.token_refresh_request_type
253+
250254
def get_token_expiry_date(self) -> AirbyteDateTime:
251255
if not self._has_access_token_been_initialized():
252256
return AirbyteDateTime.from_datetime(datetime.min)

airbyte_cdk/sources/declarative/declarative_component_schema.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1400,6 +1400,15 @@ definitions:
14001400
examples:
14011401
- refresh_token
14021402
- client_credentials
1403+
token_refresh_request_type:
1404+
title: Token Refresh Request Type
1405+
description: Configures how the token refresh request is sent. Use body_data (default) for POST with form-encoded body, body_json for POST with JSON body, or query_params for GET with query parameters (required by some APIs like Marketo).
1406+
type: string
1407+
default: "body_data"
1408+
enum:
1409+
- body_data
1410+
- body_json
1411+
- query_params
14031412
refresh_request_body:
14041413
title: Refresh Request Body
14051414
description: Body of the request sent to get a new access token.

airbyte_cdk/sources/declarative/models/declarative_component_schema.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1871,6 +1871,12 @@ class OAuthAuthenticator(BaseModel):
18711871
examples=["refresh_token", "client_credentials"],
18721872
title="Grant Type",
18731873
)
1874+
token_refresh_request_type: Optional[str] = Field(
1875+
"body_data",
1876+
description="Configures how the token refresh request is sent. Use body_data (default) for POST with form-encoded body, body_json for POST with JSON body, or query_params for GET with query parameters (required by some APIs like Marketo).",
1877+
enum=["body_data", "body_json", "query_params"],
1878+
title="Token Refresh Request Type",
1879+
)
18741880
refresh_request_body: Optional[Dict[str, Any]] = Field(
18751881
None,
18761882
description="Body of the request sent to get a new access token.",

airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2837,6 +2837,7 @@ def create_oauth_authenticator(
28372837
expires_in_name=model.expires_in_name or "expires_in",
28382838
grant_type_name=model.grant_type_name or "grant_type",
28392839
grant_type=model.grant_type or "refresh_token",
2840+
token_refresh_request_type=model.token_refresh_request_type or "body_data",
28402841
refresh_request_body=model.refresh_request_body,
28412842
refresh_request_headers=model.refresh_request_headers,
28422843
refresh_token_name=model.refresh_token_name or "refresh_token",

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

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -239,9 +239,12 @@ def _make_handled_request(self) -> Any:
239239
"""
240240
Makes a handled HTTP request to refresh an OAuth token.
241241
242-
This method sends a POST request to the token refresh endpoint with the necessary
243-
headers and body to obtain a new access token. It handles various exceptions that
244-
may occur during the request and logs the response for troubleshooting purposes.
242+
This method sends an HTTP request to the token refresh endpoint with the necessary
243+
headers and body/params to obtain a new access token. The HTTP method and parameter
244+
encoding are determined by get_token_refresh_request_type():
245+
- "body_data" (default): POST with form-encoded body
246+
- "body_json": POST with JSON body
247+
- "query_params": GET with query parameters
245248
246249
Returns:
247250
Mapping[str, Any]: The JSON response from the token refresh endpoint.
@@ -254,12 +257,32 @@ def _make_handled_request(self) -> Any:
254257
Exception: For any other exceptions that occur during the request.
255258
"""
256259
try:
257-
response = requests.request(
258-
method="POST",
259-
url=self.get_token_refresh_endpoint(), # type: ignore # returns None, if not provided, but str | bytes is expected.
260-
data=self.build_refresh_request_body(),
261-
headers=self.build_refresh_request_headers(),
262-
)
260+
request_type = self.get_token_refresh_request_type()
261+
headers = self.build_refresh_request_headers()
262+
body = self.build_refresh_request_body()
263+
url = self.get_token_refresh_endpoint()
264+
265+
if request_type == "query_params":
266+
response = requests.request(
267+
method="GET",
268+
url=url, # type: ignore[arg-type] # returns None if not provided, but str | bytes is expected
269+
params=body,
270+
headers=headers,
271+
)
272+
elif request_type == "body_json":
273+
response = requests.request(
274+
method="POST",
275+
url=url, # type: ignore[arg-type] # returns None if not provided, but str | bytes is expected
276+
json=body,
277+
headers=headers,
278+
)
279+
else:
280+
response = requests.request(
281+
method="POST",
282+
url=url, # type: ignore[arg-type] # returns None if not provided, but str | bytes is expected
283+
data=body,
284+
headers=headers,
285+
)
263286

264287
if not response.ok:
265288
# log the response even if the request failed for troubleshooting purposes
@@ -543,6 +566,16 @@ def get_grant_type(self) -> str:
543566
def get_grant_type_name(self) -> str:
544567
"""Returns grant_type specified name for requesting access_token"""
545568

569+
def get_token_refresh_request_type(self) -> str:
570+
"""Returns the request type for the token refresh request.
571+
572+
Supported values:
573+
- "body_data": POST with form-encoded body (default, standard OAuth2)
574+
- "body_json": POST with JSON-encoded body
575+
- "query_params": GET with query parameters (e.g., Marketo)
576+
"""
577+
return "body_data"
578+
546579
@property
547580
@abstractmethod
548581
def access_token(self) -> str:

0 commit comments

Comments
 (0)