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

Cleanup tokens cache #81

Merged
merged 5 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Except for `requests_auth.testing`, only direct access via `requests_auth.` was considered publicly exposed. This is now explicit, as inner packages are now using private prefix (`_`).
If you were relying on some classes or functions that are now internal, feel free to open an issue.
- `requests_auth.JsonTokenFileCache` and `requests_auth.TokenMemoryCache` `get_token` method does not handle kwargs anymore, the `on_missing_token` callable does not expect any arguments anymore.
- `requests_auth.JsonTokenFileCache` does not expose `tokens_path` or `last_save_time` attributes anymore and is also allowing `pathlib.Path` instances as cache location.
- `requests_auth.TokenMemoryCache` does not expose `forbid_concurrent_cache_access` or `forbid_concurrent_missing_token_function_call` attributes anymore.

### Fixed
- Type information is now provided following [PEP 561](https://www.python.org/dev/peps/pep-0561/)
- Type information is now provided following [PEP 561](https://www.python.org/dev/peps/pep-0561/).
- Remove deprecation warnings due to usage of `utcnow` and `utcfromtimestamp`.
- Tokens cache `DEBUG` logs will not display tokens anymore.

### Removed
- Removing support for Python `3.7`.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -806,7 +806,7 @@ import datetime
from requests_auth.testing import browser_mock, BrowserMock, create_token

def test_something(browser_mock: BrowserMock):
token_expiry = datetime.datetime.utcnow() + datetime.timedelta(hours=1)
token_expiry = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=1)
token = create_token(token_expiry)
tab = browser_mock.add_response(
opened_url="http://url_opened_by_browser?state=1234",
Expand Down
6 changes: 4 additions & 2 deletions requests_auth/_oauth2/implicit.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,14 @@ def __call__(self, r):
token = OAuth2.token_cache.get_token(
key=self.state,
early_expiry=self.early_expiry,
on_missing_token=authentication_responses_server.request_new_grant,
grant_details=self.grant_details,
on_missing_token=self.request_new_token,
)
r.headers[self.header_name] = self.header_value.format(token=token)
return r

def request_new_token(self) -> tuple:
return authentication_responses_server.request_new_grant(self.grant_details)


class AzureActiveDirectoryImplicit(OAuth2Implicit):
"""
Expand Down
71 changes: 35 additions & 36 deletions requests_auth/_oauth2/tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import datetime
import threading
import logging
from pathlib import Path

from requests_auth._errors import *

logger = logging.getLogger(__name__)
Expand All @@ -23,16 +25,15 @@ def _decode_base64(base64_encoded_string: str) -> str:


def _is_expired(expiry: float, early_expiry: float) -> bool:
return (
datetime.datetime.utcfromtimestamp(expiry - early_expiry)
< datetime.datetime.utcnow()
)
return datetime.datetime.fromtimestamp(
expiry - early_expiry, datetime.timezone.utc
) < datetime.datetime.now(datetime.timezone.utc)


def _to_expiry(expires_in: Union[int, str]) -> float:
expiry = datetime.datetime.utcnow().replace(
tzinfo=datetime.timezone.utc
) + datetime.timedelta(seconds=int(expires_in))
expiry = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(
seconds=int(expires_in)
)
return expiry.timestamp()


Expand All @@ -43,8 +44,8 @@ class TokenMemoryCache:

def __init__(self):
self.tokens = {}
self.forbid_concurrent_cache_access = threading.Lock()
self.forbid_concurrent_missing_token_function_call = threading.Lock()
self._forbid_concurrent_cache_access = threading.Lock()
self._forbid_concurrent_missing_token_function_call = threading.Lock()

def _add_bearer_token(self, key: str, token: str):
"""
Expand Down Expand Up @@ -92,11 +93,11 @@ def _add_token(
:param expiry: UTC timestamp of expiry
:param refresh_token: refresh token value
"""
with self.forbid_concurrent_cache_access:
with self._forbid_concurrent_cache_access:
self.tokens[key] = token, expiry, refresh_token
self._save_tokens()
logger.debug(
f'Inserting token expiring on {datetime.datetime.utcfromtimestamp(expiry)} (UTC) with "{key}" key: {token}'
f'Inserting token expiring on {datetime.datetime.fromtimestamp(expiry, datetime.timezone.utc)} with "{key}" key.'
)

def get_token(
Expand All @@ -106,7 +107,6 @@ def get_token(
early_expiry: float = 30.0,
on_missing_token=None,
on_expired_token=None,
**on_missing_token_kwargs,
) -> str:
"""
Return the bearer token.
Expand All @@ -118,13 +118,12 @@ def get_token(
expired 30 seconds before real expiry by default.
:param on_missing_token: function to call when token is expired or missing (returning token and expiry tuple)
:param on_expired_token: function to call to refresh the token when it is expired
:param on_missing_token_kwargs: arguments of the on_missing_token function (key-value arguments)
:return: the token
:raise AuthenticationFailed: in case token cannot be retrieved.
"""
logger.debug(f'Retrieving token with "{key}" key.')
refresh_token = None
with self.forbid_concurrent_cache_access:
with self._forbid_concurrent_cache_access:
self._load_tokens()
if key in self.tokens:
token = self.tokens[key]
Expand All @@ -137,32 +136,32 @@ def get_token(
del self.tokens[key]
else:
logger.debug(
f"Using already received authentication, will expire on {datetime.datetime.utcfromtimestamp(expiry)} (UTC)."
f"Using already received authentication, will expire on {datetime.datetime.fromtimestamp(expiry, datetime.timezone.utc)}."
)
return bearer

if refresh_token is not None and on_expired_token is not None:
try:
with self.forbid_concurrent_missing_token_function_call:
with self._forbid_concurrent_missing_token_function_call:
state, token, expires_in, refresh_token = on_expired_token(
refresh_token
)
self._add_access_token(state, token, expires_in, refresh_token)
logger.debug(f"Refreshed token with key {key}.")
with self.forbid_concurrent_cache_access:
with self._forbid_concurrent_cache_access:
if state in self.tokens:
bearer, expiry, refresh_token = self.tokens[state]
logger.debug(
f"Using newly refreshed token, expiring on {datetime.datetime.utcfromtimestamp(expiry)} (UTC)."
f"Using newly refreshed token, expiring on {datetime.datetime.fromtimestamp(expiry, datetime.timezone.utc)}."
)
return bearer
except (InvalidGrantRequest, GrantNotProvided):
logger.debug(f"Failed to refresh token.")

logger.debug("Token cannot be found in cache.")
if on_missing_token is not None:
with self.forbid_concurrent_missing_token_function_call:
new_token = on_missing_token(**on_missing_token_kwargs)
with self._forbid_concurrent_missing_token_function_call:
new_token = on_missing_token()
if len(new_token) == 2: # Bearer token
state, token = new_token
self._add_bearer_token(state, token)
Expand All @@ -176,21 +175,21 @@ def get_token(
logger.warning(
f"Using a token received on another key than expected. Expecting {key} but was {state}."
)
with self.forbid_concurrent_cache_access:
with self._forbid_concurrent_cache_access:
if state in self.tokens:
bearer, expiry, refresh_token = self.tokens[state]
logger.debug(
f"Using newly received authentication, expiring on {datetime.datetime.utcfromtimestamp(expiry)} (UTC)."
f"Using newly received authentication, expiring on {datetime.datetime.fromtimestamp(expiry, datetime.timezone.utc)}."
)
return bearer

logger.debug(
f"User was not authenticated: key {key} cannot be found in {self.tokens}."
f"User was not authenticated: key {key} cannot be found in {list(self.tokens)}."
)
raise AuthenticationFailed()

def clear(self):
with self.forbid_concurrent_cache_access:
with self._forbid_concurrent_cache_access:
logger.debug("Clearing token cache.")
self.tokens = {}
self._clear()
Expand All @@ -210,36 +209,36 @@ class JsonTokenFileCache(TokenMemoryCache):
Class to manage tokens using a cache file.
"""

def __init__(self, tokens_path: str):
def __init__(self, tokens_path: Union[str, Path]):
TokenMemoryCache.__init__(self)
self.tokens_path = tokens_path
self.last_save_time = 0
self._tokens_path = Path(tokens_path)
self._last_save_time = 0
self._load_tokens()

def _clear(self):
self.last_save_time = 0
self._last_save_time = 0
try:
os.remove(self.tokens_path)
self._tokens_path.unlink(missing_ok=True)
except:
logger.debug("Cannot remove tokens file.")

def _save_tokens(self):
try:
with open(self.tokens_path, "w") as tokens_cache_file:
with self._tokens_path.open(mode="w") as tokens_cache_file:
json.dump(self.tokens, tokens_cache_file)
self.last_save_time = os.path.getmtime(self.tokens_path)
self._last_save_time = os.path.getmtime(self._tokens_path)
except:
logger.exception("Cannot save tokens.")

def _load_tokens(self):
if not os.path.exists(self.tokens_path):
if not self._tokens_path.exists():
logger.debug("No token loaded. Token cache does not exists.")
return
try:
last_modification_time = os.path.getmtime(self.tokens_path)
if last_modification_time > self.last_save_time:
self.last_save_time = last_modification_time
with open(self.tokens_path, "r") as tokens_cache_file:
last_modification_time = os.path.getmtime(self._tokens_path)
if last_modification_time > self._last_save_time:
self._last_save_time = last_modification_time
with self._tokens_path.open(mode="r") as tokens_cache_file:
self.tokens = json.load(tokens_cache_file)
except:
logger.exception("Cannot load tokens.")
8 changes: 6 additions & 2 deletions tests/test_add_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,9 @@ def test_oauth2_implicit_and_api_key_authentication_can_be_combined(
token_cache, responses: RequestsMock, browser_mock: BrowserMock
):
implicit_auth = requests_auth.OAuth2Implicit("http://provide_token")
expiry_in_1_hour = datetime.datetime.utcnow() + datetime.timedelta(hours=1)
expiry_in_1_hour = datetime.datetime.now(
datetime.timezone.utc
) + datetime.timedelta(hours=1)
token = create_token(expiry_in_1_hour)
tab = browser_mock.add_response(
opened_url="http://provide_token?response_type=token&state=42a85b271b7a652ca3cc4c398cfd3f01b9ad36bf9c945ba823b023e8f8b95c4638576a0e3dcc96838b838bec33ec6c0ee2609d62ed82480b3b8114ca494c0521&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F",
Expand All @@ -376,7 +378,9 @@ def test_oauth2_implicit_and_multiple_authentication_can_be_combined(
token_cache, responses: RequestsMock, browser_mock: BrowserMock
):
implicit_auth = requests_auth.OAuth2Implicit("http://provide_token")
expiry_in_1_hour = datetime.datetime.utcnow() + datetime.timedelta(hours=1)
expiry_in_1_hour = datetime.datetime.now(
datetime.timezone.utc
) + datetime.timedelta(hours=1)
token = create_token(expiry_in_1_hour)
tab = browser_mock.add_response(
opened_url="http://provide_token?response_type=token&state=42a85b271b7a652ca3cc4c398cfd3f01b9ad36bf9c945ba823b023e8f8b95c4638576a0e3dcc96838b838bec33ec6c0ee2609d62ed82480b3b8114ca494c0521&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F",
Expand Down
8 changes: 6 additions & 2 deletions tests/test_and_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,9 @@ def test_oauth2_implicit_and_api_key_authentication_can_be_combined(
token_cache, responses: RequestsMock, browser_mock: BrowserMock
):
implicit_auth = requests_auth.OAuth2Implicit("http://provide_token")
expiry_in_1_hour = datetime.datetime.utcnow() + datetime.timedelta(hours=1)
expiry_in_1_hour = datetime.datetime.now(
datetime.timezone.utc
) + datetime.timedelta(hours=1)
token = create_token(expiry_in_1_hour)
tab = browser_mock.add_response(
opened_url="http://provide_token?response_type=token&state=42a85b271b7a652ca3cc4c398cfd3f01b9ad36bf9c945ba823b023e8f8b95c4638576a0e3dcc96838b838bec33ec6c0ee2609d62ed82480b3b8114ca494c0521&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F",
Expand All @@ -376,7 +378,9 @@ def test_oauth2_implicit_and_multiple_authentication_can_be_combined(
token_cache, responses: RequestsMock, browser_mock: BrowserMock
):
implicit_auth = requests_auth.OAuth2Implicit("http://provide_token")
expiry_in_1_hour = datetime.datetime.utcnow() + datetime.timedelta(hours=1)
expiry_in_1_hour = datetime.datetime.now(
datetime.timezone.utc
) + datetime.timedelta(hours=1)
token = create_token(expiry_in_1_hour)
tab = browser_mock.add_response(
opened_url="http://provide_token?response_type=token&state=42a85b271b7a652ca3cc4c398cfd3f01b9ad36bf9c945ba823b023e8f8b95c4638576a0e3dcc96838b838bec33ec6c0ee2609d62ed82480b3b8114ca494c0521&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F",
Expand Down
Loading