Skip to content

Commit 1a50cee

Browse files
committed
fixes some bugs and added functionality of fetching public key
1 parent 3d07f9a commit 1a50cee

File tree

4 files changed

+128
-22
lines changed

4 files changed

+128
-22
lines changed

descope/auth.py

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010

1111
from descope.common import (
1212
DEFAULT_BASE_URI,
13+
DEFAULT_FETCH_PUBLIC_KEY_URI,
1314
EMAIL_REGEX,
15+
GET_KEYS_PATH,
1416
PHONE_REGEX,
1517
SIGNIN_OTP_PATH,
1618
SIGNUP_OTP_PATH,
@@ -22,8 +24,7 @@
2224

2325

2426
class AuthClient:
25-
def __init__(self, project_id: str, public_key: str):
26-
27+
def __init__(self, project_id: str, public_key: str = None):
2728
# validate project id
2829
if project_id is None or project_id == "":
2930
# try get the project_id from env
@@ -37,14 +38,15 @@ def __init__(self, project_id: str, public_key: str):
3738
self.project_id = project_id
3839

3940
if public_key is None or public_key == "":
40-
public_key = os.getenv("DESCOPE_PUBLIC_KEY", "")
41-
if public_key == "":
42-
raise AuthException(
43-
500,
44-
"Init failure",
45-
"Failed to init AuthClient object, public key cannot be found",
46-
)
41+
public_key = os.getenv("DESCOPE_PUBLIC_KEY", None)
42+
43+
if public_key is None:
44+
self.public_key = None # public key will be fetch later (on demand)
45+
else:
46+
self.public_key = self._validate_and_load_public_key(public_key)
4747

48+
@staticmethod
49+
def _validate_and_load_public_key(public_key) -> jwt.PyJWK:
4850
if isinstance(public_key, str):
4951
try:
5052
public_key = json.loads(public_key)
@@ -64,7 +66,7 @@ def __init__(self, project_id: str, public_key: str):
6466

6567
try:
6668
# Load and validate public key
67-
self.public_key = jwt.PyJWK(public_key)
69+
return jwt.PyJWK(public_key)
6870
except jwt.InvalidKeyError as e:
6971
raise AuthException(
7072
500,
@@ -78,6 +80,40 @@ def __init__(self, project_id: str, public_key: str):
7880
f"Failed to init AuthClient object, failed to load public key {e}",
7981
)
8082

83+
def _fetch_public_key(self, kid: str) -> None:
84+
response = requests.get(
85+
f"{DEFAULT_FETCH_PUBLIC_KEY_URI}{GET_KEYS_PATH}/{self.project_id}",
86+
headers=self._get_default_headers(),
87+
)
88+
89+
if not response.ok:
90+
raise AuthException(
91+
401, "public key fetching failed", f"err: {response.reason}"
92+
)
93+
94+
jwks_data = response.text
95+
try:
96+
jwkeys = json.loads(jwks_data)
97+
except Exception as e:
98+
raise AuthException(
99+
401, "public key fetching failed", f"Failed to load jwks {e}"
100+
)
101+
102+
founded_key = None
103+
for key in jwkeys:
104+
if key["kid"] == kid:
105+
founded_key = key
106+
break
107+
108+
if founded_key:
109+
self.public_key = AuthClient._validate_and_load_public_key(founded_key)
110+
else:
111+
raise AuthException(
112+
401,
113+
"public key validation failed",
114+
"Failed to validate public key, public key not found",
115+
)
116+
81117
@staticmethod
82118
def _verify_delivery_method(method: DeliveryMethod, identifier: str) -> bool:
83119
if identifier == "" or identifier is None:
@@ -219,23 +255,29 @@ def validate_session_request(self, signed_token):
219255
"""
220256
DOC
221257
"""
258+
222259
try:
223260
unverified_header = jwt.get_unverified_header(signed_token)
224261
except Exception as e:
225262
raise AuthException(
226263
401,
227264
"token validation failure",
228-
f"Failed to get unverified token header, {e}",
265+
f"Failed to parse token header, {e}",
229266
)
230-
token_type = unverified_header.get("typ", None)
231-
alg = unverified_header.get("alg", None)
232-
if token_type is None or alg is None:
267+
268+
kid = unverified_header.get("kid", None)
269+
if kid is None:
233270
raise AuthException(
234271
401,
235272
"token validation failure",
236-
f"Token header is missing token type or algorithm, token_type={token_type} alg={alg}",
273+
"Token header is missing kid property",
237274
)
238275

276+
if self.public_key is None:
277+
self._fetch_public_key(
278+
kid
279+
) # will set self.public_key or raise exception if failed
280+
239281
try:
240282
jwt.decode(jwt=signed_token, key=self.public_key.key, algorithms=["ES384"])
241283
except Exception as e:

descope/common.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from enum import Enum
22

33
DEFAULT_BASE_URI = "http://localhost:8191"
4+
DEFAULT_FETCH_PUBLIC_KEY_URI = "http://localhost:8152" # will use the same base uri as above once gateway will be available
45

56
PHONE_REGEX = """^(?:(?:\\(?(?:00|\\+)([1-4]\\d\\d|[1-9]\\d?)\\)?)?[\\-\\.\\ \\\\/]?)?((?:\\(?\\d{1,}\\)?[\\-\\.\\ \\\\/]?){0,})(?:[\\-\\.\\ \\\\/]?(?:#|ext\\.?|extension|x)[\\-\\.\\ \\\\/]?(\\d+))?$"""
67
# EMAIL_REGEX = """^(((([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|((\\x22)((((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(([\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(\\([\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(\\x22)))@((([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$"""
@@ -9,6 +10,7 @@
910
SIGNIN_OTP_PATH = "/v1/auth/signin/otp"
1011
SIGNUP_OTP_PATH = "/v1/auth/signup/otp"
1112
VERIFY_CODE_PATH = "/v1/auth/code/verify"
13+
GET_KEYS_PATH = "/v1/keys"
1214

1315
COOKIE_NAME = "S"
1416

samples/sample_app.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,27 @@ def main():
3737
)
3838
logging.info("Code is valid")
3939
token = cookies.get(COOKIE_NAME)
40-
except AuthException:
41-
logging.info("Invalid code")
40+
except AuthException as e:
41+
logging.info(f"Invalid code {e}")
4242
raise
4343

4444
try:
4545
logging.info("going to validate session..")
4646
auth_client.validate_session_request(token)
4747
logging.info("Session is valid and all is OK")
48-
except AuthException:
49-
logging.info("Session is not valid")
48+
except AuthException as e:
49+
logging.info(f"Session is not valid {e}")
50+
51+
try:
52+
old_public_key = auth_client.public_key
53+
# fetch and load the public key associated with this project (by kid)
54+
auth_client._fetch_public_key(project_id)
55+
if old_public_key != auth_client.public_key:
56+
logging.info("new public key fetched successfully")
57+
else:
58+
logging.info("failed to fetch new public_key")
59+
except AuthException as e:
60+
logging.info(f"failed to fetch public key for this project {e}")
5061

5162
except AuthException:
5263
raise

tests/test_auth.py

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,68 @@ def test_auth_client(self):
2525
AuthException, AuthClient, project_id=None, public_key="dummy"
2626
)
2727
self.assertRaises(AuthException, AuthClient, project_id="", public_key="dummy")
28-
self.assertRaises(
29-
AuthException, AuthClient, project_id="dummy", public_key=None
28+
self.assertIsNotNone(
29+
AuthException, AuthClient(project_id="dummy", public_key=None)
30+
)
31+
self.assertIsNotNone(
32+
AuthException, AuthClient(project_id="dummy", public_key="")
3033
)
31-
self.assertRaises(AuthException, AuthClient, project_id="dummy", public_key="")
3234
self.assertRaises(
3335
AuthException, AuthClient, project_id="dummy", public_key="not dict object"
3436
)
3537
self.assertIsNotNone(
3638
AuthClient(project_id="dummy", public_key=self.public_key_str)
3739
)
3840

41+
def test_validate_and_load_public_key(self):
42+
# test invalid json
43+
self.assertRaises(
44+
AuthException,
45+
AuthClient._validate_and_load_public_key,
46+
public_key="invalid json",
47+
)
48+
# test not dict object
49+
self.assertRaises(
50+
AuthException, AuthClient._validate_and_load_public_key, public_key=555
51+
)
52+
# test invalid dict
53+
self.assertRaises(
54+
AuthException,
55+
AuthClient._validate_and_load_public_key,
56+
public_key={"kid": "dummy"},
57+
)
58+
59+
def test_fetch_public_key(self):
60+
client = AuthClient(self.dummy_project_id, self.public_key_dict)
61+
valid_keys_response = """[
62+
{
63+
"alg": "ES384",
64+
"crv": "P-384",
65+
"kid": "299psneX92K3vpbqPMRCnbZKb27",
66+
"kty": "EC",
67+
"use": "sig",
68+
"x": "435yhcD0tqH6z5M8kNFYEcEYXjzBQWiOvIOZO17rOatpXj-MbA6CKrktiblT4xMb",
69+
"y": "YMf1EIz68z2_RKBys5byWRUXlqNF_BhO5F0SddkaRtiqZ8M6n7ZnKl65JGN0EEGr"
70+
}
71+
]
72+
"""
73+
74+
# Test failed flows
75+
with patch("requests.get") as mock_get:
76+
mock_get.return_value.ok = False
77+
self.assertRaises(AuthException, client._fetch_public_key, "dummy_kid")
78+
79+
with patch("requests.get") as mock_get:
80+
mock_get.return_value.ok = True
81+
mock_get.return_value.text = "invalid json"
82+
self.assertRaises(AuthException, client._fetch_public_key, "dummy_kid")
83+
84+
# test success flow
85+
with patch("requests.get") as mock_get:
86+
mock_get.return_value.ok = True
87+
mock_get.return_value.text = valid_keys_response
88+
self.assertIsNone(client._fetch_public_key("299psneX92K3vpbqPMRCnbZKb27"))
89+
3990
def test_verify_delivery_method(self):
4091
self.assertEqual(
4192
AuthClient._verify_delivery_method(DeliveryMethod.EMAIL, "[email protected]"),

0 commit comments

Comments
 (0)