Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: oauth sdk implementation #799

Merged
merged 15 commits into from
Jul 23, 2024
Merged
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,
manisha1997 marked this conversation as resolved.
Show resolved Hide resolved
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
Loading