Skip to content

Commit

Permalink
Feature/cache tokens (#61)
Browse files Browse the repository at this point in the history
* feat: add CacheTokenStack to speed up all request times
  • Loading branch information
acostapazo committed Feb 24, 2023
1 parent b4d6290 commit a61a9f1
Show file tree
Hide file tree
Showing 9 changed files with 338 additions and 98 deletions.
48 changes: 21 additions & 27 deletions alice/auth/auth.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import json
from typing import Optional, Union

import requests
from meiga import Failure, Result, Success
from requests import Response, Session
from requests import Session

from alice.config import Config

from .auth_client import AuthClient
from .auth_errors import AuthError
from .token_tools import get_token_from_response

DEFAULT_URL = "https://apis.alicebiometrics.com/onboarding"

Expand Down Expand Up @@ -42,12 +42,13 @@ def __init__(
self.url = url
self.verbose = verbose

def create_backend_token(
self, user_id: Union[str, None] = None, verbose: Optional[bool] = False
def create_user_token(
self, user_id: str, verbose: Optional[bool] = False
) -> Result[str, AuthError]:
"""
Returns a BACKEND_TOKEN or BACKEND_TOKEN_WITH_USER depending of user_id given parameter.
Both BACKEND_TOKEN and BACKEND_TOKEN_WITH_USER are used to secure global requests.
Returns a USER_TOKEN.
The USER_TOKEN is used to secure requests made by the users on their mobile devices or web clients.
Parameters
----------
Expand All @@ -56,31 +57,30 @@ def create_backend_token(
verbose
Used for print service response as well as the time elapsed
Returns
-------
A Result where if the operation is successful it returns BACKEND_TOKEN or BACKEND_TOKEN_WITH_USER.
A Result where if the operation is successful it returns USER_TOKEN.
Otherwise, it returns an OnboardingError.
"""
verbose = self.verbose or verbose
response = self._auth_client.create_backend_token(user_id, verbose=verbose)
response = self._auth_client.create_user_token(user_id, verbose=verbose)

if response.status_code == 200:
return Success(self.__get_token_from_response(response))
return Success(get_token_from_response(response))
else:
suffix = " (with user)" if user_id else ""
return Failure(
AuthError.from_response(
operation=f"create_backend_token{suffix}", response=response
operation="create_user_token", response=response
)
)

def create_user_token(
self, user_id: str, verbose: Optional[bool] = False
def create_backend_token(
self, user_id: Union[str, None] = None, verbose: Optional[bool] = False
) -> Result[str, AuthError]:
"""
Returns a USER_TOKEN.
The USER_TOKEN is used to secure requests made by the users on their mobile devices or web clients.
Returns a BACKEND_TOKEN or BACKEND_TOKEN_WITH_USER depending of user_id given parameter.
Both BACKEND_TOKEN and BACKEND_TOKEN_WITH_USER are used to secure global requests.
Parameters
----------
Expand All @@ -89,26 +89,20 @@ def create_user_token(
verbose
Used for print service response as well as the time elapsed
Returns
-------
A Result where if the operation is successful it returns USER_TOKEN.
A Result where if the operation is successful it returns BACKEND_TOKEN or BACKEND_TOKEN_WITH_USER.
Otherwise, it returns an OnboardingError.
"""
verbose = self.verbose or verbose
response = self._auth_client.create_user_token(user_id, verbose=verbose)
response = self._auth_client.create_backend_token(user_id, verbose=verbose)

if response.status_code == 200:
return Success(self.__get_token_from_response(response))
return Success(get_token_from_response(response))
else:
suffix = " (with user)" if user_id else ""
return Failure(
AuthError.from_response(
operation="create_user_token", response=response
operation=f"create_backend_token{suffix}", response=response
)
)

@staticmethod
def __get_token_from_response(response: Response) -> str:
response_json = json.loads(response.content)
token: str = response_json["token"]
return token
160 changes: 102 additions & 58 deletions alice/auth/auth_client.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
import json
import time
from typing import Optional, Union
from unittest.mock import Mock

import jwt
import requests
from meiga import Failure, Result, Success
from requests import Response, Session

from alice.auth.cached_token_stack import CachedTokenStack
from alice.auth.token_tools import (
get_reponse_from_token,
get_token_from_response,
is_valid_token,
)
from alice.onboarding.tools import print_intro, print_response, timeit


def get_response_timeout() -> Response:
response = Mock(spec=Response)
response.json.return_value = {"message": "Request timed out"}
response.text.return_value = "Request timed out"
response.status_code = 408
return response


class AuthClient:
def __init__(
self,
Expand All @@ -20,74 +32,122 @@ def __init__(
):
self.url = url
self._api_key = api_key
self._login_token: Union[str, None] = None
self._cached_login_token: Union[str, None] = None
self._cached_backend_token: Union[str, None] = None
self._cached_backend_token_stack = CachedTokenStack()
self._cached_user_token_stack = CachedTokenStack()
self.session = session
self.timeout = timeout

@timeit
def create_backend_token(
self, user_id: Union[str, None] = None, verbose: Optional[bool] = False
def create_user_token(
self, user_id: str, verbose: Optional[bool] = False
) -> Response:

suffix = " (with user)" if user_id else ""
print_intro(f"create_backend_token{suffix}", verbose=verbose)
print_intro("create_user_token", verbose=verbose)

if not self._is_valid_token(self._login_token):
response = self._create_login_token()
token = self._cached_user_token_stack.get(user_id)
if token:
return get_reponse_from_token(token)

result = self._get_login_token()
if result.is_failure:
return result.value # type: ignore
login_token = result.unwrap()

url = f"{self.url}/user_token/{user_id}"
headers = {"Authorization": f"Bearer {login_token}"}
try:
response = self.session.get(url, headers=headers, timeout=self.timeout)
if response.status_code == 200:
self._login_token = self._get_token_from_response(response)
else:
return response
self._cached_user_token_stack.add(
user_id, get_token_from_response(response)
)
except requests.exceptions.Timeout:
response = get_response_timeout()

print_response(response=response, verbose=verbose)

return response

final_url = f"{self.url}/backend_token"
@timeit
def create_backend_token(
self, user_id: Union[str, None] = None, verbose: Optional[bool] = False
) -> Response:
if user_id:
final_url += f"/{user_id}"
return self._create_backend_token_with_user_id(user_id, verbose)
else:
return self._create_backend_token(verbose)

def _create_backend_token(self, verbose: Optional[bool] = False) -> Response:
print_intro("create_backend_token", verbose=verbose)

headers = {"Authorization": f"Bearer {self._login_token}"}
token = self._get_cached_backend_token()
if token:
return get_reponse_from_token(token)

result = self._get_login_token()
if result.is_failure:
return result.value # type: ignore
login_token = result.unwrap()

url = f"{self.url}/backend_token"
headers = {"Authorization": f"Bearer {login_token}"}
try:
response = self.session.get(
final_url, headers=headers, timeout=self.timeout
)
response = self.session.get(url, headers=headers, timeout=self.timeout)
if response.status_code == 200:
self._cached_backend_token = get_token_from_response(response)
except requests.exceptions.Timeout:
response = Mock(spec=Response)
response.json.return_value = {"message": "Request timed out"}
response.text.return_value = "Request timed out"
response.status_code = 408
response = get_response_timeout()

print_response(response=response, verbose=verbose)

return response

@timeit
def create_user_token(
def _get_cached_backend_token(self) -> Union[str, None]:
if not is_valid_token(self._cached_backend_token):
return None
return self._cached_backend_token

def _create_backend_token_with_user_id(
self, user_id: str, verbose: Optional[bool] = False
) -> Response:
print_intro("create_backend_token (with user)", verbose=verbose)

print_intro("create_user_token", verbose=verbose)
token = self._cached_backend_token_stack.get(user_id)
if token:
return get_reponse_from_token(token)

if not self._is_valid_token(self._login_token):
response = self._create_login_token()
if response.status_code == 200:
self._login_token = self._get_token_from_response(response)
else:
return response
result = self._get_login_token()
if result.is_failure:
return result.value # type: ignore
login_token = result.unwrap()

final_url = f"{self.url}/user_token/{user_id}"
headers = {"Authorization": f"Bearer {self._login_token}"}
url = f"{self.url}/backend_token/{user_id}"
headers = {"Authorization": f"Bearer {login_token}"}
try:
response = self.session.get(
final_url, headers=headers, timeout=self.timeout
)
response = self.session.get(url, headers=headers, timeout=self.timeout)
if response.status_code == 200:
self._cached_backend_token_stack.add(
user_id, get_token_from_response(response)
)
except requests.exceptions.Timeout:
response = Mock(spec=Response)
response.json.return_value = {"message": "Request timed out"}
response.text.return_value = "Request timed out"
response.status_code = 408
response = get_response_timeout()

print_response(response=response, verbose=verbose)

return response

def _get_login_token(self) -> Result[str, Response]:
if not is_valid_token(self._cached_login_token):
response = self._create_login_token()
if response.status_code == 200:
self._cached_login_token = get_token_from_response(response)
return Success(self._cached_login_token)
else:
return Failure(response)
return Success(self._cached_login_token) # type: ignore

def _create_login_token(self) -> Response:
final_url = f"{self.url}/login_token"
headers = {"apikey": self._api_key}
Expand All @@ -96,22 +156,6 @@ def _create_login_token(self) -> Response:
final_url, headers=headers, timeout=self.timeout
)
except requests.exceptions.Timeout:
response = Mock(spec=Response)
response.json.return_value = {"message": "Request timed out"}
response.text.return_value = "Request timed out"
response.status_code = 408
response = get_response_timeout()

return response

@staticmethod
def _is_valid_token(token: Union[str, None], margin_seconds: int = 60) -> bool:
if not token:
return False
decoded_token = jwt.decode(token, options={"verify_signature": False})
return bool(decoded_token["exp"] > time.time() - margin_seconds)

@staticmethod
def _get_token_from_response(response: Response) -> str:
response_json = json.loads(response.content)
token: str = response_json["token"]
return token
Loading

0 comments on commit a61a9f1

Please sign in to comment.