Skip to content

Commit b4c5734

Browse files
authored
feat: oauth sdk implementation (#799)
* chore: oauth sdk implementation
1 parent 6324a1c commit b4c5734

14 files changed

+406
-3
lines changed

twilio/base/client_base.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ def request(
6969
auth: Optional[Tuple[str, str]] = None,
7070
timeout: Optional[float] = None,
7171
allow_redirects: bool = False,
72+
is_oauth: bool = False,
73+
domain: Optional[str] = None
7274
) -> Response:
7375
"""
7476
Makes a request to the Twilio API using the configured http client
@@ -85,9 +87,15 @@ def request(
8587
8688
:returns: Response from the Twilio API
8789
"""
88-
auth = self.get_auth(auth)
90+
if not is_oauth:
91+
auth = self.get_auth(auth)
8992
headers = self.get_headers(method, headers)
9093
uri = self.get_hostname(uri)
94+
if is_oauth:
95+
OauthTokenBase = dynamic_import("twilio.base.oauth_token_base", "OauthTokenBase")
96+
token = OauthTokenBase().get_oauth_token(domain, "v1", self.username, self.password)
97+
headers['Authorization'] = f'Bearer {token}'
98+
headers.get('Authorization')
9199

92100
return self.http_client.request(
93101
method,
@@ -110,6 +118,7 @@ async def request_async(
110118
auth: Optional[Tuple[str, str]] = None,
111119
timeout: Optional[float] = None,
112120
allow_redirects: bool = False,
121+
is_oauth: bool = False,
113122
) -> Response:
114123
"""
115124
Asynchronously makes a request to the Twilio API using the configured http client
@@ -131,10 +140,15 @@ async def request_async(
131140
raise RuntimeError(
132141
"http_client must be asynchronous to support async API requests"
133142
)
134-
135-
auth = self.get_auth(auth)
143+
if not is_oauth:
144+
auth = self.get_auth(auth)
136145
headers = self.get_headers(method, headers)
137146
uri = self.get_hostname(uri)
147+
if is_oauth:
148+
OauthTokenBase = dynamic_import("twilio.base.oauth_token_base", "OauthTokenBase")
149+
token = OauthTokenBase().get_oauth_token(domain, "v1", self.username, self.password)
150+
headers['Authorization'] = f'Bearer {token}'
151+
headers.get('Authorization')
138152

139153
return await self.http_client.request(
140154
method,
@@ -232,3 +246,8 @@ def __repr__(self) -> str:
232246
:returns: Machine friendly representation
233247
"""
234248
return "<Twilio {}>".format(self.account_sid)
249+
250+
def dynamic_import(module_name, class_name):
251+
from importlib import import_module
252+
module = import_module(module_name)
253+
return getattr(module, class_name)

twilio/base/domain.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def request(
3232
auth: Optional[Tuple[str, str]] = None,
3333
timeout: Optional[float] = None,
3434
allow_redirects: bool = False,
35+
is_oauth: bool = False,
3536
) -> Response:
3637
"""
3738
Makes an HTTP request to this domain.
@@ -55,6 +56,8 @@ def request(
5556
auth=auth,
5657
timeout=timeout,
5758
allow_redirects=allow_redirects,
59+
is_oauth=is_oauth,
60+
domain=self.base_url,
5861
)
5962

6063
async def request_async(
@@ -67,6 +70,7 @@ async def request_async(
6770
auth: Optional[Tuple[str, str]] = None,
6871
timeout: Optional[float] = None,
6972
allow_redirects: bool = False,
73+
is_oauth: bool = False,
7074
) -> Response:
7175
"""
7276
Makes an asynchronous HTTP request to this domain.
@@ -90,4 +94,5 @@ async def request_async(
9094
auth=auth,
9195
timeout=timeout,
9296
allow_redirects=allow_redirects,
97+
is_oauth=is_oauth
9398
)

twilio/base/oauth_token_base.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from twilio.http.token_manager_initializer import TokenManagerInitializer
2+
3+
# Dynamic import utility function
4+
def dynamic_import(module_name, class_name):
5+
from importlib import import_module
6+
module = import_module(module_name)
7+
return getattr(module, class_name)
8+
9+
class OauthTokenBase:
10+
def get_oauth_token(self, domain: str, version: str, username: str, password: str):
11+
Domain = dynamic_import("twilio.base.domain", "Domain")
12+
Version = dynamic_import("twilio.base.version", "Version")
13+
BearerTokenHTTPClient = dynamic_import("twilio.http.bearer_token_http_client", "BearerTokenHTTPClient")
14+
OrgTokenManager = dynamic_import("twilio.http.orgs_token_manager", "OrgTokenManager")
15+
Client = dynamic_import("twilio.rest", "Client")
16+
try:
17+
orgs_token_manager = TokenManagerInitializer.get_token_manager()
18+
return BearerTokenHTTPClient(orgs_token_manager).get_access_token(Version(Domain(Client(username, password), domain), version))
19+
except Exception:
20+
orgs_token_manager = OrgTokenManager(grant_type='client_credentials',
21+
client_id=username,
22+
client_secret=password)
23+
TokenManagerInitializer().set_token_manager(orgs_token_manager)
24+
return BearerTokenHTTPClient(orgs_token_manager).get_access_token(Version(Domain(Client(username, password), domain), version))

twilio/base/version.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def request(
3939
auth: Optional[Tuple[str, str]] = None,
4040
timeout: Optional[float] = None,
4141
allow_redirects: bool = False,
42+
is_oauth: bool = False,
4243
) -> Response:
4344
"""
4445
Make an HTTP request.
@@ -53,6 +54,7 @@ def request(
5354
auth=auth,
5455
timeout=timeout,
5556
allow_redirects=allow_redirects,
57+
is_oauth=is_oauth
5658
)
5759

5860
async def request_async(
@@ -65,6 +67,7 @@ async def request_async(
6567
auth: Optional[Tuple[str, str]] = None,
6668
timeout: Optional[float] = None,
6769
allow_redirects: bool = False,
70+
is_oauth: bool = False,
6871
) -> Response:
6972
"""
7073
Make an asynchronous HTTP request
@@ -79,6 +82,7 @@ async def request_async(
7982
auth=auth,
8083
timeout=timeout,
8184
allow_redirects=allow_redirects,
85+
is_oauth=is_oauth
8286
)
8387

8488
@classmethod
@@ -123,6 +127,7 @@ def fetch(
123127
auth: Optional[Tuple[str, str]] = None,
124128
timeout: Optional[float] = None,
125129
allow_redirects: bool = False,
130+
is_oauth: bool = False,
126131
) -> Any:
127132
"""
128133
Fetch a resource instance.
@@ -136,6 +141,7 @@ def fetch(
136141
auth=auth,
137142
timeout=timeout,
138143
allow_redirects=allow_redirects,
144+
is_oauth=is_oauth
139145
)
140146

141147
return self._parse_fetch(method, uri, response)
@@ -150,6 +156,7 @@ async def fetch_async(
150156
auth: Optional[Tuple[str, str]] = None,
151157
timeout: Optional[float] = None,
152158
allow_redirects: bool = False,
159+
is_oauth: bool = False,
153160
) -> Any:
154161
"""
155162
Asynchronously fetch a resource instance.
@@ -163,6 +170,7 @@ async def fetch_async(
163170
auth=auth,
164171
timeout=timeout,
165172
allow_redirects=allow_redirects,
173+
is_oauth=is_oauth
166174
)
167175

168176
return self._parse_fetch(method, uri, response)
@@ -186,6 +194,7 @@ def update(
186194
auth: Optional[Tuple[str, str]] = None,
187195
timeout: Optional[float] = None,
188196
allow_redirects: bool = False,
197+
is_oauth: bool = False,
189198
) -> Any:
190199
"""
191200
Update a resource instance.
@@ -213,6 +222,7 @@ async def update_async(
213222
auth: Optional[Tuple[str, str]] = None,
214223
timeout: Optional[float] = None,
215224
allow_redirects: bool = False,
225+
is_oauth: bool = False,
216226
) -> Any:
217227
"""
218228
Asynchronously update a resource instance.
@@ -226,6 +236,7 @@ async def update_async(
226236
auth=auth,
227237
timeout=timeout,
228238
allow_redirects=allow_redirects,
239+
is_oauth=is_oauth
229240
)
230241

231242
return self._parse_update(method, uri, response)
@@ -249,6 +260,7 @@ def delete(
249260
auth: Optional[Tuple[str, str]] = None,
250261
timeout: Optional[float] = None,
251262
allow_redirects: bool = False,
263+
is_oauth: bool = False,
252264
) -> bool:
253265
"""
254266
Delete a resource.
@@ -276,6 +288,7 @@ async def delete_async(
276288
auth: Optional[Tuple[str, str]] = None,
277289
timeout: Optional[float] = None,
278290
allow_redirects: bool = False,
291+
is_oauth: bool = False,
279292
) -> bool:
280293
"""
281294
Asynchronously delete a resource.
@@ -289,6 +302,7 @@ async def delete_async(
289302
auth=auth,
290303
timeout=timeout,
291304
allow_redirects=allow_redirects,
305+
is_oauth=is_oauth
292306
)
293307

294308
return self._parse_delete(method, uri, response)
@@ -347,6 +361,7 @@ async def page_async(
347361
auth: Optional[Tuple[str, str]] = None,
348362
timeout: Optional[float] = None,
349363
allow_redirects: bool = False,
364+
is_oauth: bool = False,
350365
) -> Response:
351366
"""
352367
Makes an asynchronous HTTP request.
@@ -360,6 +375,7 @@ async def page_async(
360375
auth=auth,
361376
timeout=timeout,
362377
allow_redirects=allow_redirects,
378+
is_oauth=is_oauth
363379
)
364380

365381
def stream(
@@ -447,6 +463,7 @@ def create(
447463
auth: Optional[Tuple[str, str]] = None,
448464
timeout: Optional[float] = None,
449465
allow_redirects: bool = False,
466+
is_oauth: bool = False,
450467
) -> Any:
451468
"""
452469
Create a resource instance.
@@ -474,6 +491,7 @@ async def create_async(
474491
auth: Optional[Tuple[str, str]] = None,
475492
timeout: Optional[float] = None,
476493
allow_redirects: bool = False,
494+
is_oauth: bool = False,
477495
) -> Any:
478496
"""
479497
Asynchronously create a resource instance.
@@ -487,6 +505,7 @@ async def create_async(
487505
auth=auth,
488506
timeout=timeout,
489507
allow_redirects=allow_redirects,
508+
is_oauth=is_oauth
490509
)
491510

492511
return self._parse_create(method, uri, response)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import datetime
2+
import jwt
3+
4+
from twilio.base.version import Version
5+
from twilio.http.token_manager import TokenManager
6+
from twilio.twilio_bearer_token_auth import TwilioBearerTokenAuth
7+
8+
9+
class BearerTokenHTTPClient:
10+
def __init__(self, orgs_token_manager: TokenManager):
11+
self.orgs_token_manager = orgs_token_manager
12+
13+
def get_access_token(self, version: Version):
14+
if TwilioBearerTokenAuth.get_access_token() is None or self.is_token_expired(
15+
TwilioBearerTokenAuth.get_access_token()
16+
):
17+
access_token = self.orgs_token_manager.fetch_access_token(version)
18+
TwilioBearerTokenAuth.init(access_token)
19+
else:
20+
access_token = TwilioBearerTokenAuth.get_access_token()
21+
22+
return access_token
23+
24+
def is_token_expired(self, token):
25+
decoded_jwt = jwt.decode(token, options={"verify_signature": True})
26+
expires_at = decoded_jwt.get("exp")
27+
# Add a buffer of 30 seconds
28+
buffer_seconds = 30
29+
buffer_expires_at = expires_at - buffer_seconds
30+
return buffer_expires_at < datetime.datetime.now().timestamp()

twilio/http/http_client.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def request(
5757
auth: Optional[Tuple[str, str]] = None,
5858
timeout: Optional[float] = None,
5959
allow_redirects: bool = False,
60+
is_oauth: bool = False,
6061
) -> Response:
6162
"""
6263
Make an HTTP Request with parameters provided.

twilio/http/no_auth_http_client.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
class NoAuthHTTPClient:
2+
def get_headers(self):
3+
headers = {}
4+
return headers

twilio/http/orgs_token_manager.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from twilio.base.version import Version
2+
from twilio.http.token_manager import TokenManager
3+
from twilio.rest.preview_iam.organizations.token import TokenList
4+
5+
6+
class OrgTokenManager(TokenManager):
7+
"""
8+
Orgs Token Manager
9+
"""
10+
11+
def __init__(
12+
self,
13+
grant_type: str,
14+
client_id: str,
15+
client_secret: str,
16+
code: str = None,
17+
redirect_uri: str = None,
18+
audience: str = None,
19+
refreshToken: str = None,
20+
scope: str = None,
21+
):
22+
self.grant_type = grant_type
23+
self.client_id = client_id
24+
self.client_secret = client_secret
25+
self.code = code
26+
self.redirect_uri = redirect_uri
27+
self.audience = audience
28+
self.refreshToken = refreshToken
29+
self.scope = scope
30+
31+
def fetch_access_token(self, version: Version):
32+
token_list = TokenList(version)
33+
token_instance = token_list.create(
34+
grant_type=self.grant_type,
35+
client_id=self.client_id,
36+
client_secret=self.client_secret,
37+
code=self.code,
38+
redirect_uri=self.redirect_uri,
39+
audience=self.audience,
40+
scope=self.scope,
41+
)
42+
return token_instance.access_token

twilio/http/token_manager.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from twilio.base.version import Version
2+
3+
4+
class TokenManager:
5+
6+
def fetch_access_token(self, version: Version):
7+
pass
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from twilio.http.token_manager import TokenManager
2+
3+
4+
class TokenManagerInitializer:
5+
6+
org_token_manager = None
7+
8+
@classmethod
9+
def set_token_manager(cls, token_manager: TokenManager):
10+
cls.org_token_manager = token_manager
11+
12+
@classmethod
13+
def get_token_manager(cls):
14+
if cls.org_token_manager is None:
15+
raise Exception('Token Manager not initialized')
16+
return cls.org_token_manager

0 commit comments

Comments
 (0)