airbyte_cdk.sources.streams.http.requests_native_auth.abstract_oauth
1# 2# Copyright (c) 2023 Airbyte, Inc., all rights reserved. 3# 4 5import logging 6import threading 7from abc import abstractmethod 8from datetime import timedelta 9from json import JSONDecodeError 10from typing import Any, List, Mapping, MutableMapping, Optional, Tuple, Union 11 12import backoff 13import requests 14from requests.auth import AuthBase 15 16from airbyte_cdk.models import FailureType, Level 17from airbyte_cdk.sources.http_logger import format_http_message 18from airbyte_cdk.sources.message import MessageRepository, NoopMessageRepository 19from airbyte_cdk.utils import AirbyteTracedException 20from airbyte_cdk.utils.airbyte_secrets_utils import add_to_secrets 21from airbyte_cdk.utils.datetime_helpers import AirbyteDateTime, ab_datetime_now, ab_datetime_parse 22 23from ..exceptions import DefaultBackoffException 24 25logger = logging.getLogger("airbyte") 26_NOOP_MESSAGE_REPOSITORY = NoopMessageRepository() 27 28 29class ResponseKeysMaxRecurtionReached(AirbyteTracedException): 30 """ 31 Raised when the max level of recursion is reached, when trying to 32 find-and-get the target key, during the `_make_handled_request` 33 """ 34 35 36class AbstractOauth2Authenticator(AuthBase): 37 """ 38 Abstract class for an OAuth authenticators that implements the OAuth token refresh flow. The authenticator 39 is designed to generically perform the refresh flow without regard to how config fields are get/set by 40 delegating that behavior to the classes implementing the interface. 41 """ 42 43 _NO_STREAM_NAME = None 44 45 # Class-level lock to prevent concurrent token refresh across multiple authenticator instances. 46 # This is necessary because multiple streams may share the same OAuth credentials (refresh token) 47 # through the connector config. Without this lock, concurrent refresh attempts can cause race 48 # conditions where one stream successfully refreshes the token while others fail because the 49 # refresh token has been invalidated (especially for single-use refresh tokens). 50 _token_refresh_lock: threading.Lock = threading.Lock() 51 52 def __init__( 53 self, 54 refresh_token_error_status_codes: Tuple[int, ...] = (), 55 refresh_token_error_key: str = "", 56 refresh_token_error_values: Tuple[str, ...] = (), 57 ) -> None: 58 """ 59 If all of refresh_token_error_status_codes, refresh_token_error_key, and refresh_token_error_values are set, 60 then http errors with such params will be wrapped in AirbyteTracedException. 61 """ 62 self._refresh_token_error_status_codes = refresh_token_error_status_codes 63 self._refresh_token_error_key = refresh_token_error_key 64 self._refresh_token_error_values = refresh_token_error_values 65 66 def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest: 67 """Attach the HTTP headers required to authenticate on the HTTP request""" 68 request.headers.update(self.get_auth_header()) 69 return request 70 71 @property 72 def _is_access_token_flow(self) -> bool: 73 return self.get_token_refresh_endpoint() is None and self.access_token is not None 74 75 @property 76 def token_expiry_is_time_of_expiration(self) -> bool: 77 """ 78 Indicates that the Token Expiry returns the date until which the token will be valid, not the amount of time it will be valid. 79 """ 80 81 return False 82 83 @property 84 def token_expiry_date_format(self) -> Optional[str]: 85 """ 86 Format of the datetime; exists it if expires_in is returned as the expiration datetime instead of seconds until it expires 87 """ 88 89 return None 90 91 def get_auth_header(self) -> Mapping[str, Any]: 92 """HTTP header to set on the requests""" 93 token = self.access_token if self._is_access_token_flow else self.get_access_token() 94 return {"Authorization": f"Bearer {token}"} 95 96 def get_access_token(self) -> str: 97 """ 98 Returns the access token. 99 100 This method uses double-checked locking to ensure thread-safe token refresh. 101 When multiple threads (streams) detect an expired token simultaneously, only one 102 will perform the refresh while others wait. After acquiring the lock, the token 103 expiry is re-checked to avoid redundant refresh attempts. 104 """ 105 if self.token_has_expired(): 106 with self._token_refresh_lock: 107 # Double-check after acquiring lock - another thread may have already refreshed 108 if self.token_has_expired(): 109 self.refresh_and_set_access_token() 110 111 return self.access_token 112 113 def refresh_and_set_access_token(self) -> None: 114 """Force refresh the access token and update internal state. 115 116 This method refreshes the access token regardless of whether it has expired, 117 and updates the internal token and expiry date. Subclasses may override this 118 to handle additional state updates (e.g., persisting new refresh tokens). 119 """ 120 token, expires_in = self.refresh_access_token() 121 self.access_token = token 122 self.set_token_expiry_date(expires_in) 123 124 def token_has_expired(self) -> bool: 125 """Returns True if the token is expired""" 126 return ab_datetime_now() > self.get_token_expiry_date() 127 128 def _build_standard_refresh_args(self) -> MutableMapping[str, Any]: 129 """Build the standard OAuth refresh args (grant_type, refresh_token, client 130 credentials, scopes, plus any user-configured `refresh_request_body` extras). 131 132 Used by both `build_refresh_request_body()` and 133 `build_refresh_request_query_params()` so the same set of args can be emitted 134 in either the body or the URL query string depending on 135 `should_send_refresh_request_as_query_params()`. 136 137 Client credentials (client_id and client_secret) are excluded when 138 `refresh_request_headers` contains an `Authorization` header (e.g. Basic 139 auth). This is required by OAuth providers like Gong that expect credentials 140 ONLY in the Authorization header and reject requests that include them in 141 both places. 142 """ 143 headers = self.get_refresh_request_headers() 144 credentials_in_header = headers and "Authorization" in headers 145 include_client_credentials = not credentials_in_header 146 147 payload: MutableMapping[str, Any] = { 148 self.get_grant_type_name(): self.get_grant_type(), 149 } 150 151 if include_client_credentials: 152 payload[self.get_client_id_name()] = self.get_client_id() 153 payload[self.get_client_secret_name()] = self.get_client_secret() 154 155 payload[self.get_refresh_token_name()] = self.get_refresh_token() 156 157 if self.get_scopes(): 158 payload["scopes"] = self.get_scopes() 159 160 if self.get_refresh_request_body(): 161 for key, val in self.get_refresh_request_body().items(): 162 # Existing oauth args take precedence over custom configured fields. 163 if key not in payload: 164 payload[key] = val 165 166 return payload 167 168 def build_refresh_request_body(self) -> Mapping[str, Any]: 169 """Returns the request body to set on the refresh request. 170 171 When `should_send_refresh_request_as_query_params()` is `True`, the standard 172 refresh args are emitted on the URL query string instead and this method 173 returns an empty body. This supports OAuth providers like Gong that document 174 their refresh endpoint as a `POST` with parameters on the URL query string 175 and an empty body. 176 """ 177 if self.should_send_refresh_request_as_query_params(): 178 return {} 179 return self._build_standard_refresh_args() 180 181 def build_refresh_request_headers(self) -> Mapping[str, Any] | None: 182 """ 183 Returns the request headers to set on the refresh request 184 185 """ 186 headers = self.get_refresh_request_headers() 187 return headers if headers else None 188 189 def build_refresh_request_query_params(self) -> Mapping[str, Any] | None: 190 """Returns the URL query string parameters to set on the refresh request. 191 192 When `should_send_refresh_request_as_query_params()` is `True`, the standard 193 refresh args (grant_type, refresh_token, client credentials, scopes, plus 194 any user-configured `refresh_request_body` extras) are returned here and 195 `build_refresh_request_body()` returns an empty body. 196 197 Returns `None` otherwise so existing authenticators retain their previous 198 behavior (no query params on the refresh URL). 199 """ 200 if not self.should_send_refresh_request_as_query_params(): 201 return None 202 return self._build_standard_refresh_args() 203 204 def refresh_access_token(self) -> Tuple[str, AirbyteDateTime]: 205 """ 206 Returns the refresh token and its expiration datetime 207 208 :return: a tuple of (access_token, token_lifespan) 209 """ 210 try: 211 response_json = self._make_handled_request() 212 except ( 213 requests.exceptions.ConnectionError, 214 requests.exceptions.ConnectTimeout, 215 requests.exceptions.ReadTimeout, 216 ) as e: 217 raise AirbyteTracedException( 218 message="OAuth access token refresh request failed due to a network error.", 219 internal_message=f"Network error during OAuth token refresh after retries were exhausted: {e}", 220 failure_type=FailureType.transient_error, 221 ) from e 222 self._ensure_access_token_in_response(response_json) 223 224 return ( 225 self._extract_access_token(response_json), 226 self._extract_token_expiry_date(response_json), 227 ) 228 229 # ---------------- 230 # PRIVATE METHODS 231 # ---------------- 232 233 def _default_token_expiry_date(self) -> AirbyteDateTime: 234 """ 235 Returns the default token expiry date 236 """ 237 # 1 hour was chosen as a middle ground to avoid unnecessary frequent refreshes and token expiration 238 default_token_expiry_duration_hours = 1 # 1 hour 239 return ab_datetime_now() + timedelta(hours=default_token_expiry_duration_hours) 240 241 def _wrap_refresh_token_exception( 242 self, exception: requests.exceptions.RequestException 243 ) -> bool: 244 """ 245 Wraps and handles exceptions that occur during the refresh token process. 246 247 This method checks if the provided exception is related to a refresh token error 248 by examining the response status code and specific error content. 249 250 Args: 251 exception (requests.exceptions.RequestException): The exception raised during the request. 252 253 Returns: 254 bool: True if the exception is related to a refresh token error, False otherwise. 255 """ 256 try: 257 if exception.response is not None: 258 exception_content = exception.response.json() 259 else: 260 return False 261 except JSONDecodeError: 262 return False 263 return ( 264 exception.response.status_code in self._refresh_token_error_status_codes 265 and exception_content.get(self._refresh_token_error_key) 266 in self._refresh_token_error_values 267 ) 268 269 @backoff.on_exception( 270 backoff.expo, 271 ( 272 DefaultBackoffException, 273 requests.exceptions.ConnectionError, 274 requests.exceptions.ConnectTimeout, 275 requests.exceptions.ReadTimeout, 276 ), 277 on_backoff=lambda details: logger.info( 278 f"Caught retryable error after {details['tries']} tries. Waiting {details['wait']} seconds then retrying..." 279 ), 280 max_time=300, 281 ) 282 def _make_handled_request(self) -> Any: 283 """ 284 Makes a handled HTTP request to refresh an OAuth token. 285 286 This method sends a POST request to the token refresh endpoint with the necessary 287 headers and body to obtain a new access token. It handles various exceptions that 288 may occur during the request and logs the response for troubleshooting purposes. 289 290 Returns: 291 Mapping[str, Any]: The JSON response from the token refresh endpoint. 292 293 Raises: 294 DefaultBackoffException: If the response status code is 429 (Too Many Requests) 295 or any 5xx server error. 296 AirbyteTracedException: If the refresh token is invalid or expired, prompting 297 re-authentication. 298 Exception: For any other exceptions that occur during the request. 299 """ 300 try: 301 response = requests.request( 302 method="POST", 303 url=self.get_token_refresh_endpoint(), # type: ignore # returns None, if not provided, but str | bytes is expected. 304 data=self.build_refresh_request_body(), 305 headers=self.build_refresh_request_headers(), 306 params=self.build_refresh_request_query_params(), 307 ) 308 309 if not response.ok: 310 # log the response even if the request failed for troubleshooting purposes 311 self._log_response(response) 312 response.raise_for_status() 313 314 response_json = response.json() 315 316 try: 317 # extract the access token and add to secrets to avoid logging the raw value 318 access_key = self._extract_access_token(response_json) 319 if access_key: 320 add_to_secrets(access_key) 321 except ResponseKeysMaxRecurtionReached as e: 322 # could not find the access token in the response, so do nothing 323 pass 324 325 self._log_response(response) 326 327 return response_json 328 except requests.exceptions.RequestException as e: 329 if e.response is not None: 330 if e.response.status_code == 429 or e.response.status_code >= 500: 331 raise DefaultBackoffException( 332 request=e.response.request, 333 response=e.response, 334 failure_type=FailureType.transient_error, 335 ) 336 if self._wrap_refresh_token_exception(e): 337 response_info = ( 338 f"HTTP {e.response.status_code}: {e.response.text[:1000]}" 339 if e.response is not None 340 else str(e) 341 ) 342 raise AirbyteTracedException( 343 internal_message=f"Refresh token rejected by the OAuth token endpoint. {response_info}", 344 message="Refresh token was rejected by the OAuth provider (invalid, expired, or already used). Re-authenticate this source's credentials in its connection settings.", 345 failure_type=FailureType.config_error, 346 ) from e 347 raise 348 except Exception as e: 349 raise AirbyteTracedException( 350 message="OAuth access token refresh request failed.", 351 internal_message=f"Unexpected error during OAuth token refresh: {e}", 352 failure_type=FailureType.system_error, 353 ) from e 354 355 def _ensure_access_token_in_response(self, response_data: Mapping[str, Any]) -> None: 356 """ 357 Ensures that the access token is present in the response data. 358 359 This method attempts to extract the access token from the provided response data. 360 If the access token is not found, it raises an exception indicating that the token 361 refresh API response was missing the access token. 362 363 Args: 364 response_data (Mapping[str, Any]): The response data from which to extract the access token. 365 366 Raises: 367 Exception: If the access token is not found in the response data. 368 ResponseKeysMaxRecurtionReached: If the maximum recursion depth is reached while extracting the access token. 369 """ 370 try: 371 access_key = self._extract_access_token(response_data) 372 if not access_key: 373 raise Exception( 374 f"Token refresh API response was missing access token {self.get_access_token_name()}" 375 ) 376 except ResponseKeysMaxRecurtionReached as e: 377 raise e 378 379 def _parse_token_expiration_date(self, value: Union[str, int]) -> AirbyteDateTime: 380 """ 381 Parse a string or integer token expiration date into a datetime object 382 383 :return: expiration datetime 384 """ 385 if self.token_expiry_is_time_of_expiration: 386 if not self.token_expiry_date_format: 387 raise ValueError( 388 f"Invalid token expiry date format {self.token_expiry_date_format}; a string representing the format is required." 389 ) 390 try: 391 return ab_datetime_parse(str(value)) 392 except ValueError as e: 393 raise ValueError(f"Invalid token expiry date format: {e}") 394 else: 395 try: 396 # Only accept numeric values (as int/float/string) when no format specified 397 seconds = int(float(str(value))) 398 return ab_datetime_now() + timedelta(seconds=seconds) 399 except (ValueError, TypeError): 400 raise ValueError( 401 f"Invalid expires_in value: {value}. Expected number of seconds when no format specified." 402 ) 403 404 def _extract_access_token(self, response_data: Mapping[str, Any]) -> Any: 405 """ 406 Extracts the access token from the given response data. 407 408 Args: 409 response_data (Mapping[str, Any]): The response data from which to extract the access token. 410 411 Returns: 412 str: The extracted access token. 413 """ 414 return self._find_and_get_value_from_response(response_data, self.get_access_token_name()) 415 416 def _extract_refresh_token(self, response_data: Mapping[str, Any]) -> Any: 417 """ 418 Extracts the refresh token from the given response data. 419 420 Args: 421 response_data (Mapping[str, Any]): The response data from which to extract the refresh token. 422 423 Returns: 424 str: The extracted refresh token. 425 """ 426 return self._find_and_get_value_from_response(response_data, self.get_refresh_token_name()) 427 428 def _extract_token_expiry_date(self, response_data: Mapping[str, Any]) -> AirbyteDateTime: 429 """ 430 Extracts the token_expiry_date, like `expires_in` or `expires_at`, etc from the given response data. 431 432 If the token_expiry_date is not found, it will return an existing token expiry date if set, or a default token expiry date. 433 434 Args: 435 response_data (Mapping[str, Any]): The response data from which to extract the token_expiry_date. 436 437 Returns: 438 The extracted token_expiry_date or None if not found. 439 """ 440 expires_in = self._find_and_get_value_from_response( 441 response_data, self.get_expires_in_name() 442 ) 443 if expires_in is not None: 444 return self._parse_token_expiration_date(expires_in) 445 446 # expires_in is None 447 existing_expiry_date = self.get_token_expiry_date() 448 if existing_expiry_date and not self.token_has_expired(): 449 return existing_expiry_date 450 451 return self._default_token_expiry_date() 452 453 def _find_and_get_value_from_response( 454 self, 455 response_data: Mapping[str, Any], 456 key_name: str, 457 max_depth: int = 5, 458 current_depth: int = 0, 459 ) -> Any: 460 """ 461 Recursively searches for a specified key in a nested dictionary or list and returns its value if found. 462 463 Args: 464 response_data (Mapping[str, Any]): The response data to search through, which can be a dictionary or a list. 465 key_name (str): The key to search for in the response data. 466 max_depth (int, optional): The maximum depth to search for the key to avoid infinite recursion. Defaults to 5. 467 current_depth (int, optional): The current depth of the recursion. Defaults to 0. 468 469 Returns: 470 Any: The value associated with the specified key if found, otherwise None. 471 472 Raises: 473 AirbyteTracedException: If the maximum recursion depth is reached without finding the key. 474 """ 475 if current_depth > max_depth: 476 # this is needed to avoid an inf loop, possible with a very deep nesting observed. 477 message = f"The maximum level of recursion is reached. Couldn't find the specified `{key_name}` in the response." 478 raise ResponseKeysMaxRecurtionReached( 479 internal_message=message, message=message, failure_type=FailureType.config_error 480 ) 481 482 if isinstance(response_data, dict): 483 # get from the root level 484 if key_name in response_data: 485 return response_data[key_name] 486 487 # get from the nested object 488 for _, value in response_data.items(): 489 result = self._find_and_get_value_from_response( 490 value, key_name, max_depth, current_depth + 1 491 ) 492 if result is not None: 493 return result 494 495 # get from the nested array object 496 elif isinstance(response_data, list): 497 for item in response_data: 498 result = self._find_and_get_value_from_response( 499 item, key_name, max_depth, current_depth + 1 500 ) 501 if result is not None: 502 return result 503 504 return None 505 506 @property 507 def _message_repository(self) -> Optional[MessageRepository]: 508 """ 509 The implementation can define a message_repository if it wants debugging logs for HTTP requests 510 """ 511 return _NOOP_MESSAGE_REPOSITORY 512 513 def _log_response(self, response: requests.Response) -> None: 514 """ 515 Logs the HTTP response using the message repository if it is available. 516 517 Args: 518 response (requests.Response): The HTTP response to log. 519 """ 520 if self._message_repository: 521 self._message_repository.log_message( 522 Level.DEBUG, 523 lambda: format_http_message( 524 response, 525 "Refresh token", 526 "Obtains access token", 527 self._NO_STREAM_NAME, 528 is_auxiliary=True, 529 type="AUTH", 530 ), 531 ) 532 533 # ---------------- 534 # ABSTR METHODS 535 # ---------------- 536 537 @abstractmethod 538 def get_token_refresh_endpoint(self) -> Optional[str]: 539 """Returns the endpoint to refresh the access token""" 540 541 @abstractmethod 542 def get_client_id_name(self) -> str: 543 """The client id name to authenticate""" 544 545 @abstractmethod 546 def get_client_id(self) -> str: 547 """The client id to authenticate""" 548 549 @abstractmethod 550 def get_client_secret_name(self) -> str: 551 """The client secret name to authenticate""" 552 553 @abstractmethod 554 def get_client_secret(self) -> str: 555 """The client secret to authenticate""" 556 557 @abstractmethod 558 def get_refresh_token_name(self) -> str: 559 """The refresh token name to authenticate""" 560 561 @abstractmethod 562 def get_refresh_token(self) -> Optional[str]: 563 """The token used to refresh the access token when it expires""" 564 565 @abstractmethod 566 def get_scopes(self) -> List[str]: 567 """List of requested scopes""" 568 569 @abstractmethod 570 def get_token_expiry_date(self) -> AirbyteDateTime: 571 """Expiration date of the access token""" 572 573 @abstractmethod 574 def set_token_expiry_date(self, value: AirbyteDateTime) -> None: 575 """Setter for access token expiration date""" 576 577 @abstractmethod 578 def get_access_token_name(self) -> str: 579 """Field to extract access token from in the response""" 580 581 @abstractmethod 582 def get_expires_in_name(self) -> str: 583 """Returns the expires_in field name""" 584 585 @abstractmethod 586 def get_refresh_request_body(self) -> Mapping[str, Any]: 587 """Returns the request body to set on the refresh request""" 588 589 @abstractmethod 590 def get_refresh_request_headers(self) -> Mapping[str, Any]: 591 """Returns the request headers to set on the refresh request""" 592 593 def should_send_refresh_request_as_query_params(self) -> bool: 594 """Returns `True` if the standard refresh args should be sent on the URL 595 query string instead of in the request body. 596 597 Defaults to `False` so existing authenticators retain their previous 598 behavior (params in body, no query params on the refresh URL). Subclasses 599 can override this to opt into the URL-query-string shape required by OAuth 600 providers like Gong. 601 """ 602 return False 603 604 @abstractmethod 605 def get_grant_type(self) -> str: 606 """Returns grant_type specified for requesting access_token""" 607 608 @abstractmethod 609 def get_grant_type_name(self) -> str: 610 """Returns grant_type specified name for requesting access_token""" 611 612 @property 613 @abstractmethod 614 def access_token(self) -> str: 615 """Returns the access token""" 616 617 @access_token.setter 618 @abstractmethod 619 def access_token(self, value: str) -> str: 620 """Setter for the access token"""
30class ResponseKeysMaxRecurtionReached(AirbyteTracedException): 31 """ 32 Raised when the max level of recursion is reached, when trying to 33 find-and-get the target key, during the `_make_handled_request` 34 """
Raised when the max level of recursion is reached, when trying to
find-and-get the target key, during the _make_handled_request
37class AbstractOauth2Authenticator(AuthBase): 38 """ 39 Abstract class for an OAuth authenticators that implements the OAuth token refresh flow. The authenticator 40 is designed to generically perform the refresh flow without regard to how config fields are get/set by 41 delegating that behavior to the classes implementing the interface. 42 """ 43 44 _NO_STREAM_NAME = None 45 46 # Class-level lock to prevent concurrent token refresh across multiple authenticator instances. 47 # This is necessary because multiple streams may share the same OAuth credentials (refresh token) 48 # through the connector config. Without this lock, concurrent refresh attempts can cause race 49 # conditions where one stream successfully refreshes the token while others fail because the 50 # refresh token has been invalidated (especially for single-use refresh tokens). 51 _token_refresh_lock: threading.Lock = threading.Lock() 52 53 def __init__( 54 self, 55 refresh_token_error_status_codes: Tuple[int, ...] = (), 56 refresh_token_error_key: str = "", 57 refresh_token_error_values: Tuple[str, ...] = (), 58 ) -> None: 59 """ 60 If all of refresh_token_error_status_codes, refresh_token_error_key, and refresh_token_error_values are set, 61 then http errors with such params will be wrapped in AirbyteTracedException. 62 """ 63 self._refresh_token_error_status_codes = refresh_token_error_status_codes 64 self._refresh_token_error_key = refresh_token_error_key 65 self._refresh_token_error_values = refresh_token_error_values 66 67 def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest: 68 """Attach the HTTP headers required to authenticate on the HTTP request""" 69 request.headers.update(self.get_auth_header()) 70 return request 71 72 @property 73 def _is_access_token_flow(self) -> bool: 74 return self.get_token_refresh_endpoint() is None and self.access_token is not None 75 76 @property 77 def token_expiry_is_time_of_expiration(self) -> bool: 78 """ 79 Indicates that the Token Expiry returns the date until which the token will be valid, not the amount of time it will be valid. 80 """ 81 82 return False 83 84 @property 85 def token_expiry_date_format(self) -> Optional[str]: 86 """ 87 Format of the datetime; exists it if expires_in is returned as the expiration datetime instead of seconds until it expires 88 """ 89 90 return None 91 92 def get_auth_header(self) -> Mapping[str, Any]: 93 """HTTP header to set on the requests""" 94 token = self.access_token if self._is_access_token_flow else self.get_access_token() 95 return {"Authorization": f"Bearer {token}"} 96 97 def get_access_token(self) -> str: 98 """ 99 Returns the access token. 100 101 This method uses double-checked locking to ensure thread-safe token refresh. 102 When multiple threads (streams) detect an expired token simultaneously, only one 103 will perform the refresh while others wait. After acquiring the lock, the token 104 expiry is re-checked to avoid redundant refresh attempts. 105 """ 106 if self.token_has_expired(): 107 with self._token_refresh_lock: 108 # Double-check after acquiring lock - another thread may have already refreshed 109 if self.token_has_expired(): 110 self.refresh_and_set_access_token() 111 112 return self.access_token 113 114 def refresh_and_set_access_token(self) -> None: 115 """Force refresh the access token and update internal state. 116 117 This method refreshes the access token regardless of whether it has expired, 118 and updates the internal token and expiry date. Subclasses may override this 119 to handle additional state updates (e.g., persisting new refresh tokens). 120 """ 121 token, expires_in = self.refresh_access_token() 122 self.access_token = token 123 self.set_token_expiry_date(expires_in) 124 125 def token_has_expired(self) -> bool: 126 """Returns True if the token is expired""" 127 return ab_datetime_now() > self.get_token_expiry_date() 128 129 def _build_standard_refresh_args(self) -> MutableMapping[str, Any]: 130 """Build the standard OAuth refresh args (grant_type, refresh_token, client 131 credentials, scopes, plus any user-configured `refresh_request_body` extras). 132 133 Used by both `build_refresh_request_body()` and 134 `build_refresh_request_query_params()` so the same set of args can be emitted 135 in either the body or the URL query string depending on 136 `should_send_refresh_request_as_query_params()`. 137 138 Client credentials (client_id and client_secret) are excluded when 139 `refresh_request_headers` contains an `Authorization` header (e.g. Basic 140 auth). This is required by OAuth providers like Gong that expect credentials 141 ONLY in the Authorization header and reject requests that include them in 142 both places. 143 """ 144 headers = self.get_refresh_request_headers() 145 credentials_in_header = headers and "Authorization" in headers 146 include_client_credentials = not credentials_in_header 147 148 payload: MutableMapping[str, Any] = { 149 self.get_grant_type_name(): self.get_grant_type(), 150 } 151 152 if include_client_credentials: 153 payload[self.get_client_id_name()] = self.get_client_id() 154 payload[self.get_client_secret_name()] = self.get_client_secret() 155 156 payload[self.get_refresh_token_name()] = self.get_refresh_token() 157 158 if self.get_scopes(): 159 payload["scopes"] = self.get_scopes() 160 161 if self.get_refresh_request_body(): 162 for key, val in self.get_refresh_request_body().items(): 163 # Existing oauth args take precedence over custom configured fields. 164 if key not in payload: 165 payload[key] = val 166 167 return payload 168 169 def build_refresh_request_body(self) -> Mapping[str, Any]: 170 """Returns the request body to set on the refresh request. 171 172 When `should_send_refresh_request_as_query_params()` is `True`, the standard 173 refresh args are emitted on the URL query string instead and this method 174 returns an empty body. This supports OAuth providers like Gong that document 175 their refresh endpoint as a `POST` with parameters on the URL query string 176 and an empty body. 177 """ 178 if self.should_send_refresh_request_as_query_params(): 179 return {} 180 return self._build_standard_refresh_args() 181 182 def build_refresh_request_headers(self) -> Mapping[str, Any] | None: 183 """ 184 Returns the request headers to set on the refresh request 185 186 """ 187 headers = self.get_refresh_request_headers() 188 return headers if headers else None 189 190 def build_refresh_request_query_params(self) -> Mapping[str, Any] | None: 191 """Returns the URL query string parameters to set on the refresh request. 192 193 When `should_send_refresh_request_as_query_params()` is `True`, the standard 194 refresh args (grant_type, refresh_token, client credentials, scopes, plus 195 any user-configured `refresh_request_body` extras) are returned here and 196 `build_refresh_request_body()` returns an empty body. 197 198 Returns `None` otherwise so existing authenticators retain their previous 199 behavior (no query params on the refresh URL). 200 """ 201 if not self.should_send_refresh_request_as_query_params(): 202 return None 203 return self._build_standard_refresh_args() 204 205 def refresh_access_token(self) -> Tuple[str, AirbyteDateTime]: 206 """ 207 Returns the refresh token and its expiration datetime 208 209 :return: a tuple of (access_token, token_lifespan) 210 """ 211 try: 212 response_json = self._make_handled_request() 213 except ( 214 requests.exceptions.ConnectionError, 215 requests.exceptions.ConnectTimeout, 216 requests.exceptions.ReadTimeout, 217 ) as e: 218 raise AirbyteTracedException( 219 message="OAuth access token refresh request failed due to a network error.", 220 internal_message=f"Network error during OAuth token refresh after retries were exhausted: {e}", 221 failure_type=FailureType.transient_error, 222 ) from e 223 self._ensure_access_token_in_response(response_json) 224 225 return ( 226 self._extract_access_token(response_json), 227 self._extract_token_expiry_date(response_json), 228 ) 229 230 # ---------------- 231 # PRIVATE METHODS 232 # ---------------- 233 234 def _default_token_expiry_date(self) -> AirbyteDateTime: 235 """ 236 Returns the default token expiry date 237 """ 238 # 1 hour was chosen as a middle ground to avoid unnecessary frequent refreshes and token expiration 239 default_token_expiry_duration_hours = 1 # 1 hour 240 return ab_datetime_now() + timedelta(hours=default_token_expiry_duration_hours) 241 242 def _wrap_refresh_token_exception( 243 self, exception: requests.exceptions.RequestException 244 ) -> bool: 245 """ 246 Wraps and handles exceptions that occur during the refresh token process. 247 248 This method checks if the provided exception is related to a refresh token error 249 by examining the response status code and specific error content. 250 251 Args: 252 exception (requests.exceptions.RequestException): The exception raised during the request. 253 254 Returns: 255 bool: True if the exception is related to a refresh token error, False otherwise. 256 """ 257 try: 258 if exception.response is not None: 259 exception_content = exception.response.json() 260 else: 261 return False 262 except JSONDecodeError: 263 return False 264 return ( 265 exception.response.status_code in self._refresh_token_error_status_codes 266 and exception_content.get(self._refresh_token_error_key) 267 in self._refresh_token_error_values 268 ) 269 270 @backoff.on_exception( 271 backoff.expo, 272 ( 273 DefaultBackoffException, 274 requests.exceptions.ConnectionError, 275 requests.exceptions.ConnectTimeout, 276 requests.exceptions.ReadTimeout, 277 ), 278 on_backoff=lambda details: logger.info( 279 f"Caught retryable error after {details['tries']} tries. Waiting {details['wait']} seconds then retrying..." 280 ), 281 max_time=300, 282 ) 283 def _make_handled_request(self) -> Any: 284 """ 285 Makes a handled HTTP request to refresh an OAuth token. 286 287 This method sends a POST request to the token refresh endpoint with the necessary 288 headers and body to obtain a new access token. It handles various exceptions that 289 may occur during the request and logs the response for troubleshooting purposes. 290 291 Returns: 292 Mapping[str, Any]: The JSON response from the token refresh endpoint. 293 294 Raises: 295 DefaultBackoffException: If the response status code is 429 (Too Many Requests) 296 or any 5xx server error. 297 AirbyteTracedException: If the refresh token is invalid or expired, prompting 298 re-authentication. 299 Exception: For any other exceptions that occur during the request. 300 """ 301 try: 302 response = requests.request( 303 method="POST", 304 url=self.get_token_refresh_endpoint(), # type: ignore # returns None, if not provided, but str | bytes is expected. 305 data=self.build_refresh_request_body(), 306 headers=self.build_refresh_request_headers(), 307 params=self.build_refresh_request_query_params(), 308 ) 309 310 if not response.ok: 311 # log the response even if the request failed for troubleshooting purposes 312 self._log_response(response) 313 response.raise_for_status() 314 315 response_json = response.json() 316 317 try: 318 # extract the access token and add to secrets to avoid logging the raw value 319 access_key = self._extract_access_token(response_json) 320 if access_key: 321 add_to_secrets(access_key) 322 except ResponseKeysMaxRecurtionReached as e: 323 # could not find the access token in the response, so do nothing 324 pass 325 326 self._log_response(response) 327 328 return response_json 329 except requests.exceptions.RequestException as e: 330 if e.response is not None: 331 if e.response.status_code == 429 or e.response.status_code >= 500: 332 raise DefaultBackoffException( 333 request=e.response.request, 334 response=e.response, 335 failure_type=FailureType.transient_error, 336 ) 337 if self._wrap_refresh_token_exception(e): 338 response_info = ( 339 f"HTTP {e.response.status_code}: {e.response.text[:1000]}" 340 if e.response is not None 341 else str(e) 342 ) 343 raise AirbyteTracedException( 344 internal_message=f"Refresh token rejected by the OAuth token endpoint. {response_info}", 345 message="Refresh token was rejected by the OAuth provider (invalid, expired, or already used). Re-authenticate this source's credentials in its connection settings.", 346 failure_type=FailureType.config_error, 347 ) from e 348 raise 349 except Exception as e: 350 raise AirbyteTracedException( 351 message="OAuth access token refresh request failed.", 352 internal_message=f"Unexpected error during OAuth token refresh: {e}", 353 failure_type=FailureType.system_error, 354 ) from e 355 356 def _ensure_access_token_in_response(self, response_data: Mapping[str, Any]) -> None: 357 """ 358 Ensures that the access token is present in the response data. 359 360 This method attempts to extract the access token from the provided response data. 361 If the access token is not found, it raises an exception indicating that the token 362 refresh API response was missing the access token. 363 364 Args: 365 response_data (Mapping[str, Any]): The response data from which to extract the access token. 366 367 Raises: 368 Exception: If the access token is not found in the response data. 369 ResponseKeysMaxRecurtionReached: If the maximum recursion depth is reached while extracting the access token. 370 """ 371 try: 372 access_key = self._extract_access_token(response_data) 373 if not access_key: 374 raise Exception( 375 f"Token refresh API response was missing access token {self.get_access_token_name()}" 376 ) 377 except ResponseKeysMaxRecurtionReached as e: 378 raise e 379 380 def _parse_token_expiration_date(self, value: Union[str, int]) -> AirbyteDateTime: 381 """ 382 Parse a string or integer token expiration date into a datetime object 383 384 :return: expiration datetime 385 """ 386 if self.token_expiry_is_time_of_expiration: 387 if not self.token_expiry_date_format: 388 raise ValueError( 389 f"Invalid token expiry date format {self.token_expiry_date_format}; a string representing the format is required." 390 ) 391 try: 392 return ab_datetime_parse(str(value)) 393 except ValueError as e: 394 raise ValueError(f"Invalid token expiry date format: {e}") 395 else: 396 try: 397 # Only accept numeric values (as int/float/string) when no format specified 398 seconds = int(float(str(value))) 399 return ab_datetime_now() + timedelta(seconds=seconds) 400 except (ValueError, TypeError): 401 raise ValueError( 402 f"Invalid expires_in value: {value}. Expected number of seconds when no format specified." 403 ) 404 405 def _extract_access_token(self, response_data: Mapping[str, Any]) -> Any: 406 """ 407 Extracts the access token from the given response data. 408 409 Args: 410 response_data (Mapping[str, Any]): The response data from which to extract the access token. 411 412 Returns: 413 str: The extracted access token. 414 """ 415 return self._find_and_get_value_from_response(response_data, self.get_access_token_name()) 416 417 def _extract_refresh_token(self, response_data: Mapping[str, Any]) -> Any: 418 """ 419 Extracts the refresh token from the given response data. 420 421 Args: 422 response_data (Mapping[str, Any]): The response data from which to extract the refresh token. 423 424 Returns: 425 str: The extracted refresh token. 426 """ 427 return self._find_and_get_value_from_response(response_data, self.get_refresh_token_name()) 428 429 def _extract_token_expiry_date(self, response_data: Mapping[str, Any]) -> AirbyteDateTime: 430 """ 431 Extracts the token_expiry_date, like `expires_in` or `expires_at`, etc from the given response data. 432 433 If the token_expiry_date is not found, it will return an existing token expiry date if set, or a default token expiry date. 434 435 Args: 436 response_data (Mapping[str, Any]): The response data from which to extract the token_expiry_date. 437 438 Returns: 439 The extracted token_expiry_date or None if not found. 440 """ 441 expires_in = self._find_and_get_value_from_response( 442 response_data, self.get_expires_in_name() 443 ) 444 if expires_in is not None: 445 return self._parse_token_expiration_date(expires_in) 446 447 # expires_in is None 448 existing_expiry_date = self.get_token_expiry_date() 449 if existing_expiry_date and not self.token_has_expired(): 450 return existing_expiry_date 451 452 return self._default_token_expiry_date() 453 454 def _find_and_get_value_from_response( 455 self, 456 response_data: Mapping[str, Any], 457 key_name: str, 458 max_depth: int = 5, 459 current_depth: int = 0, 460 ) -> Any: 461 """ 462 Recursively searches for a specified key in a nested dictionary or list and returns its value if found. 463 464 Args: 465 response_data (Mapping[str, Any]): The response data to search through, which can be a dictionary or a list. 466 key_name (str): The key to search for in the response data. 467 max_depth (int, optional): The maximum depth to search for the key to avoid infinite recursion. Defaults to 5. 468 current_depth (int, optional): The current depth of the recursion. Defaults to 0. 469 470 Returns: 471 Any: The value associated with the specified key if found, otherwise None. 472 473 Raises: 474 AirbyteTracedException: If the maximum recursion depth is reached without finding the key. 475 """ 476 if current_depth > max_depth: 477 # this is needed to avoid an inf loop, possible with a very deep nesting observed. 478 message = f"The maximum level of recursion is reached. Couldn't find the specified `{key_name}` in the response." 479 raise ResponseKeysMaxRecurtionReached( 480 internal_message=message, message=message, failure_type=FailureType.config_error 481 ) 482 483 if isinstance(response_data, dict): 484 # get from the root level 485 if key_name in response_data: 486 return response_data[key_name] 487 488 # get from the nested object 489 for _, value in response_data.items(): 490 result = self._find_and_get_value_from_response( 491 value, key_name, max_depth, current_depth + 1 492 ) 493 if result is not None: 494 return result 495 496 # get from the nested array object 497 elif isinstance(response_data, list): 498 for item in response_data: 499 result = self._find_and_get_value_from_response( 500 item, key_name, max_depth, current_depth + 1 501 ) 502 if result is not None: 503 return result 504 505 return None 506 507 @property 508 def _message_repository(self) -> Optional[MessageRepository]: 509 """ 510 The implementation can define a message_repository if it wants debugging logs for HTTP requests 511 """ 512 return _NOOP_MESSAGE_REPOSITORY 513 514 def _log_response(self, response: requests.Response) -> None: 515 """ 516 Logs the HTTP response using the message repository if it is available. 517 518 Args: 519 response (requests.Response): The HTTP response to log. 520 """ 521 if self._message_repository: 522 self._message_repository.log_message( 523 Level.DEBUG, 524 lambda: format_http_message( 525 response, 526 "Refresh token", 527 "Obtains access token", 528 self._NO_STREAM_NAME, 529 is_auxiliary=True, 530 type="AUTH", 531 ), 532 ) 533 534 # ---------------- 535 # ABSTR METHODS 536 # ---------------- 537 538 @abstractmethod 539 def get_token_refresh_endpoint(self) -> Optional[str]: 540 """Returns the endpoint to refresh the access token""" 541 542 @abstractmethod 543 def get_client_id_name(self) -> str: 544 """The client id name to authenticate""" 545 546 @abstractmethod 547 def get_client_id(self) -> str: 548 """The client id to authenticate""" 549 550 @abstractmethod 551 def get_client_secret_name(self) -> str: 552 """The client secret name to authenticate""" 553 554 @abstractmethod 555 def get_client_secret(self) -> str: 556 """The client secret to authenticate""" 557 558 @abstractmethod 559 def get_refresh_token_name(self) -> str: 560 """The refresh token name to authenticate""" 561 562 @abstractmethod 563 def get_refresh_token(self) -> Optional[str]: 564 """The token used to refresh the access token when it expires""" 565 566 @abstractmethod 567 def get_scopes(self) -> List[str]: 568 """List of requested scopes""" 569 570 @abstractmethod 571 def get_token_expiry_date(self) -> AirbyteDateTime: 572 """Expiration date of the access token""" 573 574 @abstractmethod 575 def set_token_expiry_date(self, value: AirbyteDateTime) -> None: 576 """Setter for access token expiration date""" 577 578 @abstractmethod 579 def get_access_token_name(self) -> str: 580 """Field to extract access token from in the response""" 581 582 @abstractmethod 583 def get_expires_in_name(self) -> str: 584 """Returns the expires_in field name""" 585 586 @abstractmethod 587 def get_refresh_request_body(self) -> Mapping[str, Any]: 588 """Returns the request body to set on the refresh request""" 589 590 @abstractmethod 591 def get_refresh_request_headers(self) -> Mapping[str, Any]: 592 """Returns the request headers to set on the refresh request""" 593 594 def should_send_refresh_request_as_query_params(self) -> bool: 595 """Returns `True` if the standard refresh args should be sent on the URL 596 query string instead of in the request body. 597 598 Defaults to `False` so existing authenticators retain their previous 599 behavior (params in body, no query params on the refresh URL). Subclasses 600 can override this to opt into the URL-query-string shape required by OAuth 601 providers like Gong. 602 """ 603 return False 604 605 @abstractmethod 606 def get_grant_type(self) -> str: 607 """Returns grant_type specified for requesting access_token""" 608 609 @abstractmethod 610 def get_grant_type_name(self) -> str: 611 """Returns grant_type specified name for requesting access_token""" 612 613 @property 614 @abstractmethod 615 def access_token(self) -> str: 616 """Returns the access token""" 617 618 @access_token.setter 619 @abstractmethod 620 def access_token(self, value: str) -> str: 621 """Setter for the access token"""
Abstract class for an OAuth authenticators that implements the OAuth token refresh flow. The authenticator is designed to generically perform the refresh flow without regard to how config fields are get/set by delegating that behavior to the classes implementing the interface.
53 def __init__( 54 self, 55 refresh_token_error_status_codes: Tuple[int, ...] = (), 56 refresh_token_error_key: str = "", 57 refresh_token_error_values: Tuple[str, ...] = (), 58 ) -> None: 59 """ 60 If all of refresh_token_error_status_codes, refresh_token_error_key, and refresh_token_error_values are set, 61 then http errors with such params will be wrapped in AirbyteTracedException. 62 """ 63 self._refresh_token_error_status_codes = refresh_token_error_status_codes 64 self._refresh_token_error_key = refresh_token_error_key 65 self._refresh_token_error_values = refresh_token_error_values
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.
76 @property 77 def token_expiry_is_time_of_expiration(self) -> bool: 78 """ 79 Indicates that the Token Expiry returns the date until which the token will be valid, not the amount of time it will be valid. 80 """ 81 82 return False
Indicates that the Token Expiry returns the date until which the token will be valid, not the amount of time it will be valid.
84 @property 85 def token_expiry_date_format(self) -> Optional[str]: 86 """ 87 Format of the datetime; exists it if expires_in is returned as the expiration datetime instead of seconds until it expires 88 """ 89 90 return None
Format of the datetime; exists it if expires_in is returned as the expiration datetime instead of seconds until it expires
92 def get_auth_header(self) -> Mapping[str, Any]: 93 """HTTP header to set on the requests""" 94 token = self.access_token if self._is_access_token_flow else self.get_access_token() 95 return {"Authorization": f"Bearer {token}"}
HTTP header to set on the requests
97 def get_access_token(self) -> str: 98 """ 99 Returns the access token. 100 101 This method uses double-checked locking to ensure thread-safe token refresh. 102 When multiple threads (streams) detect an expired token simultaneously, only one 103 will perform the refresh while others wait. After acquiring the lock, the token 104 expiry is re-checked to avoid redundant refresh attempts. 105 """ 106 if self.token_has_expired(): 107 with self._token_refresh_lock: 108 # Double-check after acquiring lock - another thread may have already refreshed 109 if self.token_has_expired(): 110 self.refresh_and_set_access_token() 111 112 return self.access_token
Returns the access token.
This method uses double-checked locking to ensure thread-safe token refresh. When multiple threads (streams) detect an expired token simultaneously, only one will perform the refresh while others wait. After acquiring the lock, the token expiry is re-checked to avoid redundant refresh attempts.
114 def refresh_and_set_access_token(self) -> None: 115 """Force refresh the access token and update internal state. 116 117 This method refreshes the access token regardless of whether it has expired, 118 and updates the internal token and expiry date. Subclasses may override this 119 to handle additional state updates (e.g., persisting new refresh tokens). 120 """ 121 token, expires_in = self.refresh_access_token() 122 self.access_token = token 123 self.set_token_expiry_date(expires_in)
Force refresh the access token and update internal state.
This method refreshes the access token regardless of whether it has expired, and updates the internal token and expiry date. Subclasses may override this to handle additional state updates (e.g., persisting new refresh tokens).
125 def token_has_expired(self) -> bool: 126 """Returns True if the token is expired""" 127 return ab_datetime_now() > self.get_token_expiry_date()
Returns True if the token is expired
169 def build_refresh_request_body(self) -> Mapping[str, Any]: 170 """Returns the request body to set on the refresh request. 171 172 When `should_send_refresh_request_as_query_params()` is `True`, the standard 173 refresh args are emitted on the URL query string instead and this method 174 returns an empty body. This supports OAuth providers like Gong that document 175 their refresh endpoint as a `POST` with parameters on the URL query string 176 and an empty body. 177 """ 178 if self.should_send_refresh_request_as_query_params(): 179 return {} 180 return self._build_standard_refresh_args()
Returns the request body to set on the refresh request.
When should_send_refresh_request_as_query_params() is True, the standard
refresh args are emitted on the URL query string instead and this method
returns an empty body. This supports OAuth providers like Gong that document
their refresh endpoint as a POST with parameters on the URL query string
and an empty body.
182 def build_refresh_request_headers(self) -> Mapping[str, Any] | None: 183 """ 184 Returns the request headers to set on the refresh request 185 186 """ 187 headers = self.get_refresh_request_headers() 188 return headers if headers else None
Returns the request headers to set on the refresh request
190 def build_refresh_request_query_params(self) -> Mapping[str, Any] | None: 191 """Returns the URL query string parameters to set on the refresh request. 192 193 When `should_send_refresh_request_as_query_params()` is `True`, the standard 194 refresh args (grant_type, refresh_token, client credentials, scopes, plus 195 any user-configured `refresh_request_body` extras) are returned here and 196 `build_refresh_request_body()` returns an empty body. 197 198 Returns `None` otherwise so existing authenticators retain their previous 199 behavior (no query params on the refresh URL). 200 """ 201 if not self.should_send_refresh_request_as_query_params(): 202 return None 203 return self._build_standard_refresh_args()
Returns the URL query string parameters to set on the refresh request.
When should_send_refresh_request_as_query_params() is True, the standard
refresh args (grant_type, refresh_token, client credentials, scopes, plus
any user-configured refresh_request_body extras) are returned here and
build_refresh_request_body() returns an empty body.
Returns None otherwise so existing authenticators retain their previous
behavior (no query params on the refresh URL).
205 def refresh_access_token(self) -> Tuple[str, AirbyteDateTime]: 206 """ 207 Returns the refresh token and its expiration datetime 208 209 :return: a tuple of (access_token, token_lifespan) 210 """ 211 try: 212 response_json = self._make_handled_request() 213 except ( 214 requests.exceptions.ConnectionError, 215 requests.exceptions.ConnectTimeout, 216 requests.exceptions.ReadTimeout, 217 ) as e: 218 raise AirbyteTracedException( 219 message="OAuth access token refresh request failed due to a network error.", 220 internal_message=f"Network error during OAuth token refresh after retries were exhausted: {e}", 221 failure_type=FailureType.transient_error, 222 ) from e 223 self._ensure_access_token_in_response(response_json) 224 225 return ( 226 self._extract_access_token(response_json), 227 self._extract_token_expiry_date(response_json), 228 )
Returns the refresh token and its expiration datetime
Returns
a tuple of (access_token, token_lifespan)
538 @abstractmethod 539 def get_token_refresh_endpoint(self) -> Optional[str]: 540 """Returns the endpoint to refresh the access token"""
Returns the endpoint to refresh the access token
542 @abstractmethod 543 def get_client_id_name(self) -> str: 544 """The client id name to authenticate"""
The client id name to authenticate
550 @abstractmethod 551 def get_client_secret_name(self) -> str: 552 """The client secret name to authenticate"""
The client secret name to authenticate
554 @abstractmethod 555 def get_client_secret(self) -> str: 556 """The client secret to authenticate"""
The client secret to authenticate
558 @abstractmethod 559 def get_refresh_token_name(self) -> str: 560 """The refresh token name to authenticate"""
The refresh token name to authenticate
562 @abstractmethod 563 def get_refresh_token(self) -> Optional[str]: 564 """The token used to refresh the access token when it expires"""
The token used to refresh the access token when it expires
570 @abstractmethod 571 def get_token_expiry_date(self) -> AirbyteDateTime: 572 """Expiration date of the access token"""
Expiration date of the access token
574 @abstractmethod 575 def set_token_expiry_date(self, value: AirbyteDateTime) -> None: 576 """Setter for access token expiration date"""
Setter for access token expiration date
578 @abstractmethod 579 def get_access_token_name(self) -> str: 580 """Field to extract access token from in the response"""
Field to extract access token from in the response
582 @abstractmethod 583 def get_expires_in_name(self) -> str: 584 """Returns the expires_in field name"""
Returns the expires_in field name
586 @abstractmethod 587 def get_refresh_request_body(self) -> Mapping[str, Any]: 588 """Returns the request body to set on the refresh request"""
Returns the request body to set on the refresh request
590 @abstractmethod 591 def get_refresh_request_headers(self) -> Mapping[str, Any]: 592 """Returns the request headers to set on the refresh request"""
Returns the request headers to set on the refresh request
594 def should_send_refresh_request_as_query_params(self) -> bool: 595 """Returns `True` if the standard refresh args should be sent on the URL 596 query string instead of in the request body. 597 598 Defaults to `False` so existing authenticators retain their previous 599 behavior (params in body, no query params on the refresh URL). Subclasses 600 can override this to opt into the URL-query-string shape required by OAuth 601 providers like Gong. 602 """ 603 return False
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.
605 @abstractmethod 606 def get_grant_type(self) -> str: 607 """Returns grant_type specified for requesting access_token"""
Returns grant_type specified for requesting access_token