Skip to content

Commit

Permalink
VerificationAPI
Browse files Browse the repository at this point in the history
  • Loading branch information
650elx authored Jan 26, 2024
1 parent 556c595 commit 83c91f8
Show file tree
Hide file tree
Showing 33 changed files with 1,004 additions and 81 deletions.
8 changes: 7 additions & 1 deletion .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ env:
APP_ID: ${{ secrets.APP_ID }}
EMPTY_PROJECT_ID: ${{ secrets.EMPTY_PROJECT_ID }}
TEMPLATES_ORIGIN: ${{ secrets.TEMPLATES_ORIGIN }}

APPLICATION_SECRET: ${{ secrets.APPLICATION_SECRET }}
APPLICATION_KEY: ${{ secrets.APPLICATION_KEY }}
VERIFICATION_ID: ${{ secrets.VERIFICATION_ID }}
VERIFICATION_ORIGIN: ${{ secrets.VERIFICATION_ORIGIN}}
VERIFICATION_REQUEST_SIGNATURE_TIMESTAMP: ${{ secrets.VERIFICATION_REQUEST_SIGNATURE_TIMESTAMP}}
VERIFICATION_REQUEST_WITH_EMPTY_BODY_SIGNATURE: ${{ secrets.VERIFICATION_REQUEST_WITH_EMPTY_BODY_SIGNATURE}}
VERIFICATION_REQUEST_SIGNATURE: ${{ secrets.VERIFICATION_REQUEST_SIGNATURE}}

jobs:
build:
Expand Down
18 changes: 9 additions & 9 deletions sinch/core/clients/sinch_client_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from sinch.domains.numbers import NumbersAsync
from sinch.domains.conversation import ConversationAsync
from sinch.domains.sms import SMSAsync
from sinch.domains.verification import Verification as VerificationAsync


class ClientAsync(ClientBase):
Expand All @@ -20,25 +21,24 @@ def __init__(
key_secret,
project_id,
logger_name=None,
logger=None
logger=None,
application_key: str = None,
application_secret: str = None
):
super().__init__(
key_id=key_id,
key_secret=key_secret,
project_id=project_id,
logger_name=logger_name,
logger=logger
)
self.configuration = Configuration(
key_id=key_id,
key_secret=key_secret,
project_id=project_id,
logger_name=logger_name,
logger=logger,
transport=HTTPTransportAioHTTP(self),
token_manager=TokenManagerAsync(self)
token_manager=TokenManagerAsync(self),
application_secret=application_secret,
application_key=application_key
)

self.authentication = AuthenticationAsync(self)
self.numbers = NumbersAsync(self)
self.conversation = ConversationAsync(self)
self.sms = SMSAsync(self)
self.verification = VerificationAsync(self)
32 changes: 13 additions & 19 deletions sinch/core/clients/sinch_client_base.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from abc import ABC
from sinch.core.exceptions import ValidationException
from abc import ABC, abstractmethod
from sinch.core.clients.sinch_client_configuration import Configuration
from sinch.domains.authentication import AuthenticationBase
from sinch.domains.numbers import NumbersBase
Expand All @@ -10,32 +9,27 @@
class ClientBase(ABC):
"""
Sinch abstract base class for concrete Sinch Client implementations.
By default this SDK provides two implementations - sync and async.
By default, this SDK provides two implementations - sync and async.
Feel free to utilize any of them for you custom implementation.
"""
configuration = Configuration
authentication = AuthenticationBase
numbers = NumbersBase
conversation = ConversationBase
sms = SMSBase

@abstractmethod
def __init__(
self,
key_id,
key_secret,
project_id,
logger_name=None,
logger=None
logger=None,
application_key: str = None,
application_secret: str = None
):
if not key_id or not key_secret or not project_id:
raise ValidationException(
message=(
"key_id, key_secret and project_id are required by the Sinch Client. "
"Those credentials can be obtained from Sinch portal."
),
is_from_server=False,
response=None
)

self.configuration = Configuration
self.authentication = AuthenticationBase
self.numbers = NumbersBase
self.conversation = ConversationBase
self.sms = SMSBase
pass

def __repr__(self):
return f"Sinch SDK client for project_id: {self.configuration.project_id}"
7 changes: 6 additions & 1 deletion sinch/core/clients/sinch_client_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,19 @@ def __init__(
logger=None,
logger_name: str = None,
disable_https=False,
connection_timeout=10
connection_timeout=10,
application_key: str = None,
application_secret: str = None
):
self.key_id = key_id
self.key_secret = key_secret
self.project_id = project_id
self.application_key = application_key
self.application_secret = application_secret
self.connection_timeout = connection_timeout
self.auth_origin = "auth.sinch.com"
self.numbers_origin = "numbers.api.sinch.com"
self.verification_origin = "verification.api.sinch.com"
self._conversation_region = "eu"
self._conversation_domain = ".conversation.api.sinch.com"
self._sms_region = "us"
Expand Down
18 changes: 9 additions & 9 deletions sinch/core/clients/sinch_client_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from sinch.domains.numbers import Numbers
from sinch.domains.conversation import Conversation
from sinch.domains.sms import SMS
from sinch.domains.verification import Verification


class Client(ClientBase):
Expand All @@ -20,25 +21,24 @@ def __init__(
key_secret,
project_id,
logger_name=None,
logger=None
logger=None,
application_key: str = None,
application_secret: str = None
):
super().__init__(
key_id=key_id,
key_secret=key_secret,
project_id=project_id,
logger_name=logger_name,
logger=logger
)
self.configuration = Configuration(
key_id=key_id,
key_secret=key_secret,
project_id=project_id,
logger_name=logger_name,
logger=logger,
transport=HTTPTransportRequests(self),
token_manager=TokenManager(self)
token_manager=TokenManager(self),
application_key=application_key,
application_secret=application_secret
)

self.authentication = Authentication(self)
self.numbers = Numbers(self)
self.conversation = Conversation(self)
self.sms = SMS(self)
self.verification = Verification(self)
3 changes: 3 additions & 0 deletions sinch/core/endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ class HTTPEndpoint(ABC):
def __init__(self, project_id, request_data):
pass

def get_url_without_origin(self, sinch):
return '/' + '/'.join(self.build_url(sinch).split('/')[1:])

def build_url(self, sinch):
return

Expand Down
1 change: 1 addition & 0 deletions sinch/core/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ class HTTPMethods(Enum):
class HTTPAuthentication(Enum):
BASIC = "BASIC"
OAUTH = "OAUTH"
SIGNED = "SIGNED"
38 changes: 36 additions & 2 deletions sinch/core/ports/http_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
from abc import ABC, abstractmethod
from platform import python_version
from sinch.core.endpoint import HTTPEndpoint
from sinch.core.signature import Signature
from sinch.core.models.http_request import HttpRequest
from sinch.core.models.http_response import HTTPResponse
from sinch.core.exceptions import ValidationException
from sinch.core.enums import HTTPAuthentication
from sinch.core.token_manager import TokenState
from sinch import __version__ as sdk_version
Expand All @@ -18,6 +20,21 @@ def request(self, endpoint: HTTPEndpoint) -> HTTPResponse:
pass

def authenticate(self, endpoint, request_data):
if endpoint.HTTP_AUTHENTICATION in (HTTPAuthentication.BASIC.value, HTTPAuthentication.OAUTH.value):
if (
not self.sinch.configuration.key_id
or not self.sinch.configuration.key_secret
or not self.sinch.configuration.project_id
):
raise ValidationException(
message=(
"key_id, key_secret and project_id are required by this API. "
"Those credentials can be obtained from Sinch portal."
),
is_from_server=False,
response=None
)

if endpoint.HTTP_AUTHENTICATION == HTTPAuthentication.BASIC.value:
request_data.auth = (self.sinch.configuration.key_id, self.sinch.configuration.key_secret)
else:
Expand All @@ -29,6 +46,23 @@ def authenticate(self, endpoint, request_data):
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
})
elif endpoint.HTTP_AUTHENTICATION == HTTPAuthentication.SIGNED.value:
if not self.sinch.configuration.application_key or not self.sinch.configuration.application_secret:
raise ValidationException(
message=(
"application key and application secret are required by this API. "
"Those credentials can be obtained from Sinch portal."
),
is_from_server=False,
response=None
)
signature = Signature(
self.sinch,
endpoint.HTTP_METHOD,
request_data.request_body,
endpoint.get_url_without_origin(self.sinch)
)
request_data.headers = signature.get_http_headers_with_signature()

return request_data

Expand All @@ -50,7 +84,7 @@ def prepare_request(self, endpoint: HTTPEndpoint) -> HttpRequest:
)

def handle_response(self, endpoint: HTTPEndpoint, http_response: HTTPResponse):
if http_response.status_code == 401:
if http_response.status_code == 401 and endpoint.HTTP_AUTHENTICATION == HTTPAuthentication.OAUTH.value:
self.sinch.configuration.token_manager.handle_invalid_token(http_response)
if self.sinch.configuration.token_manager.token_state == TokenState.EXPIRED:
return self.request(endpoint=endpoint)
Expand Down Expand Up @@ -78,7 +112,7 @@ async def authenticate(self, endpoint, request_data):
return request_data

async def handle_response(self, endpoint: HTTPEndpoint, http_response: HTTPResponse):
if http_response.status_code == 401:
if http_response.status_code == 401 and endpoint.HTTP_AUTHENTICATION == HTTPAuthentication.OAUTH.value:
self.sinch.configuration.token_manager.handle_invalid_token(http_response)
if self.sinch.configuration.token_manager.token_state == TokenState.EXPIRED:
return await self.request(endpoint=endpoint)
Expand Down
58 changes: 58 additions & 0 deletions sinch/core/signature.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import hashlib
import hmac
import base64
from datetime import datetime, timezone


class Signature:
def __init__(
self,
sinch,
http_method,
request_data,
request_uri,
content_type=None,
signature_timestamp=None
):
self.sinch = sinch
self.http_method = http_method
self.content_type = content_type or 'application/json; charset=UTF-8'
self.request_data = request_data
self.signature_timestamp = signature_timestamp or datetime.now(timezone.utc).isoformat()
self.request_uri = request_uri
self.authorization_signature = None

def get_http_headers_with_signature(self):
if not self.authorization_signature:
self.calculate()

return {
"Content-Type": self.content_type,
"Authorization": (
f"Application {self.sinch.configuration.application_key}:{self.authorization_signature}"
),
"x-timestamp": self.signature_timestamp
}

def calculate(self):
b64_decoded_application_secret = base64.b64decode(self.sinch.configuration.application_secret)
if self.request_data:
encoded_verification_request = hashlib.md5(self.request_data.encode())
encoded_verification_request = base64.b64encode(encoded_verification_request.digest())

else:
encoded_verification_request = ''.encode()

request_timestamp = "x-timestamp:" + self.signature_timestamp

string_to_sign = (
self.http_method + '\n'
+ encoded_verification_request.decode() + '\n'
+ self.content_type + '\n'
+ request_timestamp + '\n'
+ self.request_uri
)

self.authorization_signature = base64.b64encode(
hmac.new(b64_decoded_application_secret, string_to_sign.encode(), hashlib.sha256).digest()
).decode()
Loading

0 comments on commit 83c91f8

Please sign in to comment.