Skip to content

Commit

Permalink
feat: oauth sdk implementation (#799)
Browse files Browse the repository at this point in the history
* chore: oauth sdk implementation
  • Loading branch information
manisha1997 authored Jul 23, 2024
1 parent 6324a1c commit b4c5734
Show file tree
Hide file tree
Showing 14 changed files with 406 additions and 3 deletions.
25 changes: 22 additions & 3 deletions twilio/base/client_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ def request(
auth: Optional[Tuple[str, str]] = None,
timeout: Optional[float] = None,
allow_redirects: bool = False,
is_oauth: bool = False,
domain: Optional[str] = None
) -> Response:
"""
Makes a request to the Twilio API using the configured http client
Expand All @@ -85,9 +87,15 @@ def request(
:returns: Response from the Twilio API
"""
auth = self.get_auth(auth)
if not is_oauth:
auth = self.get_auth(auth)
headers = self.get_headers(method, headers)
uri = self.get_hostname(uri)
if is_oauth:
OauthTokenBase = dynamic_import("twilio.base.oauth_token_base", "OauthTokenBase")
token = OauthTokenBase().get_oauth_token(domain, "v1", self.username, self.password)
headers['Authorization'] = f'Bearer {token}'
headers.get('Authorization')

return self.http_client.request(
method,
Expand All @@ -110,6 +118,7 @@ async def request_async(
auth: Optional[Tuple[str, str]] = None,
timeout: Optional[float] = None,
allow_redirects: bool = False,
is_oauth: bool = False,
) -> Response:
"""
Asynchronously makes a request to the Twilio API using the configured http client
Expand All @@ -131,10 +140,15 @@ async def request_async(
raise RuntimeError(
"http_client must be asynchronous to support async API requests"
)

auth = self.get_auth(auth)
if not is_oauth:
auth = self.get_auth(auth)
headers = self.get_headers(method, headers)
uri = self.get_hostname(uri)
if is_oauth:
OauthTokenBase = dynamic_import("twilio.base.oauth_token_base", "OauthTokenBase")
token = OauthTokenBase().get_oauth_token(domain, "v1", self.username, self.password)
headers['Authorization'] = f'Bearer {token}'
headers.get('Authorization')

return await self.http_client.request(
method,
Expand Down Expand Up @@ -232,3 +246,8 @@ def __repr__(self) -> str:
:returns: Machine friendly representation
"""
return "<Twilio {}>".format(self.account_sid)

def dynamic_import(module_name, class_name):
from importlib import import_module
module = import_module(module_name)
return getattr(module, class_name)
5 changes: 5 additions & 0 deletions twilio/base/domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def request(
auth: Optional[Tuple[str, str]] = None,
timeout: Optional[float] = None,
allow_redirects: bool = False,
is_oauth: bool = False,
) -> Response:
"""
Makes an HTTP request to this domain.
Expand All @@ -55,6 +56,8 @@ def request(
auth=auth,
timeout=timeout,
allow_redirects=allow_redirects,
is_oauth=is_oauth,
domain=self.base_url,
)

async def request_async(
Expand All @@ -67,6 +70,7 @@ async def request_async(
auth: Optional[Tuple[str, str]] = None,
timeout: Optional[float] = None,
allow_redirects: bool = False,
is_oauth: bool = False,
) -> Response:
"""
Makes an asynchronous HTTP request to this domain.
Expand All @@ -90,4 +94,5 @@ async def request_async(
auth=auth,
timeout=timeout,
allow_redirects=allow_redirects,
is_oauth=is_oauth
)
24 changes: 24 additions & 0 deletions twilio/base/oauth_token_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from twilio.http.token_manager_initializer import TokenManagerInitializer

# Dynamic import utility function
def dynamic_import(module_name, class_name):
from importlib import import_module
module = import_module(module_name)
return getattr(module, class_name)

class OauthTokenBase:
def get_oauth_token(self, domain: str, version: str, username: str, password: str):
Domain = dynamic_import("twilio.base.domain", "Domain")
Version = dynamic_import("twilio.base.version", "Version")
BearerTokenHTTPClient = dynamic_import("twilio.http.bearer_token_http_client", "BearerTokenHTTPClient")
OrgTokenManager = dynamic_import("twilio.http.orgs_token_manager", "OrgTokenManager")
Client = dynamic_import("twilio.rest", "Client")
try:
orgs_token_manager = TokenManagerInitializer.get_token_manager()
return BearerTokenHTTPClient(orgs_token_manager).get_access_token(Version(Domain(Client(username, password), domain), version))
except Exception:
orgs_token_manager = OrgTokenManager(grant_type='client_credentials',
client_id=username,
client_secret=password)
TokenManagerInitializer().set_token_manager(orgs_token_manager)
return BearerTokenHTTPClient(orgs_token_manager).get_access_token(Version(Domain(Client(username, password), domain), version))
19 changes: 19 additions & 0 deletions twilio/base/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def request(
auth: Optional[Tuple[str, str]] = None,
timeout: Optional[float] = None,
allow_redirects: bool = False,
is_oauth: bool = False,
) -> Response:
"""
Make an HTTP request.
Expand All @@ -53,6 +54,7 @@ def request(
auth=auth,
timeout=timeout,
allow_redirects=allow_redirects,
is_oauth=is_oauth
)

async def request_async(
Expand All @@ -65,6 +67,7 @@ async def request_async(
auth: Optional[Tuple[str, str]] = None,
timeout: Optional[float] = None,
allow_redirects: bool = False,
is_oauth: bool = False,
) -> Response:
"""
Make an asynchronous HTTP request
Expand All @@ -79,6 +82,7 @@ async def request_async(
auth=auth,
timeout=timeout,
allow_redirects=allow_redirects,
is_oauth=is_oauth
)

@classmethod
Expand Down Expand Up @@ -123,6 +127,7 @@ def fetch(
auth: Optional[Tuple[str, str]] = None,
timeout: Optional[float] = None,
allow_redirects: bool = False,
is_oauth: bool = False,
) -> Any:
"""
Fetch a resource instance.
Expand All @@ -136,6 +141,7 @@ def fetch(
auth=auth,
timeout=timeout,
allow_redirects=allow_redirects,
is_oauth=is_oauth
)

return self._parse_fetch(method, uri, response)
Expand All @@ -150,6 +156,7 @@ async def fetch_async(
auth: Optional[Tuple[str, str]] = None,
timeout: Optional[float] = None,
allow_redirects: bool = False,
is_oauth: bool = False,
) -> Any:
"""
Asynchronously fetch a resource instance.
Expand All @@ -163,6 +170,7 @@ async def fetch_async(
auth=auth,
timeout=timeout,
allow_redirects=allow_redirects,
is_oauth=is_oauth
)

return self._parse_fetch(method, uri, response)
Expand All @@ -186,6 +194,7 @@ def update(
auth: Optional[Tuple[str, str]] = None,
timeout: Optional[float] = None,
allow_redirects: bool = False,
is_oauth: bool = False,
) -> Any:
"""
Update a resource instance.
Expand Down Expand Up @@ -213,6 +222,7 @@ async def update_async(
auth: Optional[Tuple[str, str]] = None,
timeout: Optional[float] = None,
allow_redirects: bool = False,
is_oauth: bool = False,
) -> Any:
"""
Asynchronously update a resource instance.
Expand All @@ -226,6 +236,7 @@ async def update_async(
auth=auth,
timeout=timeout,
allow_redirects=allow_redirects,
is_oauth=is_oauth
)

return self._parse_update(method, uri, response)
Expand All @@ -249,6 +260,7 @@ def delete(
auth: Optional[Tuple[str, str]] = None,
timeout: Optional[float] = None,
allow_redirects: bool = False,
is_oauth: bool = False,
) -> bool:
"""
Delete a resource.
Expand Down Expand Up @@ -276,6 +288,7 @@ async def delete_async(
auth: Optional[Tuple[str, str]] = None,
timeout: Optional[float] = None,
allow_redirects: bool = False,
is_oauth: bool = False,
) -> bool:
"""
Asynchronously delete a resource.
Expand All @@ -289,6 +302,7 @@ async def delete_async(
auth=auth,
timeout=timeout,
allow_redirects=allow_redirects,
is_oauth=is_oauth
)

return self._parse_delete(method, uri, response)
Expand Down Expand Up @@ -347,6 +361,7 @@ async def page_async(
auth: Optional[Tuple[str, str]] = None,
timeout: Optional[float] = None,
allow_redirects: bool = False,
is_oauth: bool = False,
) -> Response:
"""
Makes an asynchronous HTTP request.
Expand All @@ -360,6 +375,7 @@ async def page_async(
auth=auth,
timeout=timeout,
allow_redirects=allow_redirects,
is_oauth=is_oauth
)

def stream(
Expand Down Expand Up @@ -447,6 +463,7 @@ def create(
auth: Optional[Tuple[str, str]] = None,
timeout: Optional[float] = None,
allow_redirects: bool = False,
is_oauth: bool = False,
) -> Any:
"""
Create a resource instance.
Expand Down Expand Up @@ -474,6 +491,7 @@ async def create_async(
auth: Optional[Tuple[str, str]] = None,
timeout: Optional[float] = None,
allow_redirects: bool = False,
is_oauth: bool = False,
) -> Any:
"""
Asynchronously create a resource instance.
Expand All @@ -487,6 +505,7 @@ async def create_async(
auth=auth,
timeout=timeout,
allow_redirects=allow_redirects,
is_oauth=is_oauth
)

return self._parse_create(method, uri, response)
30 changes: 30 additions & 0 deletions twilio/http/bearer_token_http_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import datetime
import jwt

from twilio.base.version import Version
from twilio.http.token_manager import TokenManager
from twilio.twilio_bearer_token_auth import TwilioBearerTokenAuth


class BearerTokenHTTPClient:
def __init__(self, orgs_token_manager: TokenManager):
self.orgs_token_manager = orgs_token_manager

def get_access_token(self, version: Version):
if TwilioBearerTokenAuth.get_access_token() is None or self.is_token_expired(
TwilioBearerTokenAuth.get_access_token()
):
access_token = self.orgs_token_manager.fetch_access_token(version)
TwilioBearerTokenAuth.init(access_token)
else:
access_token = TwilioBearerTokenAuth.get_access_token()

return access_token

def is_token_expired(self, token):
decoded_jwt = jwt.decode(token, options={"verify_signature": True})
expires_at = decoded_jwt.get("exp")
# Add a buffer of 30 seconds
buffer_seconds = 30
buffer_expires_at = expires_at - buffer_seconds
return buffer_expires_at < datetime.datetime.now().timestamp()
1 change: 1 addition & 0 deletions twilio/http/http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def request(
auth: Optional[Tuple[str, str]] = None,
timeout: Optional[float] = None,
allow_redirects: bool = False,
is_oauth: bool = False,
) -> Response:
"""
Make an HTTP Request with parameters provided.
Expand Down
4 changes: 4 additions & 0 deletions twilio/http/no_auth_http_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
class NoAuthHTTPClient:
def get_headers(self):
headers = {}
return headers
42 changes: 42 additions & 0 deletions twilio/http/orgs_token_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from twilio.base.version import Version
from twilio.http.token_manager import TokenManager
from twilio.rest.preview_iam.organizations.token import TokenList


class OrgTokenManager(TokenManager):
"""
Orgs Token Manager
"""

def __init__(
self,
grant_type: str,
client_id: str,
client_secret: str,
code: str = None,
redirect_uri: str = None,
audience: str = None,
refreshToken: str = None,
scope: str = None,
):
self.grant_type = grant_type
self.client_id = client_id
self.client_secret = client_secret
self.code = code
self.redirect_uri = redirect_uri
self.audience = audience
self.refreshToken = refreshToken
self.scope = scope

def fetch_access_token(self, version: Version):
token_list = TokenList(version)
token_instance = token_list.create(
grant_type=self.grant_type,
client_id=self.client_id,
client_secret=self.client_secret,
code=self.code,
redirect_uri=self.redirect_uri,
audience=self.audience,
scope=self.scope,
)
return token_instance.access_token
7 changes: 7 additions & 0 deletions twilio/http/token_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from twilio.base.version import Version


class TokenManager:

def fetch_access_token(self, version: Version):
pass
16 changes: 16 additions & 0 deletions twilio/http/token_manager_initializer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from twilio.http.token_manager import TokenManager


class TokenManagerInitializer:

org_token_manager = None

@classmethod
def set_token_manager(cls, token_manager: TokenManager):
cls.org_token_manager = token_manager

@classmethod
def get_token_manager(cls):
if cls.org_token_manager is None:
raise Exception('Token Manager not initialized')
return cls.org_token_manager
Loading

0 comments on commit b4c5734

Please sign in to comment.