diff --git a/airbyte_cdk/sources/declarative/auth/oauth.py b/airbyte_cdk/sources/declarative/auth/oauth.py index f3ba528ac..3bfed9c2a 100644 --- a/airbyte_cdk/sources/declarative/auth/oauth.py +++ b/airbyte_cdk/sources/declarative/auth/oauth.py @@ -56,8 +56,12 @@ class DeclarativeOauth2Authenticator(AbstractOauth2Authenticator, DeclarativeAut token_expiry_is_time_of_expiration: bool = False access_token_name: Union[InterpolatedString, str] = "access_token" access_token_value: Optional[Union[InterpolatedString, str]] = None + client_id_name: Union[InterpolatedString, str] = "client_id" + client_secret_name: Union[InterpolatedString, str] = "client_secret" expires_in_name: Union[InterpolatedString, str] = "expires_in" + refresh_token_name: Union[InterpolatedString, str] = "refresh_token" refresh_request_body: Optional[Mapping[str, Any]] = None + grant_type_name: Union[InterpolatedString, str] = "grant_type" grant_type: Union[InterpolatedString, str] = "refresh_token" message_repository: MessageRepository = NoopMessageRepository() @@ -69,8 +73,15 @@ def __post_init__(self, parameters: Mapping[str, Any]) -> None: ) else: self._token_refresh_endpoint = None + self._client_id_name = InterpolatedString.create(self.client_id_name, parameters=parameters) self._client_id = InterpolatedString.create(self.client_id, parameters=parameters) + self._client_secret_name = InterpolatedString.create( + self.client_secret_name, parameters=parameters + ) self._client_secret = InterpolatedString.create(self.client_secret, parameters=parameters) + self._refresh_token_name = InterpolatedString.create( + self.refresh_token_name, parameters=parameters + ) if self.refresh_token is not None: self._refresh_token: Optional[InterpolatedString] = InterpolatedString.create( self.refresh_token, parameters=parameters @@ -83,6 +94,9 @@ def __post_init__(self, parameters: Mapping[str, Any]) -> None: self.expires_in_name = InterpolatedString.create( self.expires_in_name, parameters=parameters ) + self.grant_type_name = InterpolatedString.create( + self.grant_type_name, parameters=parameters + ) self.grant_type = InterpolatedString.create(self.grant_type, parameters=parameters) self._refresh_request_body = InterpolatedMapping( self.refresh_request_body or {}, parameters=parameters @@ -122,18 +136,27 @@ def get_token_refresh_endpoint(self) -> Optional[str]: return refresh_token_endpoint return None + def get_client_id_name(self) -> str: + return self._client_id_name.eval(self.config) # type: ignore # eval returns a string in this context + def get_client_id(self) -> str: client_id: str = self._client_id.eval(self.config) if not client_id: raise ValueError("OAuthAuthenticator was unable to evaluate client_id parameter") return client_id + def get_client_secret_name(self) -> str: + return self._client_secret_name.eval(self.config) # type: ignore # eval returns a string in this context + def get_client_secret(self) -> str: client_secret: str = self._client_secret.eval(self.config) if not client_secret: raise ValueError("OAuthAuthenticator was unable to evaluate client_secret parameter") return client_secret + def get_refresh_token_name(self) -> str: + return self._refresh_token_name.eval(self.config) # type: ignore # eval returns a string in this context + def get_refresh_token(self) -> Optional[str]: return None if self._refresh_token is None else str(self._refresh_token.eval(self.config)) @@ -146,6 +169,9 @@ def get_access_token_name(self) -> str: def get_expires_in_name(self) -> str: return self.expires_in_name.eval(self.config) # type: ignore # eval returns a string in this context + def get_grant_type_name(self) -> str: + return self.grant_type_name.eval(self.config) # type: ignore # eval returns a string in this context + def get_grant_type(self) -> str: return self.grant_type.eval(self.config) # type: ignore # eval returns a string in this context diff --git a/airbyte_cdk/sources/declarative/declarative_component_schema.yaml b/airbyte_cdk/sources/declarative/declarative_component_schema.yaml index 04751203e..71a3b8084 100644 --- a/airbyte_cdk/sources/declarative/declarative_component_schema.yaml +++ b/airbyte_cdk/sources/declarative/declarative_component_schema.yaml @@ -1047,6 +1047,13 @@ definitions: type: type: string enum: [OAuthAuthenticator] + client_id_name: + title: Client ID Property Name + description: The name of the property to use to refresh the `access_token`. + type: string + default: "client_id" + examples: + - custom_app_id client_id: title: Client ID description: The OAuth client ID. Fill it in the user inputs. @@ -1054,6 +1061,13 @@ definitions: examples: - "{{ config['client_id }}" - "{{ config['credentials']['client_id }}" + client_secret_name: + title: Client Secret Property Name + description: The name of the property to use to refresh the `access_token`. + type: string + default: "client_secret" + examples: + - custom_app_secret client_secret: title: Client Secret description: The OAuth client secret. Fill it in the user inputs. @@ -1061,6 +1075,13 @@ definitions: examples: - "{{ config['client_secret }}" - "{{ config['credentials']['client_secret }}" + refresh_token_name: + title: Refresh Token Property Name + description: The name of the property to use to refresh the `access_token`. + type: string + default: "refresh_token" + examples: + - custom_app_refresh_value refresh_token: title: Refresh Token description: Credential artifact used to get a new access token. @@ -1094,6 +1115,13 @@ definitions: default: "expires_in" examples: - expires_in + grant_type_name: + title: Grant Type Property Name + description: The name of the property to use to refresh the `access_token`. + type: string + default: "grant_type" + examples: + - custom_grant_type grant_type: title: Grant Type description: Specifies the OAuth2 grant type. If set to refresh_token, the refresh_token needs to be provided as well. For client_credentials, only client id and secret are required. Other grant types are not officially supported. @@ -2204,15 +2232,15 @@ definitions: Pertains to the fields defined by the connector relating to the OAuth flow. Interpolation capabilities: - - The variables placeholders are declared as `{my_var}`. - - The nested resolution variables like `{{my_nested_var}}` is allowed as well. + - The variables placeholders are declared as `{{my_var}}`. + - The nested resolution variables like `{{ {{my_nested_var}} }}` is allowed as well. - The allowed interpolation context is: - + base64Encoder - encode to `base64`, {base64Encoder:{my_var_a}:{my_var_b}} - + base64Decorer - decode from `base64` encoded string, {base64Decoder:{my_string_variable_or_string_value}} - + urlEncoder - encode the input string to URL-like format, {urlEncoder:https://test.host.com/endpoint} - + urlDecorer - decode the input url-encoded string into text format, {urlDecoder:https%3A%2F%2Fairbyte.io} - + codeChallengeS256 - get the `codeChallenge` encoded value to provide additional data-provider specific authorisation values, {codeChallengeS256:{state_value}} + + base64Encoder - encode to `base64`, {{ {{my_var_a}}:{{my_var_b}} | base64Encoder }} + + base64Decorer - decode from `base64` encoded string, {{ {{my_string_variable_or_string_value}} | base64Decoder }} + + urlEncoder - encode the input string to URL-like format, {{ https://test.host.com/endpoint | urlEncoder}} + + urlDecorer - decode the input url-encoded string into text format, {{ urlDecoder:https%3A%2F%2Fairbyte.io | urlDecoder}} + + codeChallengeS256 - get the `codeChallenge` encoded value to provide additional data-provider specific authorisation values, {{ {{state_value}} | codeChallengeS256 }} Examples: - The TikTok Marketing DeclarativeOAuth spec: @@ -2221,12 +2249,12 @@ definitions: "type": "object", "additionalProperties": false, "properties": { - "consent_url": "https://ads.tiktok.com/marketing_api/auth?{client_id_key}={{client_id_key}}&{redirect_uri_key}={urlEncoder:{{redirect_uri_key}}}&{state_key}={{state_key}}", + "consent_url": "https://ads.tiktok.com/marketing_api/auth?{{client_id_key}}={{client_id_value}}&{{redirect_uri_key}}={{ {{redirect_uri_value}} | urlEncoder}}&{{state_key}}={{state_value}}", "access_token_url": "https://business-api.tiktok.com/open_api/v1.3/oauth2/access_token/", "access_token_params": { - "{auth_code_key}": "{{auth_code_key}}", - "{client_id_key}": "{{client_id_key}}", - "{client_secret_key}": "{{client_secret_key}}" + "{{ auth_code_key }}": "{{ auth_code_value }}", + "{{ client_id_key }}": "{{ client_id_value }}", + "{{ client_secret_key }}": "{{ client_secret_value }}" }, "access_token_headers": { "Content-Type": "application/json", @@ -2244,7 +2272,6 @@ definitions: required: - consent_url - access_token_url - - extract_output properties: consent_url: title: Consent URL @@ -2253,8 +2280,8 @@ definitions: The DeclarativeOAuth Specific string URL string template to initiate the authentication. The placeholders are replaced during the processing to provide neccessary values. examples: - - https://domain.host.com/marketing_api/auth?{client_id_key}={{client_id_key}}&{redirect_uri_key}={urlEncoder:{{redirect_uri_key}}}&{state_key}={{state_key}} - - https://endpoint.host.com/oauth2/authorize?{client_id_key}={{client_id_key}}&{redirect_uri_key}={urlEncoder:{{redirect_uri_key}}}&{scope_key}={urlEncoder:{{scope_key}}}&{state_key}={{state_key}}&subdomain={subdomain} + - https://domain.host.com/marketing_api/auth?{{client_id_key}}={{client_id_value}}&{{redirect_uri_key}}={{{{redirect_uri_value}} | urlEncoder}}&{{state_key}}={{state_value}} + - https://endpoint.host.com/oauth2/authorize?{{client_id_key}}={{client_id_value}}&{{redirect_uri_key}}={{{{redirect_uri_value}} | urlEncoder}}&{{scope_key}}={{{{scope_value}} | urlEncoder}}&{{state_key}}={{state_value}}&subdomain={{subdomain}} scope: title: Scopes type: string @@ -2269,7 +2296,7 @@ definitions: The DeclarativeOAuth Specific URL templated string to obtain the `access_token`, `refresh_token` etc. The placeholders are replaced during the processing to provide neccessary values. examples: - - https://auth.host.com/oauth2/token?{client_id_key}={{client_id_key}}&{client_secret_key}={{client_secret_key}}&{auth_code_key}={{auth_code_key}}&{redirect_uri_key}={urlEncoder:{{redirect_uri_key}}} + - https://auth.host.com/oauth2/token?{{client_id_key}}={{client_id_value}}&{{client_secret_key}}={{client_secret_value}}&{{auth_code_key}}={{auth_code_value}}&{{redirect_uri_key}}={{{{redirect_uri_value}} | urlEncoder}} access_token_headers: title: Access Token Headers type: object @@ -2278,7 +2305,7 @@ definitions: The DeclarativeOAuth Specific optional headers to inject while exchanging the `auth_code` to `access_token` during `completeOAuthFlow` step. examples: - { - "Authorization": "Basic {base64Encoder:{client_id}:{client_secret}}", + "Authorization": "Basic {{ {{ client_id_value }}:{{ client_secret_value }} | base64Encoder }}", } access_token_params: title: Access Token Query Params (Json Encoded) @@ -2289,9 +2316,9 @@ definitions: When this property is provided, the query params will be encoded as `Json` and included in the outgoing API request. examples: - { - "{auth_code_key}": "{{auth_code_key}}", - "{client_id_key}": "{{client_id_key}}", - "{client_secret_key}": "{{client_secret_key}}", + "{{ auth_code_key }}": "{{ auth_code_value }}", + "{{ client_id_key }}": "{{ client_id_value }}", + "{{ client_secret_key }}": "{{ client_secret_value }}", } extract_output: title: Extract Output diff --git a/airbyte_cdk/sources/declarative/models/declarative_component_schema.py b/airbyte_cdk/sources/declarative/models/declarative_component_schema.py index 80cc01ebc..3bce45d4a 100644 --- a/airbyte_cdk/sources/declarative/models/declarative_component_schema.py +++ b/airbyte_cdk/sources/declarative/models/declarative_component_schema.py @@ -481,12 +481,24 @@ class RefreshTokenUpdater(BaseModel): class OAuthAuthenticator(BaseModel): type: Literal["OAuthAuthenticator"] + client_id_name: Optional[str] = Field( + "client_id", + description="The name of the property to use to refresh the `access_token`.", + examples=["custom_app_id"], + title="Client ID Property Name", + ) client_id: str = Field( ..., description="The OAuth client ID. Fill it in the user inputs.", examples=["{{ config['client_id }}", "{{ config['credentials']['client_id }}"], title="Client ID", ) + client_secret_name: Optional[str] = Field( + "client_secret", + description="The name of the property to use to refresh the `access_token`.", + examples=["custom_app_secret"], + title="Client Secret Property Name", + ) client_secret: str = Field( ..., description="The OAuth client secret. Fill it in the user inputs.", @@ -496,6 +508,12 @@ class OAuthAuthenticator(BaseModel): ], title="Client Secret", ) + refresh_token_name: Optional[str] = Field( + "refresh_token", + description="The name of the property to use to refresh the `access_token`.", + examples=["custom_app_refresh_value"], + title="Refresh Token Property Name", + ) refresh_token: Optional[str] = Field( None, description="Credential artifact used to get a new access token.", @@ -529,6 +547,12 @@ class OAuthAuthenticator(BaseModel): examples=["expires_in"], title="Token Expiry Property Name", ) + grant_type_name: Optional[str] = Field( + "grant_type", + description="The name of the property to use to refresh the `access_token`.", + examples=["custom_grant_type"], + title="Grant Type Property Name", + ) grant_type: Optional[str] = Field( "refresh_token", description="Specifies the OAuth2 grant type. If set to refresh_token, the refresh_token needs to be provided as well. For client_credentials, only client id and secret are required. Other grant types are not officially supported.", @@ -859,8 +883,8 @@ class Config: ..., description="The DeclarativeOAuth Specific string URL string template to initiate the authentication.\nThe placeholders are replaced during the processing to provide neccessary values.", examples=[ - "https://domain.host.com/marketing_api/auth?{client_id_key}={{client_id_key}}&{redirect_uri_key}={urlEncoder:{{redirect_uri_key}}}&{state_key}={{state_key}}", - "https://endpoint.host.com/oauth2/authorize?{client_id_key}={{client_id_key}}&{redirect_uri_key}={urlEncoder:{{redirect_uri_key}}}&{scope_key}={urlEncoder:{{scope_key}}}&{state_key}={{state_key}}&subdomain={subdomain}", + "https://domain.host.com/marketing_api/auth?{{client_id_key}}={{client_id_value}}&{{redirect_uri_key}}={{{{redirect_uri_value}} | urlEncoder}}&{{state_key}}={{state_value}}", + "https://endpoint.host.com/oauth2/authorize?{{client_id_key}}={{client_id_value}}&{{redirect_uri_key}}={{{{redirect_uri_value}} | urlEncoder}}&{{scope_key}}={{{{scope_value}} | urlEncoder}}&{{state_key}}={{state_value}}&subdomain={{subdomain}}", ], title="Consent URL", ) @@ -874,14 +898,18 @@ class Config: ..., description="The DeclarativeOAuth Specific URL templated string to obtain the `access_token`, `refresh_token` etc.\nThe placeholders are replaced during the processing to provide neccessary values.", examples=[ - "https://auth.host.com/oauth2/token?{client_id_key}={{client_id_key}}&{client_secret_key}={{client_secret_key}}&{auth_code_key}={{auth_code_key}}&{redirect_uri_key}={urlEncoder:{{redirect_uri_key}}}" + "https://auth.host.com/oauth2/token?{{client_id_key}}={{client_id_value}}&{{client_secret_key}}={{client_secret_value}}&{{auth_code_key}}={{auth_code_value}}&{{redirect_uri_key}}={{{{redirect_uri_value}} | urlEncoder}}" ], title="Access Token URL", ) access_token_headers: Optional[Dict[str, Any]] = Field( None, description="The DeclarativeOAuth Specific optional headers to inject while exchanging the `auth_code` to `access_token` during `completeOAuthFlow` step.", - examples=[{"Authorization": "Basic {base64Encoder:{client_id}:{client_secret}}"}], + examples=[ + { + "Authorization": "Basic {{ {{ client_id_value }}:{{ client_secret_value }} | base64Encoder }}" + } + ], title="Access Token Headers", ) access_token_params: Optional[Dict[str, Any]] = Field( @@ -889,15 +917,15 @@ class Config: description="The DeclarativeOAuth Specific optional query parameters to inject while exchanging the `auth_code` to `access_token` during `completeOAuthFlow` step.\nWhen this property is provided, the query params will be encoded as `Json` and included in the outgoing API request.", examples=[ { - "{auth_code_key}": "{{auth_code_key}}", - "{client_id_key}": "{{client_id_key}}", - "{client_secret_key}": "{{client_secret_key}}", + "{{ auth_code_key }}": "{{ auth_code_value }}", + "{{ client_id_key }}": "{{ client_id_value }}", + "{{ client_secret_key }}": "{{ client_secret_value }}", } ], title="Access Token Query Params (Json Encoded)", ) - extract_output: List[str] = Field( - ..., + extract_output: Optional[List[str]] = Field( + None, description="The DeclarativeOAuth Specific list of strings to indicate which keys should be extracted and returned back to the input config.", examples=[["access_token", "refresh_token", "other_field"]], title="Extract Output", @@ -966,7 +994,7 @@ class Config: ) oauth_connector_input_specification: Optional[OauthConnectorInputSpecification] = Field( None, - description='The DeclarativeOAuth specific blob.\nPertains to the fields defined by the connector relating to the OAuth flow.\n\nInterpolation capabilities:\n- The variables placeholders are declared as `{my_var}`.\n- The nested resolution variables like `{{my_nested_var}}` is allowed as well.\n\n- The allowed interpolation context is:\n + base64Encoder - encode to `base64`, {base64Encoder:{my_var_a}:{my_var_b}}\n + base64Decorer - decode from `base64` encoded string, {base64Decoder:{my_string_variable_or_string_value}}\n + urlEncoder - encode the input string to URL-like format, {urlEncoder:https://test.host.com/endpoint}\n + urlDecorer - decode the input url-encoded string into text format, {urlDecoder:https%3A%2F%2Fairbyte.io}\n + codeChallengeS256 - get the `codeChallenge` encoded value to provide additional data-provider specific authorisation values, {codeChallengeS256:{state_value}}\n\nExamples:\n - The TikTok Marketing DeclarativeOAuth spec:\n {\n "oauth_connector_input_specification": {\n "type": "object",\n "additionalProperties": false,\n "properties": {\n "consent_url": "https://ads.tiktok.com/marketing_api/auth?{client_id_key}={{client_id_key}}&{redirect_uri_key}={urlEncoder:{{redirect_uri_key}}}&{state_key}={{state_key}}",\n "access_token_url": "https://business-api.tiktok.com/open_api/v1.3/oauth2/access_token/",\n "access_token_params": {\n "{auth_code_key}": "{{auth_code_key}}",\n "{client_id_key}": "{{client_id_key}}",\n "{client_secret_key}": "{{client_secret_key}}"\n },\n "access_token_headers": {\n "Content-Type": "application/json",\n "Accept": "application/json"\n },\n "extract_output": ["data.access_token"],\n "client_id_key": "app_id",\n "client_secret_key": "secret",\n "auth_code_key": "auth_code"\n }\n }\n }', + description='The DeclarativeOAuth specific blob.\nPertains to the fields defined by the connector relating to the OAuth flow.\n\nInterpolation capabilities:\n- The variables placeholders are declared as `{{my_var}}`.\n- The nested resolution variables like `{{ {{my_nested_var}} }}` is allowed as well.\n\n- The allowed interpolation context is:\n + base64Encoder - encode to `base64`, {{ {{my_var_a}}:{{my_var_b}} | base64Encoder }}\n + base64Decorer - decode from `base64` encoded string, {{ {{my_string_variable_or_string_value}} | base64Decoder }}\n + urlEncoder - encode the input string to URL-like format, {{ https://test.host.com/endpoint | urlEncoder}}\n + urlDecorer - decode the input url-encoded string into text format, {{ urlDecoder:https%3A%2F%2Fairbyte.io | urlDecoder}}\n + codeChallengeS256 - get the `codeChallenge` encoded value to provide additional data-provider specific authorisation values, {{ {{state_value}} | codeChallengeS256 }}\n\nExamples:\n - The TikTok Marketing DeclarativeOAuth spec:\n {\n "oauth_connector_input_specification": {\n "type": "object",\n "additionalProperties": false,\n "properties": {\n "consent_url": "https://ads.tiktok.com/marketing_api/auth?{{client_id_key}}={{client_id_value}}&{{redirect_uri_key}}={{ {{redirect_uri_value}} | urlEncoder}}&{{state_key}}={{state_value}}",\n "access_token_url": "https://business-api.tiktok.com/open_api/v1.3/oauth2/access_token/",\n "access_token_params": {\n "{{ auth_code_key }}": "{{ auth_code_value }}",\n "{{ client_id_key }}": "{{ client_id_value }}",\n "{{ client_secret_key }}": "{{ client_secret_value }}"\n },\n "access_token_headers": {\n "Content-Type": "application/json",\n "Accept": "application/json"\n },\n "extract_output": ["data.access_token"],\n "client_id_key": "app_id",\n "client_secret_key": "secret",\n "auth_code_key": "auth_code"\n }\n }\n }', title="DeclarativeOAuth Connector Specification", ) complete_oauth_output_specification: Optional[Dict[str, Any]] = Field( diff --git a/airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py b/airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py index 79fd2c122..a40022587 100644 --- a/airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py +++ b/airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py @@ -1885,15 +1885,24 @@ def create_oauth_authenticator( expires_in_name=InterpolatedString.create( model.expires_in_name or "expires_in", parameters=model.parameters or {} ).eval(config), + client_id_name=InterpolatedString.create( + model.client_id_name or "client_id", parameters=model.parameters or {} + ).eval(config), client_id=InterpolatedString.create( model.client_id, parameters=model.parameters or {} ).eval(config), + client_secret_name=InterpolatedString.create( + model.client_secret_name or "client_secret", parameters=model.parameters or {} + ).eval(config), client_secret=InterpolatedString.create( model.client_secret, parameters=model.parameters or {} ).eval(config), access_token_config_path=model.refresh_token_updater.access_token_config_path, refresh_token_config_path=model.refresh_token_updater.refresh_token_config_path, token_expiry_date_config_path=model.refresh_token_updater.token_expiry_date_config_path, + grant_type_name=InterpolatedString.create( + model.grant_type_name or "grant_type", parameters=model.parameters or {} + ).eval(config), grant_type=InterpolatedString.create( model.grant_type or "refresh_token", parameters=model.parameters or {} ).eval(config), @@ -1911,11 +1920,15 @@ def create_oauth_authenticator( return DeclarativeOauth2Authenticator( # type: ignore access_token_name=model.access_token_name or "access_token", access_token_value=model.access_token_value, + client_id_name=model.client_id_name or "client_id", client_id=model.client_id, + client_secret_name=model.client_secret_name or "client_secret", client_secret=model.client_secret, expires_in_name=model.expires_in_name or "expires_in", + grant_type_name=model.grant_type_name or "grant_type", grant_type=model.grant_type or "refresh_token", refresh_request_body=model.refresh_request_body, + refresh_token_name=model.refresh_token_name or "refresh_token", refresh_token=model.refresh_token, scopes=model.scopes, token_expiry_date=model.token_expiry_date, diff --git a/airbyte_cdk/sources/streams/http/requests_native_auth/abstract_oauth.py b/airbyte_cdk/sources/streams/http/requests_native_auth/abstract_oauth.py index 1f3c1c85e..d70d318fe 100644 --- a/airbyte_cdk/sources/streams/http/requests_native_auth/abstract_oauth.py +++ b/airbyte_cdk/sources/streams/http/requests_native_auth/abstract_oauth.py @@ -81,10 +81,10 @@ def build_refresh_request_body(self) -> Mapping[str, Any]: Override to define additional parameters """ payload: MutableMapping[str, Any] = { - "grant_type": self.get_grant_type(), - "client_id": self.get_client_id(), - "client_secret": self.get_client_secret(), - "refresh_token": self.get_refresh_token(), + self.get_grant_type_name(): self.get_grant_type(), + self.get_client_id_name(): self.get_client_id(), + self.get_client_secret_name(): self.get_client_secret(), + self.get_refresh_token_name(): self.get_refresh_token(), } if self.get_scopes(): @@ -206,14 +206,26 @@ def token_expiry_date_format(self) -> Optional[str]: def get_token_refresh_endpoint(self) -> Optional[str]: """Returns the endpoint to refresh the access token""" + @abstractmethod + def get_client_id_name(self) -> str: + """The client id name to authenticate""" + @abstractmethod def get_client_id(self) -> str: """The client id to authenticate""" + @abstractmethod + def get_client_secret_name(self) -> str: + """The client secret name to authenticate""" + @abstractmethod def get_client_secret(self) -> str: """The client secret to authenticate""" + @abstractmethod + def get_refresh_token_name(self) -> str: + """The refresh token name to authenticate""" + @abstractmethod def get_refresh_token(self) -> Optional[str]: """The token used to refresh the access token when it expires""" @@ -246,6 +258,10 @@ def get_refresh_request_body(self) -> Mapping[str, Any]: def get_grant_type(self) -> str: """Returns grant_type specified for requesting access_token""" + @abstractmethod + def get_grant_type_name(self) -> str: + """Returns grant_type specified name for requesting access_token""" + @property @abstractmethod def access_token(self) -> str: diff --git a/airbyte_cdk/sources/streams/http/requests_native_auth/oauth.py b/airbyte_cdk/sources/streams/http/requests_native_auth/oauth.py index 8e5c71458..3f3111ce8 100644 --- a/airbyte_cdk/sources/streams/http/requests_native_auth/oauth.py +++ b/airbyte_cdk/sources/streams/http/requests_native_auth/oauth.py @@ -30,12 +30,16 @@ def __init__( client_id: str, client_secret: str, refresh_token: str, + client_id_name: str = "client_id", + client_secret_name: str = "client_secret", + refresh_token_name: str = "refresh_token", scopes: List[str] | None = None, token_expiry_date: pendulum.DateTime | None = None, token_expiry_date_format: str | None = None, access_token_name: str = "access_token", expires_in_name: str = "expires_in", refresh_request_body: Mapping[str, Any] | None = None, + grant_type_name: str = "grant_type", grant_type: str = "refresh_token", token_expiry_is_time_of_expiration: bool = False, refresh_token_error_status_codes: Tuple[int, ...] = (), @@ -43,13 +47,17 @@ def __init__( refresh_token_error_values: Tuple[str, ...] = (), ): self._token_refresh_endpoint = token_refresh_endpoint + self._client_secret_name = client_secret_name self._client_secret = client_secret + self._client_id_name = client_id_name self._client_id = client_id + self._refresh_token_name = refresh_token_name self._refresh_token = refresh_token self._scopes = scopes self._access_token_name = access_token_name self._expires_in_name = expires_in_name self._refresh_request_body = refresh_request_body + self._grant_type_name = grant_type_name self._grant_type = grant_type self._token_expiry_date = token_expiry_date or pendulum.now().subtract(days=1) # type: ignore [no-untyped-call] @@ -63,12 +71,21 @@ def __init__( def get_token_refresh_endpoint(self) -> str: return self._token_refresh_endpoint + def get_client_id_name(self) -> str: + return self._client_id_name + def get_client_id(self) -> str: return self._client_id + def get_client_secret_name(self) -> str: + return self._client_secret_name + def get_client_secret(self) -> str: return self._client_secret + def get_refresh_token_name(self) -> str: + return self._refresh_token_name + def get_refresh_token(self) -> str: return self._refresh_token @@ -84,6 +101,9 @@ def get_expires_in_name(self) -> str: def get_refresh_request_body(self) -> Mapping[str, Any]: return self._refresh_request_body # type: ignore [return-value] + def get_grant_type_name(self) -> str: + return self._grant_type_name + def get_grant_type(self) -> str: return self._grant_type @@ -129,8 +149,11 @@ def __init__( expires_in_name: str = "expires_in", refresh_token_name: str = "refresh_token", refresh_request_body: Mapping[str, Any] | None = None, + grant_type_name: str = "grant_type", grant_type: str = "refresh_token", + client_id_name: str = "client_id", client_id: Optional[str] = None, + client_secret_name: str = "client_secret", client_secret: Optional[str] = None, access_token_config_path: Sequence[str] = ("credentials", "access_token"), refresh_token_config_path: Sequence[str] = ("credentials", "refresh_token"), @@ -174,23 +197,30 @@ def __init__( ("credentials", "client_secret"), ) ) + self._client_id_name = client_id_name + self._client_secret_name = client_secret_name self._access_token_config_path = access_token_config_path self._refresh_token_config_path = refresh_token_config_path self._token_expiry_date_config_path = token_expiry_date_config_path self._token_expiry_date_format = token_expiry_date_format self._refresh_token_name = refresh_token_name + self._grant_type_name = grant_type_name self._connector_config = connector_config self.__message_repository = message_repository super().__init__( - token_refresh_endpoint, - self.get_client_id(), - self.get_client_secret(), - self.get_refresh_token(), + token_refresh_endpoint=token_refresh_endpoint, + client_id_name=self._client_id_name, + client_id=self.get_client_id(), + client_secret_name=self._client_secret_name, + client_secret=self.get_client_secret(), + refresh_token=self.get_refresh_token(), + refresh_token_name=self._refresh_token_name, scopes=scopes, token_expiry_date=self.get_token_expiry_date(), access_token_name=access_token_name, expires_in_name=expires_in_name, refresh_request_body=refresh_request_body, + grant_type_name=self._grant_type_name, grant_type=grant_type, token_expiry_date_format=token_expiry_date_format, token_expiry_is_time_of_expiration=token_expiry_is_time_of_expiration, diff --git a/unit_tests/sources/streams/http/requests_native_auth/test_requests_native_auth.py b/unit_tests/sources/streams/http/requests_native_auth/test_requests_native_auth.py index 093c136e5..1fd2c611d 100644 --- a/unit_tests/sources/streams/http/requests_native_auth/test_requests_native_auth.py +++ b/unit_tests/sources/streams/http/requests_native_auth/test_requests_native_auth.py @@ -165,6 +165,41 @@ def test_refresh_request_body(self): } assert body == expected + def test_refresh_request_body_with_keys_override(self): + """ + Request body should match given configuration. + """ + scopes = ["scope1", "scope2"] + oauth = Oauth2Authenticator( + token_refresh_endpoint="refresh_end", + client_id_name="custom_client_id_key", + client_id="some_client_id", + client_secret_name="custom_client_secret_key", + client_secret="some_client_secret", + refresh_token_name="custom_refresh_token_key", + refresh_token="some_refresh_token", + scopes=["scope1", "scope2"], + token_expiry_date=pendulum.now().add(days=3), + grant_type_name="custom_grant_type", + grant_type="some_grant_type", + refresh_request_body={ + "custom_field": "in_outbound_request", + "another_field": "exists_in_body", + "scopes": ["no_override"], + }, + ) + body = oauth.build_refresh_request_body() + expected = { + "custom_grant_type": "some_grant_type", + "custom_client_id_key": "some_client_id", + "custom_client_secret_key": "some_client_secret", + "custom_refresh_token_key": "some_refresh_token", + "scopes": scopes, + "custom_field": "in_outbound_request", + "another_field": "exists_in_body", + } + assert body == expected + def test_refresh_access_token(self, mocker): oauth = Oauth2Authenticator( token_refresh_endpoint="refresh_end",