Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
7699205
initial python sdk code
May 19, 2022
b83a0da
fix github isort action
May 19, 2022
902e9fe
fix github isort action wip2
guyp-descope May 19, 2022
043ffbb
fix github isort action wip3
guyp-descope May 19, 2022
78e5a94
fix github unittest action wip4
guyp-descope May 19, 2022
03941d0
fix github unittest action wip5
guyp-descope May 19, 2022
9afaf77
fix github unittest action wip6
guyp-descope May 19, 2022
757c24d
fix github unittest action wip7
guyp-descope May 19, 2022
2b9f5b3
add github python code coverage action wip8
guyp-descope May 19, 2022
252ca25
add github python code coverage action wip9
guyp-descope May 19, 2022
30b1c74
add github python code coverage action wip10
guyp-descope May 19, 2022
3d07f9a
add github python code coverage action wip11
guyp-descope May 19, 2022
1a50cee
fixes some bugs and added functionality of fetching public key
guyp-descope May 19, 2022
18efbc8
Add unittest for better coverage
guyp-descope May 22, 2022
c9811bc
remove the pyproject.toml file (not in used)
guyp-descope May 22, 2022
bdf01c4
add gitleaks Action
guyp-descope May 22, 2022
3f45417
add gitleaks to workflow, some PR fixes
guyp-descope May 22, 2022
5946440
add github checkout action
guyp-descope May 22, 2022
2d22181
fix github action
guyp-descope May 22, 2022
0ce81e1
fix github action
guyp-descope May 22, 2022
d65b343
fix PR issues
guyp-descope May 22, 2022
31ebac5
add support for multiple public keys, add mutex for thread safe
guyp-descope May 22, 2022
be52ea7
replace email regex with python email validator package
guyp-descope May 23, 2022
d377caf
add new package to requirements file
guyp-descope May 23, 2022
6914192
change badge namedlogo
guyp-descope May 23, 2022
2ae8bf5
change coverage badge
guyp-descope May 24, 2022
8089b04
change coverage badge
guyp-descope May 24, 2022
11f02a4
revert to the latest coverage badge
guyp-descope May 24, 2022
6da0ad6
1. Add support for refresh token 2. Fix decorator (to support pre-pos…
guyp-descope May 24, 2022
d6d3409
fix UT
guyp-descope May 24, 2022
cf65450
add license check for pre-commit and part of the ci workflow
guyp-descope May 24, 2022
08e56e7
fix license checks
guyp-descope May 24, 2022
344022f
fix license checks 2
guyp-descope May 24, 2022
1e8a68d
seperate the coverage steps to run on a different workflow that run o…
guyp-descope May 25, 2022
63a161e
1. fixed few bugs 2. added flask decorator functions example 3. imp…
guyp-descope May 26, 2022
b2b1474
fix pr comments
guyp-descope May 26, 2022
4ae2458
fix some more pr comments
guyp-descope May 26, 2022
e32b425
1. change the api so the claims (jwt payload) will be available for t…
guyp-descope May 30, 2022
cfb24c8
set alg as const
guyp-descope May 30, 2022
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
185 changes: 130 additions & 55 deletions descope/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,47 +47,55 @@ def __init__(self, project_id: str, public_key: str = None):
if public_key is None or public_key == "":
self.public_keys = {}
else:
kid, pub_key = self._validate_and_load_public_key(public_key)
self.public_keys = {kid: pub_key}
kid, pub_key, alg = self._validate_and_load_public_key(public_key)
self.public_keys = {kid: (pub_key, alg)}

@staticmethod
def _validate_and_load_public_key(public_key) -> Tuple[str, jwt.PyJWK]:
def _validate_and_load_public_key(public_key) -> Tuple[str, jwt.PyJWK, str]:
if isinstance(public_key, str):
try:
public_key = json.loads(public_key)
except Exception as e:
raise AuthException(
500,
400,
"Public key failure",
f"Failed to load public key, invalid public key, err: {e}",
)

if not isinstance(public_key, dict):
raise AuthException(
500,
400,
"Public key failure",
"Failed to load public key, invalid public key (unknown type)",
)

alg = public_key.get("alg", None)
if alg is None:
raise AuthException(
400,
"Public key failure",
"Failed to load public key, missing alg property",
)

kid = public_key.get("kid", None)
if kid is None:
raise AuthException(
500,
400,
"Public key failure",
"Failed to load public key, missing kid property",
)
try:
# Load and validate public key
return (kid, jwt.PyJWK(public_key))
return (kid, jwt.PyJWK(public_key), alg)
except jwt.InvalidKeyError as e:
raise AuthException(
500,
400,
"Public key failure",
f"Failed to load public key {e}",
)
except jwt.PyJWKError as e:
raise AuthException(
500,
400,
"Public key failure",
f"Failed to load public key {e}",
)
Expand Down Expand Up @@ -118,8 +126,8 @@ def _fetch_public_keys(self) -> None:
self.public_keys = {}
for key in jwkeys:
try:
loaded_kid, pub_key = AuthClient._validate_and_load_public_key(key)
self.public_keys[loaded_kid] = pub_key
loaded_kid, pub_key, alg = AuthClient._validate_and_load_public_key(key)
self.public_keys[loaded_kid] = (pub_key, alg)
except Exception:
# just continue to the next key
pass
Expand Down Expand Up @@ -195,7 +203,9 @@ def _get_identifier_name_by_method(method: DeliveryMethod) -> str:
500, "identifier failure", f"Unknown delivery method {method}"
)

def sign_up_otp(self, method: DeliveryMethod, identifier: str, user: User) -> None:
def sign_up_otp(
self, method: DeliveryMethod, identifier: str, user: User = None
) -> None:
"""
Sign up a new user by OTP

Expand All @@ -219,13 +229,10 @@ def sign_up_otp(self, method: DeliveryMethod, identifier: str, user: User) -> No
f"Identifier {identifier} is not valid by delivery method {method}",
)

if user.username == "":
user.username = identifier
body = {self._get_identifier_name_by_method(method): identifier}

body = {
self._get_identifier_name_by_method(method): identifier,
"user": user.get_data(),
}
if user is not None:
body["user"] = user.get_data()

uri = AuthClient._compose_signup_url(method)
response = requests.post(
Expand Down Expand Up @@ -263,7 +270,7 @@ def sign_in_otp(self, method: DeliveryMethod, identifier: str) -> None:

def verify_code(
self, method: DeliveryMethod, identifier: str, code: str
) -> requests.cookies.RequestsCookieJar:
) -> Tuple[dict, dict]: # Tuple(dict of claims, dict of tokens)
"""Verify OTP code sent by the delivery method that chosen

Args:
Expand All @@ -277,14 +284,13 @@ def verify_code(

code (str): The authorization code you get by the delivery method during signup/signin

Return value (requests.cookies.RequestsCookieJar):
Return the authorization cookies (session token and session refresh token)
cookies can be access as a dict like the following:
for name, val in cookies.items():
response.set_cookie(name, val)
Return value (Tuple[dict, dict]):
Return two dicts where the first contains the jwt claims data and
second that contains the existing signed token (or the new signed
token in case the old one expired) and refreshed session token

Raise:
AuthException: for any case code is not valid and verification failed
AuthException: for any case code is not valid or tokens verification failed
"""

if not self._verify_delivery_method(method, identifier):
Expand All @@ -304,7 +310,12 @@ def verify_code(
)
if not response.ok:
raise AuthException(response.status_code, "", response.reason)
return response.cookies

session_token = response.cookies.get(SESSION_COOKIE_NAME)
refresh_token = response.cookies.get(REFRESH_SESSION_COOKIE_NAME)

claims, tokens = self._validate_and_load_tokens(session_token, refresh_token)
return (claims, tokens)

def refresh_token(self, signed_token: str, signed_refresh_token: str) -> str:
cookies = {
Expand Down Expand Up @@ -334,42 +345,34 @@ def refresh_token(self, signed_token: str, signed_refresh_token: str) -> str:
)
return ds_cookie

def validate_session_request(
def _validate_and_load_tokens(
self, signed_token: str, signed_refresh_token: str
) -> str:
"""
Validate session request by verify the session JWT token
and refresh it in case it expired
) -> Tuple[dict, dict]: # Tuple(dict of claims, dict of tokens)

Args:
signed_token (str): The session JWT token to get its signature verified

signed_refresh_token (str): The session refresh JWT token that will be use to refresh the session token (if expired)

Return value (str):
Return the existing signed token or the signed refreshed token
if token signature expired

Raise:
AuthException: for any case token is not valid means session is not
authorized
"""
if signed_token is None or signed_refresh_token is None:
raise AuthException(
401,
"token validation failure",
f"signed token {signed_token} or/and signed refresh token {signed_refresh_token} are empty",
)

try:
unverified_header = jwt.get_unverified_header(signed_token)
except Exception as e:
raise AuthException(
401,
"token validation failure",
f"Failed to parse token header, {e}",
401, "token validation failure", f"Failed to parse token header, {e}"
)

alg_header = unverified_header.get("alg", None)
if alg_header is None or alg_header == "none":
raise AuthException(
401, "token validation failure", "Token header is missing alg property"
)

kid = unverified_header.get("kid", None)
if kid is None:
raise AuthException(
401,
"token validation failure",
"Token header is missing kid property",
401, "token validation failure", "Token header is missing kid property"
)

with self.lock_public_keys:
Expand All @@ -387,19 +390,89 @@ def validate_session_request(
# (as another thread can change the self.public_keys dict)
copy_key = found_key

alg_from_key = copy_key[1]
if alg_header != alg_from_key:
raise AuthException(
401,
"token validation failure",
"header algorithm is not matched key algorithm",
)

try:
jwt.decode(jwt=signed_token, key=copy_key.key, algorithms=["ES384"])
return signed_token
claims = jwt.decode(
jwt=signed_token, key=copy_key[0].key, algorithms=[alg_header]
)
tokens = {
SESSION_COOKIE_NAME: signed_token,
REFRESH_SESSION_COOKIE_NAME: signed_refresh_token,
}
return (claims, tokens)
except ExpiredSignatureError:
return self.refresh_token(
# Session token expired, check that refresh token is valid
try:
jwt.decode(
jwt=signed_refresh_token,
key=copy_key[0].key,
algorithms=[alg_header],
)
except Exception as e:
raise AuthException(
401, "token validation failure", f"refresh token is not valid, {e}"
)
# Refresh token is valid now refresh the session token
refreshed_session_token = self.refresh_token(
signed_token, signed_refresh_token
) # return the new session cookie
)
# Parse the new session token
try:
claims = jwt.decode(
jwt=refreshed_session_token,
key=copy_key[0].key,
algorithms=[alg_header],
)
tokens = {
SESSION_COOKIE_NAME: refreshed_session_token,
REFRESH_SESSION_COOKIE_NAME: signed_refresh_token,
}
return (claims, tokens)
except Exception as e:
raise AuthException(
401,
"token validation failure",
f"new session token is not valid, {e}",
)
except Exception as e:
raise AuthException(
401, "token validation failure", f"token is not valid, {e}"
)

def logout(self, signed_token: str, signed_refresh_token: str) -> None:
def validate_session_request(
self, signed_token: str, signed_refresh_token: str
) -> Tuple[dict, dict]: # Tuple(dict of claims, dict of tokens)
"""
Validate session request by verify the session JWT session token
and session refresh token in case it expired

Args:
signed_token (str): The session JWT token to get its signature verified

signed_refresh_token (str): The session refresh JWT token that will be
use to refresh the session token (if expired)

Return value (Tuple[dict, dict]):
Return two dicts where the first contains the jwt claims data and
second that contains the existing signed token (or the new signed
token in case the old one expired) and refreshed session token

Raise:
AuthException: for any case token is not valid means session is not
authorized
"""
return self._validate_and_load_tokens(signed_token, signed_refresh_token)

def logout(
self, signed_token: str, signed_refresh_token: str
) -> requests.cookies.RequestsCookieJar:
uri = AuthClient._compose_logout_url()
cookies = {
SESSION_COOKIE_NAME: signed_token,
Expand All @@ -419,6 +492,8 @@ def logout(self, signed_token: str, signed_refresh_token: str) -> None:
f"logout request failed with error {response.text}",
)

return response.cookies

def _get_default_headers(self):
headers = {}
headers["Content-Type"] = "application/json"
Expand Down
Loading