From 1c9557e4b4dc07af7aab3813938f39d91a2b3b16 Mon Sep 17 00:00:00 2001 From: Slavik Markovich Date: Fri, 2 Sep 2022 12:28:29 -0700 Subject: [PATCH 1/3] Validate session allows either session token or refresh token to be empty --- descope/auth.py | 83 +++++++++++++++++++++------------------ descope/descope_client.py | 6 +-- 2 files changed, 47 insertions(+), 42 deletions(-) diff --git a/descope/auth.py b/descope/auth.py index 3639735ec..8cdd177e9 100644 --- a/descope/auth.py +++ b/descope/auth.py @@ -45,7 +45,7 @@ def __init__( raise AuthException( 400, ERROR_TYPE_INVALID_ARGUMENT, - "Unable to init AuthHelper object because project_id cannot be empty. Set environment variable DESCOPE_PROJECT_ID or pass your Project ID to the init function.", + "Unable to init Auth object because project_id cannot be empty. Set environment variable DESCOPE_PROJECT_ID or pass your Project ID to the init function.", ) self.project_id = project_id @@ -203,8 +203,7 @@ def refresh_token(self, refresh_token: str) -> dict: response = self.do_get(uri, None, None, refresh_token) resp = response.json() - auth_info = self._generate_auth_info(resp, refresh_token) - return auth_info + return self._generate_auth_info(resp, refresh_token) @staticmethod def _validate_and_load_public_key(public_key) -> Tuple[str, jwt.PyJWK, str]: @@ -294,19 +293,12 @@ def _generate_auth_info(self, response_body: dict, refresh_token: str) -> dict: jwt_response = {} st_jwt = response_body.get("sessionJwt", "") if st_jwt: - jwt_response[SESSION_TOKEN_NAME] = self._validate_and_load_tokens( - st_jwt, None - ) + jwt_response[SESSION_TOKEN_NAME] = self._validate_token(st_jwt) rt_jwt = response_body.get("refreshJwt", "") - if rt_jwt: - jwt_response[REFRESH_SESSION_TOKEN_NAME] = self._validate_and_load_tokens( - rt_jwt, None - ) - if refresh_token: - jwt_response[REFRESH_SESSION_TOKEN_NAME] = self._validate_and_load_tokens( - refresh_token, None - ) + jwt_response[REFRESH_SESSION_TOKEN_NAME] = self._validate_token(refresh_token) + elif rt_jwt: + jwt_response[REFRESH_SESSION_TOKEN_NAME] = self._validate_token(rt_jwt) jwt_response[COOKIE_DATA_NAME] = { "exp": response_body.get("cookieExpiration", 0), @@ -344,16 +336,16 @@ def _get_default_headers(self, pswd: str = None): headers["Authorization"] = f"Bearer {bearer}" return headers - def _validate_and_load_tokens(self, session_token: str, refresh_token: str) -> dict: - if not session_token: + # Validate a token and load the public key if needed + def _validate_token(self, token: str) -> dict: + if not token: raise AuthException( 500, ERROR_TYPE_INVALID_TOKEN, - f"empty signed token: {session_token}", + f"Token validation received empty token", ) - try: - unverified_header = jwt.get_unverified_header(session_token) + unverified_header = jwt.get_unverified_header(token) except Exception as e: raise AuthException( 500, @@ -395,33 +387,46 @@ def _validate_and_load_tokens(self, session_token: str, refresh_token: str) -> d ERROR_TYPE_INVALID_PUBLIC_KEY, "Algorithm signature in JWT header does not match the algorithm signature in the public key", ) + claims = jwt.decode(jwt=token, key=copy_key[0].key, algorithms=[alg_header]) + claims["jwt"] = token + return claims + - try: - claims = jwt.decode( - jwt=session_token, key=copy_key[0].key, algorithms=[alg_header] + def _validate_and_load_tokens(self, session_token: str, refresh_token: str) -> dict: + if not session_token and not refresh_token: + raise AuthException( + 500, + ERROR_TYPE_INVALID_TOKEN, + "Both refresh token and session token are empty", ) - claims["jwt"] = session_token - return claims - - except ExpiredSignatureError: - # Session token expired, check that refresh token is valid + if session_token: try: - jwt.decode( - jwt=refresh_token, - key=copy_key[0].key, - algorithms=[alg_header], - ) + return self._validate_token(session_token) + except ExpiredSignatureError: + # Session token expired, check that refresh token is valid + if refresh_token: + try: + self._validate_token(refresh_token) + except Exception as e: + raise AuthException(401, ERROR_TYPE_INVALID_TOKEN, f"Invalid refresh token: {e}") + else: + raise AuthException(401, ERROR_TYPE_INVALID_TOKEN, "Session token expired and no refresh token provided") + # Refresh token is valid now refresh the session token + return self.refresh_token(refresh_token) # return jwt_response dict except Exception as e: - raise AuthException( - 401, ERROR_TYPE_INVALID_TOKEN, f"Invalid refresh token: {e}" - ) - - # Refresh token is valid now refresh the session token - return self.refresh_token(refresh_token) # return jwt_response dict + raise AuthException(500, ERROR_TYPE_INVALID_TOKEN, f"Invalid token: {e}") + # If we got here, we did not have a session token so only do the refresh + try: + self._validate_token(refresh_token) except Exception as e: - raise AuthException(500, ERROR_TYPE_INVALID_TOKEN, f"Invalid token: {e}") + raise AuthException( + 401, ERROR_TYPE_INVALID_TOKEN, f"Invalid refresh token: {e}" + ) + # Refresh token is valid now refresh the session token + return self.refresh_token(refresh_token) # return jwt_response dict + @staticmethod def _compose_refresh_token_url() -> str: diff --git a/descope/descope_client.py b/descope/descope_client.py index 6c2e612ea..aa7d7c0df 100644 --- a/descope/descope_client.py +++ b/descope/descope_client.py @@ -58,6 +58,7 @@ def validate_session_request(self, session_token: str, refresh_token: str) -> di """ Validate the session for a given request. If the user is authenticated but the session has expired, the session token will automatically be refreshed. + Either the session_token or the refresh_token must be provided. Call this function every time you make a private API call that requires an authorized user. @@ -76,9 +77,8 @@ def validate_session_request(self, session_token: str, refresh_token: str) -> di ) # return jwt_response dict # Check if we had to refresh the session token and got a new one - if res.get(SESSION_TOKEN_NAME, None) and session_token != res.get( - SESSION_TOKEN_NAME - ).get("jwt"): + if res.get(SESSION_TOKEN_NAME, None) and \ + session_token != res.get(SESSION_TOKEN_NAME).get("jwt"): return res else: # In such case we return only the data related to the session token From 6f171afdd7d88676bc8653f08ebff5b3f6d97658 Mon Sep 17 00:00:00 2001 From: Slavik Markovich Date: Fri, 2 Sep 2022 12:37:13 -0700 Subject: [PATCH 2/3] Fixing some linting issues --- descope/auth.py | 4 +--- descope/descope_client.py | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/descope/auth.py b/descope/auth.py index e5307dba0..044f95f9a 100644 --- a/descope/auth.py +++ b/descope/auth.py @@ -363,7 +363,7 @@ def _validate_token(self, token: str) -> dict: raise AuthException( 500, ERROR_TYPE_INVALID_TOKEN, - f"Token validation received empty token", + "Token validation received empty token", ) try: unverified_header = jwt.get_unverified_header(token) @@ -411,7 +411,6 @@ def _validate_token(self, token: str) -> dict: claims = jwt.decode(jwt=token, key=copy_key[0].key, algorithms=[alg_header]) claims["jwt"] = token return claims - def _validate_and_load_tokens(self, session_token: str, refresh_token: str) -> dict: if not session_token and not refresh_token: @@ -448,7 +447,6 @@ def _validate_and_load_tokens(self, session_token: str, refresh_token: str) -> d # Refresh token is valid now refresh the session token return self.refresh_token(refresh_token) # return jwt_response dict - @staticmethod def _compose_refresh_token_url() -> str: return EndpointsV1.refreshTokenPath diff --git a/descope/descope_client.py b/descope/descope_client.py index 8baedc30d..739a3f0f7 100644 --- a/descope/descope_client.py +++ b/descope/descope_client.py @@ -76,8 +76,7 @@ def validate_session_request(self, session_token: str, refresh_token: str) -> di ) # return jwt_response dict # Check if we had to refresh the session token and got a new one - if res.get(SESSION_TOKEN_NAME, None) and \ - session_token != res.get(SESSION_TOKEN_NAME).get("jwt"): + if res.get(SESSION_TOKEN_NAME, None) and session_token != res.get(SESSION_TOKEN_NAME).get("jwt"): return res else: # In such case we return only the data related to the session token From 990fb428f8d616c541512b0d20930a5ad8286eab Mon Sep 17 00:00:00 2001 From: Slavik Markovich Date: Fri, 2 Sep 2022 13:09:25 -0700 Subject: [PATCH 3/3] Format using black --- descope/auth.py | 18 ++++++++++++++---- descope/descope_client.py | 4 +++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/descope/auth.py b/descope/auth.py index 044f95f9a..eef5e1add 100644 --- a/descope/auth.py +++ b/descope/auth.py @@ -317,7 +317,9 @@ def _generate_auth_info(self, response_body: dict, refresh_token: str) -> dict: jwt_response[SESSION_TOKEN_NAME] = self._validate_token(st_jwt) rt_jwt = response_body.get("refreshJwt", "") if refresh_token: - jwt_response[REFRESH_SESSION_TOKEN_NAME] = self._validate_token(refresh_token) + jwt_response[REFRESH_SESSION_TOKEN_NAME] = self._validate_token( + refresh_token + ) elif rt_jwt: jwt_response[REFRESH_SESSION_TOKEN_NAME] = self._validate_token(rt_jwt) @@ -429,13 +431,21 @@ def _validate_and_load_tokens(self, session_token: str, refresh_token: str) -> d try: self._validate_token(refresh_token) except Exception as e: - raise AuthException(401, ERROR_TYPE_INVALID_TOKEN, f"Invalid refresh token: {e}") + raise AuthException( + 401, ERROR_TYPE_INVALID_TOKEN, f"Invalid refresh token: {e}" + ) else: - raise AuthException(401, ERROR_TYPE_INVALID_TOKEN, "Session token expired and no refresh token provided") + raise AuthException( + 401, + ERROR_TYPE_INVALID_TOKEN, + "Session token expired and no refresh token provided", + ) # Refresh token is valid now refresh the session token return self.refresh_token(refresh_token) # return jwt_response dict except Exception as e: - raise AuthException(500, ERROR_TYPE_INVALID_TOKEN, f"Invalid token: {e}") + raise AuthException( + 500, ERROR_TYPE_INVALID_TOKEN, f"Invalid token: {e}" + ) # If we got here, we did not have a session token so only do the refresh try: diff --git a/descope/descope_client.py b/descope/descope_client.py index 739a3f0f7..e66aeeaea 100644 --- a/descope/descope_client.py +++ b/descope/descope_client.py @@ -76,7 +76,9 @@ def validate_session_request(self, session_token: str, refresh_token: str) -> di ) # return jwt_response dict # Check if we had to refresh the session token and got a new one - if res.get(SESSION_TOKEN_NAME, None) and session_token != res.get(SESSION_TOKEN_NAME).get("jwt"): + if res.get(SESSION_TOKEN_NAME, None) and session_token != res.get( + SESSION_TOKEN_NAME + ).get("jwt"): return res else: # In such case we return only the data related to the session token