diff --git a/cli/medperf/account_management/account_management.py b/cli/medperf/account_management/account_management.py index d19cf8cad..f8ea4e88e 100644 --- a/cli/medperf/account_management/account_management.py +++ b/cli/medperf/account_management/account_management.py @@ -19,16 +19,29 @@ def set_credentials( id_token_payload, token_issued_at, token_expires_in, + login_event=False, ): email = id_token_payload["email"] TokenStore().set_tokens(email, access_token, refresh_token) + config_p = read_config() + + if login_event: + # Set the time the user logged in, so that we can track the lifetime of + # the refresh token + logged_in_at = token_issued_at + else: + # This means this is a refresh event. Preserve the logged_in_at timestamp. + logged_in_at = config_p.active_profile[config.credentials_keyword][ + "logged_in_at" + ] account_info = { "email": email, "token_issued_at": token_issued_at, "token_expires_in": token_expires_in, + "logged_in_at": logged_in_at, } - config_p = read_config() + config_p.active_profile[config.credentials_keyword] = account_info write_config(config_p) diff --git a/cli/medperf/comms/auth/auth0.py b/cli/medperf/comms/auth/auth0.py index bfb53be31..0a694d77a 100644 --- a/cli/medperf/comms/auth/auth0.py +++ b/cli/medperf/comms/auth/auth0.py @@ -66,6 +66,7 @@ def login(self, email): id_token_payload, token_issued_at, token_expires_in, + login_event=True, ) def __request_device_code(self): @@ -191,16 +192,30 @@ def _access_token(self): refresh_token = creds["refresh_token"] token_expires_in = creds["token_expires_in"] token_issued_at = creds["token_issued_at"] - leeway_time = token_issued_at + token_expires_in - config.token_expiration_leeway - absolute_expiration_time = token_issued_at + config.token_absolute_expiry + logged_in_at = creds["logged_in_at"] + + # token_issued_at and expires_in are for the access token + sliding_expiration_time = ( + token_issued_at + token_expires_in - config.token_expiration_leeway + ) + absolute_expiration_time = ( + logged_in_at + + config.token_absolute_expiry + - config.refresh_token_expiration_leeway + ) current_time = time.time() - if current_time > leeway_time and current_time <= absolute_expiration_time: - access_token = self.__refresh_access_token(refresh_token) - elif current_time > absolute_expiration_time: - # Expired token. Force logout and ask the user to re-authenticate - logging.debug(f"Token expired: {absolute_expiration_time=} <> {current_time=}") + if current_time > absolute_expiration_time: + # Expired refresh token. Force logout and ask the user to re-authenticate + logging.debug( + f"Refresh token expired: {absolute_expiration_time=} <> {current_time=}" + ) self.logout() raise AuthenticationError("Token expired. Please re-authenticate") + + if current_time > sliding_expiration_time: + # Expired access token. Refresh it. + access_token = self.__refresh_access_token(refresh_token) + return access_token def __refresh_access_token(self, refresh_token): diff --git a/cli/medperf/config.py b/cli/medperf/config.py index 3fc105c2e..2c5b520be 100644 --- a/cli/medperf/config.py +++ b/cli/medperf/config.py @@ -37,7 +37,8 @@ auth_jwks_cache_ttl = 600 # fetch jwks every 10 mins. Default value in auth0 python SDK token_expiration_leeway = 10 # Refresh tokens 10 seconds before expiration -token_absolute_expiry = 2592000 # Maximum lifetime of a given token. This value is set on auth0's configuration +refresh_token_expiration_leeway = 10 # Logout users 10 seconds before absolute token expiration. +token_absolute_expiry = 2592000 # Refresh token absolute expiration time (seconds). This value is set on auth0's configuration access_token_storage_id = "medperf_access_token" refresh_token_storage_id = "medperf_refresh_token" diff --git a/cli/medperf/storage/__init__.py b/cli/medperf/storage/__init__.py index acebb6f31..74e4e7962 100644 --- a/cli/medperf/storage/__init__.py +++ b/cli/medperf/storage/__init__.py @@ -1,5 +1,6 @@ import os import shutil +import time from medperf import config from medperf.config_management import read_config, write_config @@ -25,6 +26,7 @@ def apply_configuration_migrations(): config_p = read_config() + # Migration for moving the logs folder to a new location if "logs_folder" not in config_p.storage: return @@ -35,4 +37,16 @@ def apply_configuration_migrations(): del config_p.storage["logs_folder"] + # Migration for tracking the login timestamp (i.e., refresh token issuance timestamp) + if config.credentials_keyword in config_p.active_profile: + # So the user is logged in + if "logged_in_at" not in config_p.active_profile[config.credentials_keyword]: + # Apply migration. We will set it to the current time, since this + # will make sure they will not be logged out before the actual refresh + # token expiration (for a better user experience). However, currently logged + # in users will still face a confusing error when the refresh token expires. + config_p.active_profile[config.credentials_keyword][ + "logged_in_at" + ] = time.time() + write_config(config_p)