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: Adding examples for orgs api uptake and public oauth #824

Closed
wants to merge 36 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
b4c5734
feat: oauth sdk implementation (#799)
manisha1997 Jul 23, 2024
3e246e4
Python Orgs Api Changes
AsabuHere Sep 17, 2024
8395487
removing unwanted logs
AsabuHere Sep 17, 2024
bc5c16b
removing unwanted logs
AsabuHere Sep 17, 2024
a66f9e9
removing unwanted logs
AsabuHere Sep 17, 2024
b5a6490
removing unwanted logs
AsabuHere Sep 17, 2024
fac26ee
Fixing token fetch flow
AsabuHere Sep 17, 2024
15e15c0
twilio python changes for orgs api uptake
AsabuHere Sep 26, 2024
7b07ba7
twilio python changes for orgs api uptake
AsabuHere Sep 26, 2024
af11fd2
Update test_cluster.py
AsabuHere Sep 26, 2024
661785d
Update test_cluster.py
AsabuHere Sep 26, 2024
6a8c2d8
twilio python changes for orgs api uptake
AsabuHere Sep 26, 2024
1ba2f9b
twilio python changes for orgs api uptake
AsabuHere Sep 26, 2024
98708f0
twilio python changes for orgs api uptake
AsabuHere Sep 26, 2024
d78d5d5
twilio python changes for orgs api uptake
AsabuHere Sep 26, 2024
7bdf1b5
Merge branch 'main' into asabu_Python_changes
AsabuHere Sep 26, 2024
bc77770
twilio python changes for orgs api uptake
AsabuHere Sep 27, 2024
0211f23
twilio python changes for orgs api uptake
AsabuHere Sep 27, 2024
27dec32
twilio python changes for orgs api uptake
AsabuHere Sep 27, 2024
2959689
twilio python changes for orgs api uptake
AsabuHere Sep 27, 2024
b973065
Uptake of review comments
AsabuHere Oct 1, 2024
ceebd46
modified error messages
AsabuHere Oct 1, 2024
35b5015
Uptake of review comments
AsabuHere Oct 6, 2024
76fecab
Merge branch 'main' into asabu_Python_changes
AsabuHere Oct 6, 2024
e9eaa72
Organization api uptake changes
AsabuHere Dec 11, 2024
1c8420c
Organization api uptake changes
AsabuHere Dec 11, 2024
644f94b
Organization api uptake changes
AsabuHere Dec 11, 2024
66f3e28
Organization api uptake changes
AsabuHere Dec 11, 2024
88f6623
removing accept headers for delete operation
AsabuHere Dec 12, 2024
630c28c
Removing unwanted code
AsabuHere Dec 12, 2024
26ee4d3
Merge branch 'main' into asabu_Python_changes
AsabuHere Dec 12, 2024
ddb336b
Modified generated files
AsabuHere Dec 12, 2024
d79366e
removing unwamted logs
AsabuHere Dec 12, 2024
824ed9f
Formatting changes
AsabuHere Dec 12, 2024
3670f03
make prettier run
AsabuHere Dec 12, 2024
eafd8dd
Adding examples and pushing client token manager and cred provider
AsabuHere Dec 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,13 @@ After a brief delay, you will receive the text message on your phone.
> **Warning**
> It's okay to hardcode your credentials when testing locally, but you should use environment variables to keep them secret before committing any code or deploying to production. Check out [How to Set Environment Variables](https://www.twilio.com/blog/2017/01/how-to-set-environment-variables.html) for more information.
## OAuth Feature for Twilio APIs
We are introducing Client Credentials Flow-based OAuth 2.0 authentication. This feature is currently in beta and its implementation is subject to change.

API examples [here](https://github.com/twilio/twilio-python/blob/main/examples/public_oauth.py)

Organisation API examples [here](https://github.com/twilio/twilio-python/blob/main/examples/organization_api_calls.py)

## Use the helper library

### API Credentials
Expand Down
28 changes: 28 additions & 0 deletions examples/organization_api_calls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import os

from twilio.rest import Client
from twilio.credential.orgs_credential_provider import OrgsCredentialProvider

API_KEY = os.environ.get("TWILIO_API_KEY")
API_SECRET = os.environ.get("TWILIO_API_SECRET")
ACCOUNT_SID = os.environ.get("TWILIO_ACCOUNT_SID")
CLIENT_ID = os.environ.get("TWILIO_CLIENT_ID")
CLIENT_SECRET = os.environ.get("TWILIO_CLIENT_SECRET")
ORGS_SID = os.environ.get("TWILIO_ORGS_SID")


def example(self=None):
"""
Some example usage of fetching accounts within an organization
"""
self.client = Client(
username=API_KEY,
password=API_SECRET,
account_sid=ACCOUNT_SID,
credential_provider= OrgsCredentialProvider(CLIENT_ID, CLIENT_SECRET)
)
accounts = self.client.preview_iam.organization(organization_sid=ORGS_SID).accounts.stream()
self.assertIsNotNone(accounts)

if __name__ == "__main__":
example()
35 changes: 35 additions & 0 deletions examples/public_oauth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import os

from twilio.rest import Client
from twilio.credential.client_credential_provider import ClientCredentialProvider

API_KEY = os.environ.get("TWILIO_API_KEY")
API_SECRET = os.environ.get("TWILIO_API_SECRET")
ACCOUNT_SID = os.environ.get("TWILIO_ACCOUNT_SID")
CLIENT_ID = os.environ.get("TWILIO_CLIENT_ID")
CLIENT_SECRET = os.environ.get("TWILIO_CLIENT_SECRET")
ORGS_SID = os.environ.get("TWILIO_ORGS_SID")
FROM_NUMBER = os.environ.get("TWILIO_FROM_NUMBER")
TO_NUMBER = os.environ.get("TWILIO_TO_NUMBER")


def example(self=None):
"""
Some example usage of fetching accounts within an organization
"""
self.client = Client(
username=API_KEY,
password=API_SECRET,
account_sid=ACCOUNT_SID,
credential_provider= ClientCredentialProvider(CLIENT_ID, CLIENT_SECRET)
)
msg = self.client.messages.create(
to=self.to_number, from_=self.from_number, body="hello world"
)
self.assertEqual(msg.to, self.to_number)
self.assertEqual(msg.from_, self.from_number)
self.assertEqual(msg.body, "hello world")
self.assertIsNotNone(msg.sid)

if __name__ == "__main__":
example()
Empty file.
19 changes: 19 additions & 0 deletions twilio/auth_strategy/auth_strategy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from twilio.auth_strategy.auth_type import AuthType
from abc import abstractmethod


class AuthStrategy(object):
def __init__(self, auth_type: AuthType):
self._auth_type = auth_type

@property
def auth_type(self) -> AuthType:
return self._auth_type

@abstractmethod
def get_auth_string(self) -> str:
"""Return the authentication string."""

@abstractmethod
def requires_authentication(self) -> bool:
"""Return True if authentication is required, else False."""
12 changes: 12 additions & 0 deletions twilio/auth_strategy/auth_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from enum import Enum


class AuthType(Enum):
ORGS_TOKEN = "orgs_stoken"
NO_AUTH = "noauth"
BASIC = "basic"
API_KEY = "api_key"
CLIENT_CREDENTIALS = "client_credentials"

def __str__(self):
return self.value
13 changes: 13 additions & 0 deletions twilio/auth_strategy/no_auth_strategy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from auth_type import AuthType
from twilio.auth_strategy.auth_strategy import AuthStrategy


class NoAuthStrategy(AuthStrategy):
def __init__(self):
super().__init__(AuthType.NO_AUTH)

def get_auth_string(self) -> str:
return ""

def requires_authentication(self) -> bool:
return False
53 changes: 53 additions & 0 deletions twilio/auth_strategy/token_auth_strategy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import jwt
import threading
import logging
from datetime import datetime

from twilio.auth_strategy.auth_type import AuthType
from twilio.auth_strategy.auth_strategy import AuthStrategy
from twilio.http.token_manager import TokenManager


class TokenAuthStrategy(AuthStrategy):
def __init__(self, token_manager: TokenManager):
super().__init__(AuthType.ORGS_TOKEN)
self.token_manager = token_manager
self.token = None
self.lock = threading.Lock()
logging.basicConfig(level=logging.INFO)
self.logger = logging.getLogger(__name__)

def get_auth_string(self) -> str:
self.fetch_token()
return f"Bearer {self.token}"

def requires_authentication(self) -> bool:
return True

def fetch_token(self):
if self.token is None or self.token == "" or self.is_token_expired(self.token):
with self.lock:
if (
self.token is None
or self.token == ""
or self.is_token_expired(self.token)
):
self.logger.info("New token fetched for accessing organization API")
self.token = self.token_manager.fetch_access_token()

def is_token_expired(self, token):
try:
decoded = jwt.decode(token, options={"verify_signature": False})

Check failure

Code scanning / SonarCloud

JWT should be signed and verified

<!--SONAR_ISSUE_KEY:AZO5eXCTuNV6EPBVA_gL-->Don't use a JWT token without verifying its signature. <p>See more on <a href="https://sonarcloud.io/project/issues?id=twilio_twilio-python&issues=AZO5eXCTuNV6EPBVA_gL&open=AZO5eXCTuNV6EPBVA_gL&branch=asabu_Python_changes">SonarQube Cloud</a></p>
exp = decoded.get("exp")

if exp is None:
return True # No expiration time present, consider it expired

# Check if the expiration time has passed
return datetime.fromtimestamp(exp) < datetime.utcnow()

except jwt.DecodeError:
return True # Token is invalid
except Exception as e:
print(f"An error occurred: {e}")
return True
57 changes: 47 additions & 10 deletions twilio/base/client_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
from urllib.parse import urlparse, urlunparse

from twilio import __version__
from twilio.base.exceptions import TwilioException
from twilio.http import HttpClient
from twilio.http.http_client import TwilioHttpClient
from twilio.http.response import Response
from twilio.credential.credential_provider import CredentialProvider


class ClientBase(object):
Expand All @@ -23,6 +23,7 @@ def __init__(
environment: Optional[MutableMapping[str, str]] = None,
edge: Optional[str] = None,
user_agent_extensions: Optional[List[str]] = None,
credential_provider: Optional[CredentialProvider] = None,
):
"""
Initializes the Twilio Client
Expand All @@ -35,7 +36,9 @@ def __init__(
:param environment: Environment to look for auth details, defaults to os.environ
:param edge: Twilio Edge to make requests to, defaults to None
:param user_agent_extensions: Additions to the user agent string
:param credential_provider: credential provider for authentication method that needs to be used
"""

environment = environment or os.environ

self.username = username or environment.get("TWILIO_ACCOUNT_SID")
Expand All @@ -48,9 +51,8 @@ def __init__(
""" :type : str """
self.user_agent_extensions = user_agent_extensions or []
""" :type : list[str] """

if not self.username or not self.password:
raise TwilioException("Credentials are required to create a TwilioClient")
self.credential_provider = credential_provider or None
""" :type : CredentialProvider """

self.account_sid = account_sid or self.username
""" :type : str """
Expand Down Expand Up @@ -85,15 +87,27 @@ def request(

:returns: Response from the Twilio API
"""
auth = self.get_auth(auth)
headers = self.get_headers(method, headers)
uri = self.get_hostname(uri)

if self.credential_provider:

auth_strategy = self.credential_provider.to_auth_strategy()
headers["Authorization"] = auth_strategy.get_auth_string()
elif self.username is not None and self.password is not None:
auth = self.get_auth(auth)
else:
auth = None

if method == "DELETE":
del headers["Accept"]

uri = self.get_hostname(uri)
filtered_data = self.copy_non_none_values(data)
return self.http_client.request(
method,
uri,
params=params,
data=data,
data=filtered_data,
headers=headers,
auth=auth,
timeout=timeout,
Expand Down Expand Up @@ -132,21 +146,44 @@ async def request_async(
"http_client must be asynchronous to support async API requests"
)

auth = self.get_auth(auth)
headers = self.get_headers(method, headers)
uri = self.get_hostname(uri)
if method == "DELETE":
del headers["Accept"]

if self.credential_provider:
auth_strategy = self.credential_provider.to_auth_strategy()
headers["Authorization"] = auth_strategy.get_auth_string()
elif self.username is not None and self.password is not None:
auth = self.get_auth(auth)
else:
auth = None

uri = self.get_hostname(uri)
filtered_data = self.copy_non_none_values(data)
return await self.http_client.request(
method,
uri,
params=params,
data=data,
data=filtered_data,
headers=headers,
auth=auth,
timeout=timeout,
allow_redirects=allow_redirects,
)

def copy_non_none_values(self, data):
if isinstance(data, dict):
return {
k: self.copy_non_none_values(v)
for k, v in data.items()
if v is not None
}
elif isinstance(data, list):
return [
self.copy_non_none_values(item) for item in data if item is not None
]
return data

def get_auth(self, auth: Optional[Tuple[str, str]]) -> Tuple[str, str]:
"""
Get the request authentication object
Expand Down
2 changes: 2 additions & 0 deletions twilio/base/page.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ def load_page(self, payload: Dict[str, Any]):
key = keys - self.META_KEYS
if len(key) == 1:
return payload[key.pop()]
if "Resources" in payload:
return payload["Resources"]

raise TwilioException("Page Records can not be deserialized")

Expand Down
3 changes: 0 additions & 3 deletions twilio/base/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,6 @@ async def fetch_async(
timeout=timeout,
allow_redirects=allow_redirects,
)

return self._parse_fetch(method, uri, response)

def _parse_update(self, method: str, uri: str, response: Response) -> Any:
Expand Down Expand Up @@ -461,7 +460,6 @@ def create(
timeout=timeout,
allow_redirects=allow_redirects,
)

return self._parse_create(method, uri, response)

async def create_async(
Expand All @@ -488,5 +486,4 @@ async def create_async(
timeout=timeout,
allow_redirects=allow_redirects,
)

return self._parse_create(method, uri, response)
Empty file added twilio/credential/__init__.py
Empty file.
28 changes: 28 additions & 0 deletions twilio/credential/client_credential_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from twilio.http.orgs_token_manager import OrgTokenManager
from twilio.base.exceptions import TwilioException
from twilio.credential.credential_provider import CredentialProvider
from twilio.auth_strategy.auth_type import AuthType
from twilio.auth_strategy.token_auth_strategy import TokenAuthStrategy


class ClientCredentialProvider(CredentialProvider):
def __init__(self, client_id: str, client_secret: str, token_manager=None):
super().__init__(AuthType.CLIENT_CREDENTIALS)

if client_id is None or client_secret is None:
raise TwilioException("Client id and Client secret are mandatory")

self.grant_type = "client_credentials"
self.client_id = client_id
self.client_secret = client_secret
self.token_manager = token_manager
self.auth_strategy = None

def to_auth_strategy(self):
if self.token_manager is None:
self.token_manager = OrgTokenManager(
self.grant_type, self.client_id, self.client_secret
)
if self.auth_strategy is None:
self.auth_strategy = TokenAuthStrategy(self.token_manager)
return self.auth_strategy
13 changes: 13 additions & 0 deletions twilio/credential/credential_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from twilio.auth_strategy.auth_type import AuthType


class CredentialProvider:
def __init__(self, auth_type: AuthType):
self._auth_type = auth_type

@property
def auth_type(self) -> AuthType:
return self._auth_type

def to_auth_strategy(self):
raise NotImplementedError("Subclasses must implement this method")
Loading
Loading