airbyte_cdk.sources.streams.http.requests_native_auth
1# 2# Copyright (c) 2023 Airbyte, Inc., all rights reserved. 3# 4 5from .oauth import Oauth2Authenticator, SingleUseRefreshTokenOauth2Authenticator 6from .protocols import ResponseAwareAuthenticator, TokenRotatingAuthenticator 7from .token import BasicHttpAuthenticator, MultipleTokenAuthenticator, TokenAuthenticator 8 9__all__ = [ 10 "Oauth2Authenticator", 11 "SingleUseRefreshTokenOauth2Authenticator", 12 "TokenAuthenticator", 13 "MultipleTokenAuthenticator", 14 "BasicHttpAuthenticator", 15 "ResponseAwareAuthenticator", 16 "TokenRotatingAuthenticator", 17]
27class Oauth2Authenticator(AbstractOauth2Authenticator): 28 """ 29 Generates OAuth2.0 access tokens from an OAuth2.0 refresh token and client credentials. 30 The generated access token is attached to each request via the Authorization header. 31 If a connector_config is provided any mutation of it's value in the scope of this class will emit AirbyteControlConnectorConfigMessage. 32 """ 33 34 def __init__( 35 self, 36 token_refresh_endpoint: str, 37 client_id: str, 38 client_secret: str, 39 refresh_token: str, 40 client_id_name: str = "client_id", 41 client_secret_name: str = "client_secret", 42 refresh_token_name: str = "refresh_token", 43 scopes: List[str] | None = None, 44 token_expiry_date: AirbyteDateTime | None = None, 45 token_expiry_date_format: str | None = None, 46 access_token_name: str = "access_token", 47 expires_in_name: str = "expires_in", 48 refresh_request_body: Mapping[str, Any] | None = None, 49 refresh_request_headers: Mapping[str, Any] | None = None, 50 send_refresh_request_as_query_params: bool = False, 51 grant_type_name: str = "grant_type", 52 grant_type: str = "refresh_token", 53 token_expiry_is_time_of_expiration: bool = False, 54 refresh_token_error_status_codes: Tuple[int, ...] = (), 55 refresh_token_error_key: str = "", 56 refresh_token_error_values: Tuple[str, ...] = (), 57 ) -> None: 58 self._token_refresh_endpoint = token_refresh_endpoint 59 self._client_secret_name = client_secret_name 60 self._client_secret = client_secret 61 self._client_id_name = client_id_name 62 self._client_id = client_id 63 self._refresh_token_name = refresh_token_name 64 self._refresh_token = refresh_token 65 self._scopes = scopes 66 self._access_token_name = access_token_name 67 self._expires_in_name = expires_in_name 68 self._refresh_request_body = refresh_request_body 69 self._refresh_request_headers = refresh_request_headers 70 self._send_refresh_request_as_query_params = send_refresh_request_as_query_params 71 self._grant_type_name = grant_type_name 72 self._grant_type = grant_type 73 74 self._token_expiry_date = token_expiry_date or (ab_datetime_now() - timedelta(days=1)) 75 self._token_expiry_date_format = token_expiry_date_format 76 self._token_expiry_is_time_of_expiration = token_expiry_is_time_of_expiration 77 self._access_token = None 78 super().__init__( 79 refresh_token_error_status_codes, refresh_token_error_key, refresh_token_error_values 80 ) 81 82 def get_token_refresh_endpoint(self) -> str: 83 return self._token_refresh_endpoint 84 85 def get_client_id_name(self) -> str: 86 return self._client_id_name 87 88 def get_client_id(self) -> str: 89 return self._client_id 90 91 def get_client_secret_name(self) -> str: 92 return self._client_secret_name 93 94 def get_client_secret(self) -> str: 95 return self._client_secret 96 97 def get_refresh_token_name(self) -> str: 98 return self._refresh_token_name 99 100 def get_refresh_token(self) -> str: 101 return self._refresh_token 102 103 def get_access_token_name(self) -> str: 104 return self._access_token_name 105 106 def get_scopes(self) -> list[str]: 107 return self._scopes # type: ignore[return-value] 108 109 def get_expires_in_name(self) -> str: 110 return self._expires_in_name 111 112 def get_refresh_request_body(self) -> Mapping[str, Any]: 113 return self._refresh_request_body # type: ignore[return-value] 114 115 def get_refresh_request_headers(self) -> Mapping[str, Any]: 116 return self._refresh_request_headers # type: ignore[return-value] 117 118 def should_send_refresh_request_as_query_params(self) -> bool: 119 return self._send_refresh_request_as_query_params 120 121 def get_grant_type_name(self) -> str: 122 return self._grant_type_name 123 124 def get_grant_type(self) -> str: 125 return self._grant_type 126 127 def get_token_expiry_date(self) -> AirbyteDateTime: 128 return self._token_expiry_date 129 130 def set_token_expiry_date(self, value: AirbyteDateTime) -> None: 131 # Overrides of `refresh_access_token` written before 6.45.5 return the raw `expires_in`. 132 if not isinstance(value, datetime): 133 with suppress(ValueError, OverflowError): 134 value = self._parse_token_expiration_date(value) 135 self._token_expiry_date = value 136 137 @property 138 def token_expiry_is_time_of_expiration(self) -> bool: 139 return self._token_expiry_is_time_of_expiration 140 141 @property 142 def token_expiry_date_format(self) -> Optional[str]: 143 return self._token_expiry_date_format 144 145 @property 146 def access_token(self) -> str: 147 return self._access_token # type: ignore[return-value] 148 149 @access_token.setter 150 def access_token(self, value: str) -> None: 151 self._access_token = value # type: ignore[assignment] # Incorrect type for assignment
Generates OAuth2.0 access tokens from an OAuth2.0 refresh token and client credentials. The generated access token is attached to each request via the Authorization header. If a connector_config is provided any mutation of it's value in the scope of this class will emit AirbyteControlConnectorConfigMessage.
34 def __init__( 35 self, 36 token_refresh_endpoint: str, 37 client_id: str, 38 client_secret: str, 39 refresh_token: str, 40 client_id_name: str = "client_id", 41 client_secret_name: str = "client_secret", 42 refresh_token_name: str = "refresh_token", 43 scopes: List[str] | None = None, 44 token_expiry_date: AirbyteDateTime | None = None, 45 token_expiry_date_format: str | None = None, 46 access_token_name: str = "access_token", 47 expires_in_name: str = "expires_in", 48 refresh_request_body: Mapping[str, Any] | None = None, 49 refresh_request_headers: Mapping[str, Any] | None = None, 50 send_refresh_request_as_query_params: bool = False, 51 grant_type_name: str = "grant_type", 52 grant_type: str = "refresh_token", 53 token_expiry_is_time_of_expiration: bool = False, 54 refresh_token_error_status_codes: Tuple[int, ...] = (), 55 refresh_token_error_key: str = "", 56 refresh_token_error_values: Tuple[str, ...] = (), 57 ) -> None: 58 self._token_refresh_endpoint = token_refresh_endpoint 59 self._client_secret_name = client_secret_name 60 self._client_secret = client_secret 61 self._client_id_name = client_id_name 62 self._client_id = client_id 63 self._refresh_token_name = refresh_token_name 64 self._refresh_token = refresh_token 65 self._scopes = scopes 66 self._access_token_name = access_token_name 67 self._expires_in_name = expires_in_name 68 self._refresh_request_body = refresh_request_body 69 self._refresh_request_headers = refresh_request_headers 70 self._send_refresh_request_as_query_params = send_refresh_request_as_query_params 71 self._grant_type_name = grant_type_name 72 self._grant_type = grant_type 73 74 self._token_expiry_date = token_expiry_date or (ab_datetime_now() - timedelta(days=1)) 75 self._token_expiry_date_format = token_expiry_date_format 76 self._token_expiry_is_time_of_expiration = token_expiry_is_time_of_expiration 77 self._access_token = None 78 super().__init__( 79 refresh_token_error_status_codes, refresh_token_error_key, refresh_token_error_values 80 )
If all of refresh_token_error_status_codes, refresh_token_error_key, and refresh_token_error_values are set, then http errors with such params will be wrapped in AirbyteTracedException.
112 def get_refresh_request_body(self) -> Mapping[str, Any]: 113 return self._refresh_request_body # type: ignore[return-value]
Returns the request body to set on the refresh request
115 def get_refresh_request_headers(self) -> Mapping[str, Any]: 116 return self._refresh_request_headers # type: ignore[return-value]
Returns the request headers to set on the refresh request
118 def should_send_refresh_request_as_query_params(self) -> bool: 119 return self._send_refresh_request_as_query_params
Returns True if the standard refresh args should be sent on the URL
query string instead of in the request body.
Defaults to False so existing authenticators retain their previous
behavior (params in body, no query params on the refresh URL). Subclasses
can override this to opt into the URL-query-string shape required by OAuth
providers like Gong.
130 def set_token_expiry_date(self, value: AirbyteDateTime) -> None: 131 # Overrides of `refresh_access_token` written before 6.45.5 return the raw `expires_in`. 132 if not isinstance(value, datetime): 133 with suppress(ValueError, OverflowError): 134 value = self._parse_token_expiration_date(value) 135 self._token_expiry_date = value
Setter for access token expiration date
137 @property 138 def token_expiry_is_time_of_expiration(self) -> bool: 139 return self._token_expiry_is_time_of_expiration
Indicates that the Token Expiry returns the date until which the token will be valid, not the amount of time it will be valid.
141 @property 142 def token_expiry_date_format(self) -> Optional[str]: 143 return self._token_expiry_date_format
Format of the datetime; exists it if expires_in is returned as the expiration datetime instead of seconds until it expires
145 @property 146 def access_token(self) -> str: 147 return self._access_token # type: ignore[return-value]
Returns the access token
Inherited Members
154class SingleUseRefreshTokenOauth2Authenticator(Oauth2Authenticator): 155 """ 156 Authenticator that should be used for API implementing single use refresh tokens: 157 when refreshing access token some API returns a new refresh token that needs to used in the next refresh flow. 158 This authenticator updates the configuration with new refresh token by emitting Airbyte control message from an observed mutation. 159 By default, this authenticator expects a connector config with a "credentials" field with the following nested fields: client_id, 160 client_secret, refresh_token. This behavior can be changed by defining custom config path (using dpath paths) in client_id_config_path, 161 client_secret_config_path, refresh_token_config_path constructor arguments. 162 """ 163 164 def __init__( 165 self, 166 connector_config: Mapping[str, Any], 167 token_refresh_endpoint: str, 168 scopes: List[str] | None = None, 169 access_token_name: str = "access_token", 170 expires_in_name: str = "expires_in", 171 refresh_token_name: str = "refresh_token", 172 refresh_request_body: Mapping[str, Any] | None = None, 173 refresh_request_headers: Mapping[str, Any] | None = None, 174 send_refresh_request_as_query_params: bool = False, 175 grant_type_name: str = "grant_type", 176 grant_type: str = "refresh_token", 177 client_id_name: str = "client_id", 178 client_id: Optional[str] = None, 179 client_secret_name: str = "client_secret", 180 client_secret: Optional[str] = None, 181 access_token_config_path: Sequence[str] = ("credentials", "access_token"), 182 refresh_token_config_path: Sequence[str] = ("credentials", "refresh_token"), 183 token_expiry_date_config_path: Sequence[str] = ("credentials", "token_expiry_date"), 184 token_expiry_date_format: Optional[str] = None, 185 message_repository: MessageRepository = NoopMessageRepository(), 186 token_expiry_is_time_of_expiration: bool = False, 187 refresh_token_error_status_codes: Tuple[int, ...] = (), 188 refresh_token_error_key: str = "", 189 refresh_token_error_values: Tuple[str, ...] = (), 190 ) -> None: 191 """ 192 Args: 193 connector_config (Mapping[str, Any]): The full connector configuration 194 token_refresh_endpoint (str): Full URL to the token refresh endpoint 195 scopes (List[str], optional): List of OAuth scopes to pass in the refresh token request body. Defaults to None. 196 access_token_name (str, optional): Name of the access token field, used to parse the refresh token response. Defaults to "access_token". 197 expires_in_name (str, optional): Name of the name of the field that characterizes when the current access token will expire, used to parse the refresh token response. Defaults to "expires_in". 198 refresh_token_name (str, optional): Name of the name of the refresh token field, used to parse the refresh token response. Defaults to "refresh_token". 199 refresh_request_body (Mapping[str, Any], optional): Custom key value pair that will be added to the refresh token request body. Defaults to None. 200 refresh_request_headers (Mapping[str, Any], optional): Custom key value pair that will be added to the refresh token request headers. Defaults to None. 201 send_refresh_request_as_query_params (bool, optional): When True, the standard refresh args (`grant_type`, `refresh_token`, client credentials when not in an `Authorization` header, scopes, plus any `refresh_request_body` extras) are sent on the URL query string and the request body is emitted empty. Use this for OAuth providers like Gong that document their refresh endpoint with refresh args on the URL query string. Defaults to False. 202 grant_type (str, optional): OAuth grant type. Defaults to "refresh_token". 203 client_id (Optional[str]): The client id to authenticate. If not specified, defaults to credentials.client_id in the config object. 204 client_secret (Optional[str]): The client secret to authenticate. If not specified, defaults to credentials.client_secret in the config object. 205 access_token_config_path (Sequence[str]): Dpath to the access_token field in the connector configuration. Defaults to ("credentials", "access_token"). 206 refresh_token_config_path (Sequence[str]): Dpath to the refresh_token field in the connector configuration. Defaults to ("credentials", "refresh_token"). 207 token_expiry_date_config_path (Sequence[str]): Dpath to the token_expiry_date field in the connector configuration. Defaults to ("credentials", "token_expiry_date"). 208 token_expiry_date_format (Optional[str]): Date format of the token expiry date field (set by expires_in_name). If specified the token expiry date is interpreted as time of expiration, as with token_expiry_is_time_of_expiration, otherwise as number of seconds until expiration. 209 token_expiry_is_time_of_expiration bool: set True it if expires_in is returned as time of expiration instead of the number seconds until expiration 210 message_repository (MessageRepository): the message repository used to emit logs on HTTP requests and control message on config update 211 """ 212 self._connector_config = connector_config 213 self._client_id: str = self._get_config_value_by_path( 214 ("credentials", "client_id"), client_id 215 ) 216 self._client_secret: str = self._get_config_value_by_path( 217 ("credentials", "client_secret"), client_secret 218 ) 219 self._client_id_name = client_id_name 220 self._client_secret_name = client_secret_name 221 self._access_token_config_path = access_token_config_path 222 self._refresh_token_config_path = refresh_token_config_path 223 self._token_expiry_date_config_path = token_expiry_date_config_path 224 self._token_expiry_date_format = token_expiry_date_format 225 self._refresh_token_name = refresh_token_name 226 self._grant_type_name = grant_type_name 227 self._connector_config = connector_config 228 self.__message_repository = message_repository 229 super().__init__( 230 token_refresh_endpoint=token_refresh_endpoint, 231 client_id_name=self._client_id_name, 232 client_id=self._client_id, 233 client_secret_name=self._client_secret_name, 234 client_secret=self._client_secret, 235 refresh_token=self.get_refresh_token(), 236 refresh_token_name=self._refresh_token_name, 237 scopes=scopes, 238 token_expiry_date=self.get_token_expiry_date(), 239 access_token_name=access_token_name, 240 expires_in_name=expires_in_name, 241 refresh_request_body=refresh_request_body, 242 refresh_request_headers=refresh_request_headers, 243 send_refresh_request_as_query_params=send_refresh_request_as_query_params, 244 grant_type_name=self._grant_type_name, 245 grant_type=grant_type, 246 token_expiry_date_format=token_expiry_date_format, 247 token_expiry_is_time_of_expiration=token_expiry_is_time_of_expiration 248 or bool(token_expiry_date_format), 249 refresh_token_error_status_codes=refresh_token_error_status_codes, 250 refresh_token_error_key=refresh_token_error_key, 251 refresh_token_error_values=refresh_token_error_values, 252 ) 253 254 @property 255 def access_token(self) -> str: 256 """ 257 Retrieve the access token from the configuration. 258 259 Returns: 260 str: The access token. 261 """ 262 return self._get_config_value_by_path(self._access_token_config_path) # type: ignore[return-value] 263 264 @access_token.setter 265 def access_token(self, new_access_token: str) -> None: 266 """ 267 Sets a new access token. 268 269 Args: 270 new_access_token (str): The new access token to be set. 271 """ 272 self._set_config_value_by_path(self._access_token_config_path, new_access_token) 273 274 def get_refresh_token(self) -> str: 275 """ 276 Retrieve the refresh token from the configuration. 277 278 This method fetches the refresh token using the configuration path specified 279 by `_refresh_token_config_path`. 280 281 Returns: 282 str: The refresh token as a string. 283 """ 284 return self._get_config_value_by_path(self._refresh_token_config_path) # type: ignore[return-value] 285 286 def set_refresh_token(self, new_refresh_token: str) -> None: 287 """ 288 Updates the refresh token in the configuration. 289 290 Args: 291 new_refresh_token (str): The new refresh token to be set. 292 """ 293 self._set_config_value_by_path(self._refresh_token_config_path, new_refresh_token) 294 295 def get_token_expiry_date(self) -> AirbyteDateTime: 296 """ 297 Retrieves the token expiry date from the configuration. 298 299 This method fetches the token expiry date from the configuration using the specified path. 300 If the expiry date is an empty string, it returns the current date and time minus one day. 301 Otherwise, it parses the expiry date string into an AirbyteDateTime object. 302 303 Returns: 304 AirbyteDateTime: The parsed or calculated token expiry date. 305 306 Raises: 307 TypeError: If the result is not an instance of AirbyteDateTime. 308 """ 309 expiry_date = self._get_config_value_by_path(self._token_expiry_date_config_path) 310 result = ( 311 ab_datetime_now() - timedelta(days=1) 312 if expiry_date == "" 313 else ab_datetime_parse(str(expiry_date)) 314 ) 315 if isinstance(result, AirbyteDateTime): 316 return result 317 raise TypeError("Invalid datetime conversion") 318 319 def set_token_expiry_date(self, new_token_expiry_date: AirbyteDateTime) -> None: # type: ignore[override] 320 """ 321 Sets the token expiry date in the configuration. 322 323 Args: 324 new_token_expiry_date (AirbyteDateTime): The new expiry date for the token. 325 """ 326 if not ( 327 isinstance(new_token_expiry_date, datetime) or self.token_expiry_is_time_of_expiration 328 ): 329 # Overrides of `refresh_access_token` written before 6.45.5 return the raw `expires_in` 330 # in seconds. Other values are stored as-is and parsed on read. 331 with suppress(ValueError, OverflowError): 332 new_token_expiry_date = self._parse_token_expiration_date(new_token_expiry_date) 333 self._set_config_value_by_path( 334 self._token_expiry_date_config_path, str(new_token_expiry_date) 335 ) 336 337 def token_has_expired(self) -> bool: 338 """Returns True if the token is expired""" 339 return ab_datetime_now() > self.get_token_expiry_date() 340 341 def get_access_token(self) -> str: 342 """Retrieve new access and refresh token if the access token has expired. 343 344 This method uses double-checked locking to ensure thread-safe token refresh. 345 This is especially critical for single-use refresh tokens where concurrent 346 refresh attempts would cause failures as the refresh token is invalidated 347 after first use. 348 349 The new refresh token is persisted with the set_refresh_token function. 350 351 Returns: 352 str: The current access_token, updated if it was previously expired. 353 """ 354 if self.token_has_expired(): 355 with self._token_refresh_lock: 356 # Double-check after acquiring lock - another thread may have already refreshed 357 if self.token_has_expired(): 358 self.refresh_and_set_access_token() 359 return self.access_token 360 361 def refresh_and_set_access_token(self) -> None: 362 """Force refresh the access token and update internal state. 363 364 For single-use refresh tokens, this also persists the new refresh token 365 and emits a control message to update the connector config. If the 366 response omits a refresh token, the existing one is preserved. 367 """ 368 token_before_waiting = self._current_access_token_or_none() 369 with self._token_refresh_lock: 370 if self._current_access_token_or_none() != token_before_waiting: 371 return 372 new_access_token, access_token_expires_in, new_refresh_token = ( 373 self.refresh_access_token() 374 ) 375 self.access_token = new_access_token 376 if new_refresh_token is not None: 377 self.set_refresh_token(new_refresh_token) 378 self.set_token_expiry_date(access_token_expires_in) 379 self._emit_control_message() 380 381 def refresh_access_token(self) -> Tuple[str, AirbyteDateTime, Optional[str]]: # type: ignore[override] 382 """ 383 Refreshes the access token by making a handled request and extracting the necessary token information. 384 385 Returns: 386 A tuple of (access_token, token_expiry_date, refresh_token). The refresh token 387 is `None` when the OAuth provider omits it from the response. 388 """ 389 response_json = self._make_handled_request() 390 return ( 391 self._extract_access_token(response_json), 392 self._extract_token_expiry_date(response_json), 393 self._extract_refresh_token(response_json), 394 ) 395 396 def _set_config_value_by_path(self, config_path: Union[str, Sequence[str]], value: Any) -> None: 397 """ 398 Set a value in the connector configuration at the specified path. 399 400 Args: 401 config_path (Union[str, Sequence[str]]): The path within the configuration where the value should be set. 402 This can be a string representing a single key or a sequence of strings representing a nested path. 403 value (Any): The value to set at the specified path in the configuration. 404 405 Returns: 406 None 407 """ 408 dpath.new(self._connector_config, config_path, value) # type: ignore[arg-type] 409 410 def _get_config_value_by_path( 411 self, config_path: Union[str, Sequence[str]], default: Optional[str] = None 412 ) -> str | Any: 413 """ 414 Retrieve a value from the connector configuration using a specified path. 415 416 Args: 417 config_path (Union[str, Sequence[str]]): The path to the desired configuration value. This can be a string or a sequence of strings. 418 default (Optional[str], optional): The default value to return if the specified path does not exist in the configuration. Defaults to None. 419 420 Returns: 421 Any: The value from the configuration at the specified path, or the default value if the path does not exist. 422 """ 423 return dpath.get( 424 self._connector_config, # type: ignore[arg-type] 425 config_path, 426 default=default if default is not None else "", 427 ) 428 429 def _emit_control_message(self) -> None: 430 """ 431 Emits a control message based on the connector configuration. 432 433 Control messages for config updates (like refreshed tokens) must be printed directly 434 to stdout so the platform can process them immediately. The message repository is 435 also used to queue the message for any additional processing. 436 437 Note: 438 The function `emit_configuration_as_airbyte_control_message` prints directly to 439 stdout, which is required for the platform to detect and persist config changes. 440 """ 441 # Always emit to stdout so the platform can process the config update immediately. 442 # This is critical for single-use refresh tokens where the new token must be persisted 443 # before subsequent operations try to use the old (now invalid) token. 444 emit_configuration_as_airbyte_control_message(self._connector_config) # type: ignore[arg-type] 445 446 # Also emit to the message repository for any additional processing (e.g., logging) 447 if not isinstance(self._message_repository, NoopMessageRepository): 448 self._message_repository.emit_message( 449 create_connector_config_control_message(self._connector_config) # type: ignore[arg-type] 450 ) 451 452 @property 453 def _message_repository(self) -> MessageRepository: 454 """ 455 Overriding AbstractOauth2Authenticator._message_repository to allow for HTTP request logs 456 """ 457 return self.__message_repository
Authenticator that should be used for API implementing single use refresh tokens: when refreshing access token some API returns a new refresh token that needs to used in the next refresh flow. This authenticator updates the configuration with new refresh token by emitting Airbyte control message from an observed mutation. By default, this authenticator expects a connector config with a "credentials" field with the following nested fields: client_id, client_secret, refresh_token. This behavior can be changed by defining custom config path (using dpath paths) in client_id_config_path, client_secret_config_path, refresh_token_config_path constructor arguments.
164 def __init__( 165 self, 166 connector_config: Mapping[str, Any], 167 token_refresh_endpoint: str, 168 scopes: List[str] | None = None, 169 access_token_name: str = "access_token", 170 expires_in_name: str = "expires_in", 171 refresh_token_name: str = "refresh_token", 172 refresh_request_body: Mapping[str, Any] | None = None, 173 refresh_request_headers: Mapping[str, Any] | None = None, 174 send_refresh_request_as_query_params: bool = False, 175 grant_type_name: str = "grant_type", 176 grant_type: str = "refresh_token", 177 client_id_name: str = "client_id", 178 client_id: Optional[str] = None, 179 client_secret_name: str = "client_secret", 180 client_secret: Optional[str] = None, 181 access_token_config_path: Sequence[str] = ("credentials", "access_token"), 182 refresh_token_config_path: Sequence[str] = ("credentials", "refresh_token"), 183 token_expiry_date_config_path: Sequence[str] = ("credentials", "token_expiry_date"), 184 token_expiry_date_format: Optional[str] = None, 185 message_repository: MessageRepository = NoopMessageRepository(), 186 token_expiry_is_time_of_expiration: bool = False, 187 refresh_token_error_status_codes: Tuple[int, ...] = (), 188 refresh_token_error_key: str = "", 189 refresh_token_error_values: Tuple[str, ...] = (), 190 ) -> None: 191 """ 192 Args: 193 connector_config (Mapping[str, Any]): The full connector configuration 194 token_refresh_endpoint (str): Full URL to the token refresh endpoint 195 scopes (List[str], optional): List of OAuth scopes to pass in the refresh token request body. Defaults to None. 196 access_token_name (str, optional): Name of the access token field, used to parse the refresh token response. Defaults to "access_token". 197 expires_in_name (str, optional): Name of the name of the field that characterizes when the current access token will expire, used to parse the refresh token response. Defaults to "expires_in". 198 refresh_token_name (str, optional): Name of the name of the refresh token field, used to parse the refresh token response. Defaults to "refresh_token". 199 refresh_request_body (Mapping[str, Any], optional): Custom key value pair that will be added to the refresh token request body. Defaults to None. 200 refresh_request_headers (Mapping[str, Any], optional): Custom key value pair that will be added to the refresh token request headers. Defaults to None. 201 send_refresh_request_as_query_params (bool, optional): When True, the standard refresh args (`grant_type`, `refresh_token`, client credentials when not in an `Authorization` header, scopes, plus any `refresh_request_body` extras) are sent on the URL query string and the request body is emitted empty. Use this for OAuth providers like Gong that document their refresh endpoint with refresh args on the URL query string. Defaults to False. 202 grant_type (str, optional): OAuth grant type. Defaults to "refresh_token". 203 client_id (Optional[str]): The client id to authenticate. If not specified, defaults to credentials.client_id in the config object. 204 client_secret (Optional[str]): The client secret to authenticate. If not specified, defaults to credentials.client_secret in the config object. 205 access_token_config_path (Sequence[str]): Dpath to the access_token field in the connector configuration. Defaults to ("credentials", "access_token"). 206 refresh_token_config_path (Sequence[str]): Dpath to the refresh_token field in the connector configuration. Defaults to ("credentials", "refresh_token"). 207 token_expiry_date_config_path (Sequence[str]): Dpath to the token_expiry_date field in the connector configuration. Defaults to ("credentials", "token_expiry_date"). 208 token_expiry_date_format (Optional[str]): Date format of the token expiry date field (set by expires_in_name). If specified the token expiry date is interpreted as time of expiration, as with token_expiry_is_time_of_expiration, otherwise as number of seconds until expiration. 209 token_expiry_is_time_of_expiration bool: set True it if expires_in is returned as time of expiration instead of the number seconds until expiration 210 message_repository (MessageRepository): the message repository used to emit logs on HTTP requests and control message on config update 211 """ 212 self._connector_config = connector_config 213 self._client_id: str = self._get_config_value_by_path( 214 ("credentials", "client_id"), client_id 215 ) 216 self._client_secret: str = self._get_config_value_by_path( 217 ("credentials", "client_secret"), client_secret 218 ) 219 self._client_id_name = client_id_name 220 self._client_secret_name = client_secret_name 221 self._access_token_config_path = access_token_config_path 222 self._refresh_token_config_path = refresh_token_config_path 223 self._token_expiry_date_config_path = token_expiry_date_config_path 224 self._token_expiry_date_format = token_expiry_date_format 225 self._refresh_token_name = refresh_token_name 226 self._grant_type_name = grant_type_name 227 self._connector_config = connector_config 228 self.__message_repository = message_repository 229 super().__init__( 230 token_refresh_endpoint=token_refresh_endpoint, 231 client_id_name=self._client_id_name, 232 client_id=self._client_id, 233 client_secret_name=self._client_secret_name, 234 client_secret=self._client_secret, 235 refresh_token=self.get_refresh_token(), 236 refresh_token_name=self._refresh_token_name, 237 scopes=scopes, 238 token_expiry_date=self.get_token_expiry_date(), 239 access_token_name=access_token_name, 240 expires_in_name=expires_in_name, 241 refresh_request_body=refresh_request_body, 242 refresh_request_headers=refresh_request_headers, 243 send_refresh_request_as_query_params=send_refresh_request_as_query_params, 244 grant_type_name=self._grant_type_name, 245 grant_type=grant_type, 246 token_expiry_date_format=token_expiry_date_format, 247 token_expiry_is_time_of_expiration=token_expiry_is_time_of_expiration 248 or bool(token_expiry_date_format), 249 refresh_token_error_status_codes=refresh_token_error_status_codes, 250 refresh_token_error_key=refresh_token_error_key, 251 refresh_token_error_values=refresh_token_error_values, 252 )
Arguments:
- connector_config (Mapping[str, Any]): The full connector configuration
- token_refresh_endpoint (str): Full URL to the token refresh endpoint
- scopes (List[str], optional): List of OAuth scopes to pass in the refresh token request body. Defaults to None.
- access_token_name (str, optional): Name of the access token field, used to parse the refresh token response. Defaults to "access_token".
- expires_in_name (str, optional): Name of the name of the field that characterizes when the current access token will expire, used to parse the refresh token response. Defaults to "expires_in".
- refresh_token_name (str, optional): Name of the name of the refresh token field, used to parse the refresh token response. Defaults to "refresh_token".
- refresh_request_body (Mapping[str, Any], optional): Custom key value pair that will be added to the refresh token request body. Defaults to None.
- refresh_request_headers (Mapping[str, Any], optional): Custom key value pair that will be added to the refresh token request headers. Defaults to None.
- send_refresh_request_as_query_params (bool, optional): When True, the standard refresh args (
grant_type,refresh_token, client credentials when not in anAuthorizationheader, scopes, plus anyrefresh_request_bodyextras) are sent on the URL query string and the request body is emitted empty. Use this for OAuth providers like Gong that document their refresh endpoint with refresh args on the URL query string. Defaults to False. - grant_type (str, optional): OAuth grant type. Defaults to "refresh_token".
- client_id (Optional[str]): The client id to authenticate. If not specified, defaults to credentials.client_id in the config object.
- client_secret (Optional[str]): The client secret to authenticate. If not specified, defaults to credentials.client_secret in the config object.
- access_token_config_path (Sequence[str]): Dpath to the access_token field in the connector configuration. Defaults to ("credentials", "access_token").
- refresh_token_config_path (Sequence[str]): Dpath to the refresh_token field in the connector configuration. Defaults to ("credentials", "refresh_token").
- token_expiry_date_config_path (Sequence[str]): Dpath to the token_expiry_date field in the connector configuration. Defaults to ("credentials", "token_expiry_date").
- token_expiry_date_format (Optional[str]): Date format of the token expiry date field (set by expires_in_name). If specified the token expiry date is interpreted as time of expiration, as with token_expiry_is_time_of_expiration, otherwise as number of seconds until expiration.
- token_expiry_is_time_of_expiration bool: set True it if expires_in is returned as time of expiration instead of the number seconds until expiration
- message_repository (MessageRepository): the message repository used to emit logs on HTTP requests and control message on config update
254 @property 255 def access_token(self) -> str: 256 """ 257 Retrieve the access token from the configuration. 258 259 Returns: 260 str: The access token. 261 """ 262 return self._get_config_value_by_path(self._access_token_config_path) # type: ignore[return-value]
Retrieve the access token from the configuration.
Returns:
str: The access token.
274 def get_refresh_token(self) -> str: 275 """ 276 Retrieve the refresh token from the configuration. 277 278 This method fetches the refresh token using the configuration path specified 279 by `_refresh_token_config_path`. 280 281 Returns: 282 str: The refresh token as a string. 283 """ 284 return self._get_config_value_by_path(self._refresh_token_config_path) # type: ignore[return-value]
Retrieve the refresh token from the configuration.
This method fetches the refresh token using the configuration path specified
by _refresh_token_config_path.
Returns:
str: The refresh token as a string.
286 def set_refresh_token(self, new_refresh_token: str) -> None: 287 """ 288 Updates the refresh token in the configuration. 289 290 Args: 291 new_refresh_token (str): The new refresh token to be set. 292 """ 293 self._set_config_value_by_path(self._refresh_token_config_path, new_refresh_token)
Updates the refresh token in the configuration.
Arguments:
- new_refresh_token (str): The new refresh token to be set.
295 def get_token_expiry_date(self) -> AirbyteDateTime: 296 """ 297 Retrieves the token expiry date from the configuration. 298 299 This method fetches the token expiry date from the configuration using the specified path. 300 If the expiry date is an empty string, it returns the current date and time minus one day. 301 Otherwise, it parses the expiry date string into an AirbyteDateTime object. 302 303 Returns: 304 AirbyteDateTime: The parsed or calculated token expiry date. 305 306 Raises: 307 TypeError: If the result is not an instance of AirbyteDateTime. 308 """ 309 expiry_date = self._get_config_value_by_path(self._token_expiry_date_config_path) 310 result = ( 311 ab_datetime_now() - timedelta(days=1) 312 if expiry_date == "" 313 else ab_datetime_parse(str(expiry_date)) 314 ) 315 if isinstance(result, AirbyteDateTime): 316 return result 317 raise TypeError("Invalid datetime conversion")
Retrieves the token expiry date from the configuration.
This method fetches the token expiry date from the configuration using the specified path. If the expiry date is an empty string, it returns the current date and time minus one day. Otherwise, it parses the expiry date string into an AirbyteDateTime object.
Returns:
AirbyteDateTime: The parsed or calculated token expiry date.
Raises:
- TypeError: If the result is not an instance of AirbyteDateTime.
319 def set_token_expiry_date(self, new_token_expiry_date: AirbyteDateTime) -> None: # type: ignore[override] 320 """ 321 Sets the token expiry date in the configuration. 322 323 Args: 324 new_token_expiry_date (AirbyteDateTime): The new expiry date for the token. 325 """ 326 if not ( 327 isinstance(new_token_expiry_date, datetime) or self.token_expiry_is_time_of_expiration 328 ): 329 # Overrides of `refresh_access_token` written before 6.45.5 return the raw `expires_in` 330 # in seconds. Other values are stored as-is and parsed on read. 331 with suppress(ValueError, OverflowError): 332 new_token_expiry_date = self._parse_token_expiration_date(new_token_expiry_date) 333 self._set_config_value_by_path( 334 self._token_expiry_date_config_path, str(new_token_expiry_date) 335 )
Sets the token expiry date in the configuration.
Arguments:
- new_token_expiry_date (AirbyteDateTime): The new expiry date for the token.
337 def token_has_expired(self) -> bool: 338 """Returns True if the token is expired""" 339 return ab_datetime_now() > self.get_token_expiry_date()
Returns True if the token is expired
341 def get_access_token(self) -> str: 342 """Retrieve new access and refresh token if the access token has expired. 343 344 This method uses double-checked locking to ensure thread-safe token refresh. 345 This is especially critical for single-use refresh tokens where concurrent 346 refresh attempts would cause failures as the refresh token is invalidated 347 after first use. 348 349 The new refresh token is persisted with the set_refresh_token function. 350 351 Returns: 352 str: The current access_token, updated if it was previously expired. 353 """ 354 if self.token_has_expired(): 355 with self._token_refresh_lock: 356 # Double-check after acquiring lock - another thread may have already refreshed 357 if self.token_has_expired(): 358 self.refresh_and_set_access_token() 359 return self.access_token
Retrieve new access and refresh token if the access token has expired.
This method uses double-checked locking to ensure thread-safe token refresh. This is especially critical for single-use refresh tokens where concurrent refresh attempts would cause failures as the refresh token is invalidated after first use.
The new refresh token is persisted with the set_refresh_token function.
Returns:
str: The current access_token, updated if it was previously expired.
361 def refresh_and_set_access_token(self) -> None: 362 """Force refresh the access token and update internal state. 363 364 For single-use refresh tokens, this also persists the new refresh token 365 and emits a control message to update the connector config. If the 366 response omits a refresh token, the existing one is preserved. 367 """ 368 token_before_waiting = self._current_access_token_or_none() 369 with self._token_refresh_lock: 370 if self._current_access_token_or_none() != token_before_waiting: 371 return 372 new_access_token, access_token_expires_in, new_refresh_token = ( 373 self.refresh_access_token() 374 ) 375 self.access_token = new_access_token 376 if new_refresh_token is not None: 377 self.set_refresh_token(new_refresh_token) 378 self.set_token_expiry_date(access_token_expires_in) 379 self._emit_control_message()
Force refresh the access token and update internal state.
For single-use refresh tokens, this also persists the new refresh token and emits a control message to update the connector config. If the response omits a refresh token, the existing one is preserved.
381 def refresh_access_token(self) -> Tuple[str, AirbyteDateTime, Optional[str]]: # type: ignore[override] 382 """ 383 Refreshes the access token by making a handled request and extracting the necessary token information. 384 385 Returns: 386 A tuple of (access_token, token_expiry_date, refresh_token). The refresh token 387 is `None` when the OAuth provider omits it from the response. 388 """ 389 response_json = self._make_handled_request() 390 return ( 391 self._extract_access_token(response_json), 392 self._extract_token_expiry_date(response_json), 393 self._extract_refresh_token(response_json), 394 )
Refreshes the access token by making a handled request and extracting the necessary token information.
Returns:
A tuple of (access_token, token_expiry_date, refresh_token). The refresh token is
Nonewhen the OAuth provider omits it from the response.
Inherited Members
- Oauth2Authenticator
- get_token_refresh_endpoint
- get_client_id_name
- get_client_id
- get_client_secret_name
- get_client_secret
- get_refresh_token_name
- get_access_token_name
- get_scopes
- get_expires_in_name
- get_refresh_request_body
- get_refresh_request_headers
- should_send_refresh_request_as_query_params
- get_grant_type_name
- get_grant_type
- token_expiry_is_time_of_expiration
- token_expiry_date_format
39class TokenAuthenticator(AbstractHeaderAuthenticator): 40 """ 41 Builds auth header, based on the token provided. 42 The token is attached to each request via the `auth_header` header. 43 """ 44 45 @property 46 def auth_header(self) -> str: 47 return self._auth_header 48 49 @property 50 def token(self) -> str: 51 return f"{self._auth_method} {self._token}" 52 53 def __init__(self, token: str, auth_method: str = "Bearer", auth_header: str = "Authorization"): 54 self._auth_header = auth_header 55 self._auth_method = auth_method 56 self._token = token
Builds auth header, based on the token provided.
The token is attached to each request via the auth_header header.
15class MultipleTokenAuthenticator(AbstractHeaderAuthenticator): 16 """ 17 Builds auth header, based on the list of tokens provided. 18 Auth header is changed per each `get_auth_header` call, using each token in cycle. 19 The token is attached to each request via the `auth_header` header. 20 """ 21 22 @property 23 def auth_header(self) -> str: 24 return self._auth_header 25 26 @property 27 def token(self) -> str: 28 return f"{self._auth_method} {next(self._tokens_iter)}" 29 30 def __init__( 31 self, tokens: List[str], auth_method: str = "Bearer", auth_header: str = "Authorization" 32 ): 33 self._auth_method = auth_method 34 self._auth_header = auth_header 35 self._tokens = tokens 36 self._tokens_iter = cycle(self._tokens)
Builds auth header, based on the list of tokens provided.
Auth header is changed per each get_auth_header call, using each token in cycle.
The token is attached to each request via the auth_header header.
59class BasicHttpAuthenticator(AbstractHeaderAuthenticator): 60 """ 61 Builds auth based off the basic authentication scheme as defined by RFC 7617, which transmits credentials as USER ID/password pairs, encoded using bas64 62 https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#basic_authentication_scheme 63 """ 64 65 @property 66 def auth_header(self) -> str: 67 return self._auth_header 68 69 @property 70 def token(self) -> str: 71 return f"{self._auth_method} {self._token}" 72 73 def __init__( 74 self, 75 username: str, 76 password: str = "", 77 auth_method: str = "Basic", 78 auth_header: str = "Authorization", 79 ): 80 auth_string = f"{username}:{password}".encode("utf8") 81 b64_encoded = base64.b64encode(auth_string).decode("utf8") 82 self._auth_header = auth_header 83 self._auth_method = auth_method 84 self._token = b64_encoded
Builds auth based off the basic authentication scheme as defined by RFC 7617, which transmits credentials as USER ID/password pairs, encoded using bas64 https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#basic_authentication_scheme
73 def __init__( 74 self, 75 username: str, 76 password: str = "", 77 auth_method: str = "Basic", 78 auth_header: str = "Authorization", 79 ): 80 auth_string = f"{username}:{password}".encode("utf8") 81 b64_encoded = base64.b64encode(auth_string).decode("utf8") 82 self._auth_header = auth_header 83 self._auth_method = auth_method 84 self._token = b64_encoded
30@runtime_checkable 31class ResponseAwareAuthenticator(Protocol): 32 """An authenticator that wants to see responses, not just sign requests. 33 34 Authenticators tracking per-token quota need a feedback channel: without one they can only 35 guess at the server's view of the quota and cannot tell that a token has been rejected. 36 `HttpClient` calls `update_from_response` once per attempt on any authenticator that 37 implements this, skipping replayed cache hits -- a cached response carries stale rate-limit 38 headers and consumed no quota. 39 40 Implementations must not raise for a response they cannot interpret, and must be safe to 41 call from multiple threads. 42 """ 43 44 def update_from_response( 45 self, request: requests.PreparedRequest, response: requests.Response 46 ) -> None: 47 pass
An authenticator that wants to see responses, not just sign requests.
Authenticators tracking per-token quota need a feedback channel: without one they can only
guess at the server's view of the quota and cannot tell that a token has been rejected.
HttpClient calls update_from_response once per attempt on any authenticator that
implements this, skipping replayed cache hits -- a cached response carries stale rate-limit
headers and consumed no quota.
Implementations must not raise for a response they cannot interpret, and must be safe to call from multiple threads.
1739def _no_init_or_replace_init(self, *args, **kwargs): 1740 cls = type(self) 1741 1742 if cls._is_protocol: 1743 raise TypeError('Protocols cannot be instantiated') 1744 1745 # Already using a custom `__init__`. No need to calculate correct 1746 # `__init__` to call. This can lead to RecursionError. See bpo-45121. 1747 if cls.__init__ is not _no_init_or_replace_init: 1748 return 1749 1750 # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`. 1751 # The first instantiation of the subclass will call `_no_init_or_replace_init` which 1752 # searches for a proper new `__init__` in the MRO. The new `__init__` 1753 # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent 1754 # instantiation of the protocol subclass will thus use the new 1755 # `__init__` and no longer call `_no_init_or_replace_init`. 1756 for base in cls.__mro__: 1757 init = base.__dict__.get('__init__', _no_init_or_replace_init) 1758 if init is not _no_init_or_replace_init: 1759 cls.__init__ = init 1760 break 1761 else: 1762 # should not happen 1763 cls.__init__ = object.__init__ 1764 1765 cls.__init__(self, *args, **kwargs)
50@runtime_checkable 51class TokenRotatingAuthenticator(Protocol): 52 """An authenticator holding several interchangeable credentials. 53 54 A rate-limit backoff is computed from the response alone, so it assumes the only way 55 forward is for that quota to come back. An authenticator with a spare credential knows 56 better. `HttpClient` asks before sleeping out a rate-limit window. 57 58 Implementations must only answer True when retrying immediately would actually use a 59 different credential -- otherwise the retry hammers the same rejected one. 60 """ 61 62 def has_alternative_token(self, request: requests.PreparedRequest) -> bool: 63 pass
An authenticator holding several interchangeable credentials.
A rate-limit backoff is computed from the response alone, so it assumes the only way
forward is for that quota to come back. An authenticator with a spare credential knows
better. HttpClient asks before sleeping out a rate-limit window.
Implementations must only answer True when retrying immediately would actually use a different credential -- otherwise the retry hammers the same rejected one.
1739def _no_init_or_replace_init(self, *args, **kwargs): 1740 cls = type(self) 1741 1742 if cls._is_protocol: 1743 raise TypeError('Protocols cannot be instantiated') 1744 1745 # Already using a custom `__init__`. No need to calculate correct 1746 # `__init__` to call. This can lead to RecursionError. See bpo-45121. 1747 if cls.__init__ is not _no_init_or_replace_init: 1748 return 1749 1750 # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`. 1751 # The first instantiation of the subclass will call `_no_init_or_replace_init` which 1752 # searches for a proper new `__init__` in the MRO. The new `__init__` 1753 # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent 1754 # instantiation of the protocol subclass will thus use the new 1755 # `__init__` and no longer call `_no_init_or_replace_init`. 1756 for base in cls.__mro__: 1757 init = base.__dict__.get('__init__', _no_init_or_replace_init) 1758 if init is not _no_init_or_replace_init: 1759 cls.__init__ = init 1760 break 1761 else: 1762 # should not happen 1763 cls.__init__ = object.__init__ 1764 1765 cls.__init__(self, *args, **kwargs)