Skip to content

Commit 4a0d364

Browse files
authored
🎉 CDK: Add requests native authenticator support (#5731)
* Add requests native auth class * Update init file. Update type annotations. Bump version. * Update TokenAuthenticator implementation. Update Oauth2Authenticator implemetation. Add CHANGELOG.md record. * Update Oauth2Authenticator default value setting. Update CHANGELOG.md * Add requests native authenticator tests * Add CDK requests native __call__ method tests. Update CHANGELOG.md * Add outdated auth deprication messages * Update requests native auth __call__ method tests * Bump CDK version to 0.1.20
1 parent 278cb7d commit 4a0d364

File tree

11 files changed

+408
-5
lines changed

11 files changed

+408
-5
lines changed

airbyte-cdk/python/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## 0.1.20
4+
- Allow using `requests.auth.AuthBase` as authenticators instead of custom CDK authenticators.
5+
- Implement Oauth2Authenticator, MultipleTokenAuthenticator and TokenAuthenticator authenticators.
6+
- Add support for both legacy and requests native authenticator to HttpStream class.
7+
38
## 0.1.19
49
No longer prints full config files on validation error to prevent exposing secrets to log file: https://github.com/airbytehq/airbyte/pull/5879
510

airbyte-cdk/python/airbyte_cdk/sources/streams/http/auth/core.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@
2626
from abc import ABC, abstractmethod
2727
from typing import Any, Mapping
2828

29+
from deprecated import deprecated
2930

31+
32+
@deprecated(version="0.1.20", reason="Use requests.auth.AuthBase instead")
3033
class HttpAuthenticator(ABC):
3134
"""
3235
Base abstract class for various HTTP Authentication strategies. Authentication strategies are generally
@@ -40,6 +43,7 @@ def get_auth_header(self) -> Mapping[str, Any]:
4043
"""
4144

4245

46+
@deprecated(version="0.1.20", reason="Set `authenticator=None` instead")
4347
class NoAuth(HttpAuthenticator):
4448
def get_auth_header(self) -> Mapping[str, Any]:
4549
return {}

airbyte-cdk/python/airbyte_cdk/sources/streams/http/auth/oauth.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@
2727

2828
import pendulum
2929
import requests
30+
from deprecated import deprecated
3031

3132
from .core import HttpAuthenticator
3233

3334

35+
@deprecated(version="0.1.20", reason="Use airbyte_cdk.sources.streams.http.requests_native_auth.Oauth2Authenticator instead")
3436
class Oauth2Authenticator(HttpAuthenticator):
3537
"""
3638
Generates OAuth2.0 access tokens from an OAuth2.0 refresh token and client credentials.

airbyte-cdk/python/airbyte_cdk/sources/streams/http/auth/token.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,12 @@
2626
from itertools import cycle
2727
from typing import Any, List, Mapping
2828

29+
from deprecated import deprecated
30+
2931
from .core import HttpAuthenticator
3032

3133

34+
@deprecated(version="0.1.20", reason="Use airbyte_cdk.sources.streams.http.requests_native_auth.TokenAuthenticator instead")
3235
class TokenAuthenticator(HttpAuthenticator):
3336
def __init__(self, token: str, auth_method: str = "Bearer", auth_header: str = "Authorization"):
3437
self.auth_method = auth_method
@@ -39,6 +42,7 @@ def get_auth_header(self) -> Mapping[str, Any]:
3942
return {self.auth_header: f"{self.auth_method} {self._token}"}
4043

4144

45+
@deprecated(version="0.1.20", reason="Use airbyte_cdk.sources.streams.http.requests_native_auth.MultipleTokenAuthenticator instead")
4246
class MultipleTokenAuthenticator(HttpAuthenticator):
4347
def __init__(self, tokens: List[str], auth_method: str = "Bearer", auth_header: str = "Authorization"):
4448
self.auth_method = auth_method

airbyte-cdk/python/airbyte_cdk/sources/streams/http/http.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import requests
3030
from airbyte_cdk.models import SyncMode
3131
from airbyte_cdk.sources.streams.core import Stream
32+
from requests.auth import AuthBase
3233

3334
from .auth.core import HttpAuthenticator, NoAuth
3435
from .exceptions import DefaultBackoffException, RequestBodyException, UserDefinedBackoffException
@@ -46,10 +47,16 @@ class HttpStream(Stream, ABC):
4647
source_defined_cursor = True # Most HTTP streams use a source defined cursor (i.e: the user can't configure it like on a SQL table)
4748
page_size = None # Use this variable to define page size for API http requests with pagination support
4849

49-
def __init__(self, authenticator: HttpAuthenticator = NoAuth()):
50-
self._authenticator = authenticator
50+
# TODO: remove legacy HttpAuthenticator authenticator references
51+
def __init__(self, authenticator: Union[AuthBase, HttpAuthenticator] = None):
5152
self._session = requests.Session()
5253

54+
self._authenticator = NoAuth()
55+
if isinstance(authenticator, AuthBase):
56+
self._session.auth = authenticator
57+
elif authenticator:
58+
self._authenticator = authenticator
59+
5360
@property
5461
@abstractmethod
5562
def url_base(self) -> str:
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#
2+
# MIT License
3+
#
4+
# Copyright (c) 2020 Airbyte
5+
#
6+
# Permission is hereby granted, free of charge, to any person obtaining a copy
7+
# of this software and associated documentation files (the "Software"), to deal
8+
# in the Software without restriction, including without limitation the rights
9+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
# copies of the Software, and to permit persons to whom the Software is
11+
# furnished to do so, subject to the following conditions:
12+
#
13+
# The above copyright notice and this permission notice shall be included in all
14+
# copies or substantial portions of the Software.
15+
#
16+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
# SOFTWARE.
23+
#
24+
25+
from .oauth import Oauth2Authenticator
26+
from .token import MultipleTokenAuthenticator, TokenAuthenticator
27+
28+
__all__ = [
29+
"Oauth2Authenticator",
30+
"TokenAuthenticator",
31+
"MultipleTokenAuthenticator",
32+
]
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#
2+
# MIT License
3+
#
4+
# Copyright (c) 2020 Airbyte
5+
#
6+
# Permission is hereby granted, free of charge, to any person obtaining a copy
7+
# of this software and associated documentation files (the "Software"), to deal
8+
# in the Software without restriction, including without limitation the rights
9+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
# copies of the Software, and to permit persons to whom the Software is
11+
# furnished to do so, subject to the following conditions:
12+
#
13+
# The above copyright notice and this permission notice shall be included in all
14+
# copies or substantial portions of the Software.
15+
#
16+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
# SOFTWARE.
23+
#
24+
25+
26+
from typing import Any, List, Mapping, MutableMapping, Tuple
27+
28+
import pendulum
29+
import requests
30+
from requests.auth import AuthBase
31+
32+
33+
class Oauth2Authenticator(AuthBase):
34+
"""
35+
Generates OAuth2.0 access tokens from an OAuth2.0 refresh token and client credentials.
36+
The generated access token is attached to each request via the Authorization header.
37+
"""
38+
39+
def __init__(
40+
self,
41+
token_refresh_endpoint: str,
42+
client_id: str,
43+
client_secret: str,
44+
refresh_token: str,
45+
scopes: List[str] = None,
46+
token_expiry_date: pendulum.datetime = None,
47+
access_token_name: str = "access_token",
48+
expires_in_name: str = "expires_in",
49+
):
50+
self.token_refresh_endpoint = token_refresh_endpoint
51+
self.client_secret = client_secret
52+
self.client_id = client_id
53+
self.refresh_token = refresh_token
54+
self.scopes = scopes
55+
self.access_token_name = access_token_name
56+
self.expires_in_name = expires_in_name
57+
58+
self._token_expiry_date = token_expiry_date or pendulum.now().subtract(days=1)
59+
self._access_token = None
60+
61+
def __call__(self, request):
62+
request.headers.update(self.get_auth_header())
63+
return request
64+
65+
def get_auth_header(self) -> Mapping[str, Any]:
66+
return {"Authorization": f"Bearer {self.get_access_token()}"}
67+
68+
def get_access_token(self):
69+
if self.token_has_expired():
70+
t0 = pendulum.now()
71+
token, expires_in = self.refresh_access_token()
72+
self._access_token = token
73+
self._token_expiry_date = t0.add(seconds=expires_in)
74+
75+
return self._access_token
76+
77+
def token_has_expired(self) -> bool:
78+
return pendulum.now() > self._token_expiry_date
79+
80+
def get_refresh_request_body(self) -> Mapping[str, Any]:
81+
"""Override to define additional parameters"""
82+
payload: MutableMapping[str, Any] = {
83+
"grant_type": "refresh_token",
84+
"client_id": self.client_id,
85+
"client_secret": self.client_secret,
86+
"refresh_token": self.refresh_token,
87+
}
88+
89+
if self.scopes:
90+
payload["scopes"] = self.scopes
91+
92+
return payload
93+
94+
def refresh_access_token(self) -> Tuple[str, int]:
95+
"""
96+
returns a tuple of (access_token, token_lifespan_in_seconds)
97+
"""
98+
try:
99+
response = requests.request(method="POST", url=self.token_refresh_endpoint, data=self.get_refresh_request_body())
100+
response.raise_for_status()
101+
response_json = response.json()
102+
return response_json[self.access_token_name], response_json[self.expires_in_name]
103+
except Exception as e:
104+
raise Exception(f"Error while refreshing access token: {e}") from e
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#
2+
# MIT License
3+
#
4+
# Copyright (c) 2020 Airbyte
5+
#
6+
# Permission is hereby granted, free of charge, to any person obtaining a copy
7+
# of this software and associated documentation files (the "Software"), to deal
8+
# in the Software without restriction, including without limitation the rights
9+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
# copies of the Software, and to permit persons to whom the Software is
11+
# furnished to do so, subject to the following conditions:
12+
#
13+
# The above copyright notice and this permission notice shall be included in all
14+
# copies or substantial portions of the Software.
15+
#
16+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
# SOFTWARE.
23+
#
24+
25+
from itertools import cycle
26+
from typing import Any, List, Mapping
27+
28+
from requests.auth import AuthBase
29+
30+
31+
class MultipleTokenAuthenticator(AuthBase):
32+
"""
33+
Builds auth header, based on the list of tokens provided.
34+
Auth header is changed per each `get_auth_header` call, using each token in cycle.
35+
The token is attached to each request via the `auth_header` header.
36+
"""
37+
38+
def __init__(self, tokens: List[str], auth_method: str = "Bearer", auth_header: str = "Authorization"):
39+
self.auth_method = auth_method
40+
self.auth_header = auth_header
41+
self._tokens = tokens
42+
self._tokens_iter = cycle(self._tokens)
43+
44+
def __call__(self, request):
45+
request.headers.update(self.get_auth_header())
46+
return request
47+
48+
def get_auth_header(self) -> Mapping[str, Any]:
49+
return {self.auth_header: f"{self.auth_method} {next(self._tokens_iter)}"}
50+
51+
52+
class TokenAuthenticator(MultipleTokenAuthenticator):
53+
"""
54+
Builds auth header, based on the token provided.
55+
The token is attached to each request via the `auth_header` header.
56+
"""
57+
58+
def __init__(self, token: str, auth_method: str = "Bearer", auth_header: str = "Authorization"):
59+
super().__init__([token], auth_method, auth_header)

airbyte-cdk/python/setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535

3636
setup(
3737
name="airbyte-cdk",
38-
version="0.1.18",
38+
version="0.1.20",
3939
description="A framework for writing Airbyte Connectors.",
4040
long_description=README,
4141
long_description_content_type="text/markdown",
@@ -71,6 +71,7 @@
7171
"pydantic~=1.6",
7272
"PyYAML~=5.4",
7373
"requests",
74+
"Deprecated~=1.2",
7475
],
7576
python_requires=">=3.7.0",
7677
extras_require={"dev": ["MyPy~=0.812", "pytest", "pytest-cov", "pytest-mock", "requests-mock"]},

0 commit comments

Comments
 (0)