Skip to content
Snippets Groups Projects
Commit 23d1565e authored by John Swinbank's avatar John Swinbank
Browse files

Merge branch 'tickets/156' into 'master'

Smarter handling of tokens

See merge request astron-sdc/esap-userprofile-python-client!12
parents 8e2e4701 09df51e9
No related branches found
No related tags found
1 merge request!12Smarter handling of tokens
...@@ -14,6 +14,18 @@ import requests ...@@ -14,6 +14,18 @@ import requests
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def is_valid_token(token: str) -> bool:
"""Check that the given token has not expired"""
try:
selfdata = token.split(".")[1]
padded = data + "=" * divmod(len(data), 4)[1]
payload = json.loads(base64.urlsafe_b64decode(padded))
return payload["exp"] > int(time.time()) + 10
except Exception as e:
logger.warning(f"Couldn't parse token: {e}")
return False
class ShoppingClient: class ShoppingClient:
endpoint = "esap-api/accounts/user-profiles/" endpoint = "esap-api/accounts/user-profiles/"
...@@ -41,7 +53,6 @@ class ShoppingClient: ...@@ -41,7 +53,6 @@ class ShoppingClient:
self.token = token self.token = token
self.host = host self.host = host
self.connectors = connectors self.connectors = connectors
self.basket = None self.basket = None
def get_basket( def get_basket(
...@@ -87,7 +98,7 @@ class ShoppingClient: ...@@ -87,7 +98,7 @@ class ShoppingClient:
"shopping_cart" "shopping_cart"
] ]
else: else:
warn(f"Unable to load data from {self.host}; is your key valid?") warn(f"Unable to load data from {self.host}; check your token for expiry, scopes and audience. Please ask your ESAP administrator for more info.")
if filter_archives: if filter_archives:
self.basket = self._filter_on_archive() self.basket = self._filter_on_archive()
...@@ -97,24 +108,7 @@ class ShoppingClient: ...@@ -97,24 +108,7 @@ class ShoppingClient:
return self.basket return self.basket
def _is_valid_token(self, token: Optional[str]) -> bool:
"""Checks expiry of the token"""
if token is None:
return False
try:
data = token.split(".")[1]
padded = data + "=" * divmod(len(data), 4)[1]
payload = json.loads(base64.urlsafe_b64decode(padded))
return payload["exp"] > int(time.time()) + 10
except KeyError:
raise RuntimeError("Invalid JWT format")
def _request_header(self): def _request_header(self):
while not self._is_valid_token(self.token):
self._get_token()
return dict(Accept="application/json", Authorization=f"Bearer {self.token}") return dict(Accept="application/json", Authorization=f"Bearer {self.token}")
# filter on items belonging to the provided connectors # filter on items belonging to the provided connectors
...@@ -165,14 +159,18 @@ class ShoppingClient: ...@@ -165,14 +159,18 @@ class ShoppingClient:
) )
return self.basket return self.basket
def _get_token(self): @property
def token(self):
# If there is a user-specified token and it's valid, return that.
if self._user_token and is_valid_token(self._user_token):
return self._user_token
# Generic JH token method using authstate # Otherwise, search a variety of possible locations and return the
# first valid token we find.
# Generic JupyterHub token method using authstate
jh_api_uri = getenv("JUPYTERHUB_API_URL") jh_api_uri = getenv("JUPYTERHUB_API_URL")
jh_api_token = getenv("JUPYTERHUB_API_TOKEN") jh_api_token = getenv("JUPYTERHUB_API_TOKEN")
# Fallback to older rucio file
token_fn = getenv("RUCIO_OIDC_FILE_NAME")
try: try:
if all((jh_api_token, jh_api_uri)): if all((jh_api_token, jh_api_uri)):
res = requests.get( res = requests.get(
...@@ -180,18 +178,25 @@ class ShoppingClient: ...@@ -180,18 +178,25 @@ class ShoppingClient:
headers={"Authorization": f"token {jh_api_token}"}, headers={"Authorization": f"token {jh_api_token}"},
) )
self.token = res.json()["auth_state"]["exchanged_tokens"][self.audience] if is_valid_token(
token := res.json()["auth_state"]["exchanged_tokens"][self.audience]
):
return token
except KeyError: except KeyError:
logger.warning("JupyterHub without Authstate enabled") logger.warning("JupyterHub without Authstate enabled")
# Try to get token from Rucio OIDC file (when running in CERN DLaaS notebook) # Try to get token from Rucio OIDC file (when running in CERN DLaaS notebook)
if self.token is None and token_fn is not None: if rucio_token_filename := getenv("RUCIO_OIDC_FILE_NAME"):
with open(token_fn) as token_file: with open(rucio_token_filename) as token_file:
self.token = token_file.readline() if is_valid_token(token := token_file.readline()):
return token
# Finally, fall back to prompting the user.
if is_valid_token(token := getpass.getpass("Enter your ESAP access token:")):
return token
elif self.token is None: raise RuntimeError("No valid token available")
self.token = getpass.getpass("Enter your ESAP access token:")
if self.token is None: @token.setter
raise RuntimeError("No token found!") def token(self, user_token):
self._user_token = user_token
...@@ -5,7 +5,7 @@ with open("README.md", "r") as fh: ...@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
setuptools.setup( setuptools.setup(
name="esap_client", name="esap_client",
version="0.0.5", version="0.0.6",
author="Hugh Dickinson", author="Hugh Dickinson",
author_email="hugh.dickinson@open.ac.uk", author_email="hugh.dickinson@open.ac.uk",
description="Python client for ESAP Data Discovery Shopping Basket", description="Python client for ESAP Data Discovery Shopping Basket",
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment