Skip to content

Commit 10a4d12

Browse files
mumarkhan999M Umar Khan
andauthored
feat!: remove pyjwkest (#36707)
Co-authored-by: M Umar Khan <[email protected]>
1 parent 0a7d894 commit 10a4d12

3 files changed

Lines changed: 76 additions & 69 deletions

File tree

lms/envs/test.py

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -651,28 +651,40 @@
651651
'JWT_ISSUER': 'token-test-issuer',
652652
'JWT_SIGNING_ALGORITHM': 'RS512',
653653
'JWT_SUPPORTED_VERSION': '1.2.0',
654-
'JWT_PRIVATE_SIGNING_JWK': '''{
655-
"e": "AQAB",
656-
"d": "HIiV7KNjcdhVbpn3KT-I9n3JPf5YbGXsCIedmPqDH1d4QhBofuAqZ9zebQuxkRUpmqtYMv0Zi6ECSUqH387GYQF_XvFUFcjQRPycISd8TH0DAKaDpGr-AYNshnKiEtQpINhcP44I1AYNPCwyoxXA1fGTtmkKChsuWea7o8kytwU5xSejvh5-jiqu2SF4GEl0BEXIAPZsgbzoPIWNxgO4_RzNnWs6nJZeszcaDD0CyezVSuH9QcI6g5QFzAC_YuykSsaaFJhZ05DocBsLczShJ9Omf6PnK9xlm26I84xrEh_7x4fVmNBg3xWTLh8qOnHqGko93A1diLRCrKHOvnpvgQ",
657-
"n": "o5cn3ljSRi6FaDEKTn0PS-oL9EFyv1pI7dRgffQLD1qf5D6sprmYfWWokSsrWig8u2y0HChSygR6Jn5KXBqQn6FpM0dDJLnWQDRXHLl3Ey1iPYgDSmOIsIGrV9ZyNCQwk03wAgWbfdBTig3QSDYD-sTNOs3pc4UD_PqAvU2nz_1SS2ZiOwOn5F6gulE1L0iE3KEUEvOIagfHNVhz0oxa_VRZILkzV-zr6R_TW1m97h4H8jXl_VJyQGyhMGGypuDrQ9_vaY_RLEulLCyY0INglHWQ7pckxBtI5q55-Vio2wgewe2_qYcGsnBGaDNbySAsvYcWRrqDiFyzrJYivodqTQ",
658-
"q": "3T3DEtBUka7hLGdIsDlC96Uadx_q_E4Vb1cxx_4Ss_wGp1Loz3N3ZngGyInsKlmbBgLo1Ykd6T9TRvRNEWEtFSOcm2INIBoVoXk7W5RuPa8Cgq2tjQj9ziGQ08JMejrPlj3Q1wmALJr5VTfvSYBu0WkljhKNCy1KB6fCby0C9WE",
659-
"p": "vUqzWPZnDG4IXyo-k5F0bHV0BNL_pVhQoLW7eyFHnw74IOEfSbdsMspNcPSFIrtgPsn7981qv3lN_staZ6JflKfHayjB_lvltHyZxfl0dvruShZOx1N6ykEo7YrAskC_qxUyrIvqmJ64zPW3jkuOYrFs7Ykj3zFx3Zq1H5568G0",
660-
"kid": "token-test-sign", "kty": "RSA"
661-
}''',
662-
'JWT_PUBLIC_SIGNING_JWK_SET': '''{
663-
"keys": [
654+
'JWT_PRIVATE_SIGNING_JWK': """
655+
{
656+
"kid": "token-test-sign",
657+
"kty": "RSA",
658+
"key_ops": [
659+
"sign"
660+
],
661+
"n": "o5cn3ljSRi6FaDEKTn0PS-oL9EFyv1pI7dRgffQLD1qf5D6sprmYfWWokSsrWig8u2y0HChSygR6Jn5KXBqQn6FpM0dDJLnWQDRXHLl3Ey1iPYgDSmOIsIGrV9ZyNCQwk03wAgWbfdBTig3QSDYD-sTNOs3pc4UD_PqAvU2nz_1SS2ZiOwOn5F6gulE1L0iE3KEUEvOIagfHNVhz0oxa_VRZILkzV-zr6R_TW1m97h4H8jXl_VJyQGyhMGGypuDrQ9_vaY_RLEulLCyY0INglHWQ7pckxBtI5q55-Vio2wgewe2_qYcGsnBGaDNbySAsvYcWRrqDiFyzrJYivodqTQ",
662+
"e": "AQAB",
663+
"d": "HIiV7KNjcdhVbpn3KT-I9n3JPf5YbGXsCIedmPqDH1d4QhBofuAqZ9zebQuxkRUpmqtYMv0Zi6ECSUqH387GYQF_XvFUFcjQRPycISd8TH0DAKaDpGr-AYNshnKiEtQpINhcP44I1AYNPCwyoxXA1fGTtmkKChsuWea7o8kytwU5xSejvh5-jiqu2SF4GEl0BEXIAPZsgbzoPIWNxgO4_RzNnWs6nJZeszcaDD0CyezVSuH9QcI6g5QFzAC_YuykSsaaFJhZ05DocBsLczShJ9Omf6PnK9xlm26I84xrEh_7x4fVmNBg3xWTLh8qOnHqGko93A1diLRCrKHOvnpvgQ",
664+
"p": "3T3DEtBUka7hLGdIsDlC96Uadx_q_E4Vb1cxx_4Ss_wGp1Loz3N3ZngGyInsKlmbBgLo1Ykd6T9TRvRNEWEtFSOcm2INIBoVoXk7W5RuPa8Cgq2tjQj9ziGQ08JMejrPlj3Q1wmALJr5VTfvSYBu0WkljhKNCy1KB6fCby0C9WE",
665+
"q": "vUqzWPZnDG4IXyo-k5F0bHV0BNL_pVhQoLW7eyFHnw74IOEfSbdsMspNcPSFIrtgPsn7981qv3lN_staZ6JflKfHayjB_lvltHyZxfl0dvruShZOx1N6ykEo7YrAskC_qxUyrIvqmJ64zPW3jkuOYrFs7Ykj3zFx3Zq1H5568G0",
666+
"dp": "Azh08H8r2_sJuBXAzx_mQ6iZnAZQ619PnJFOXjTqnMgcaK8iSHLL2CgDIUQwteUcBphgP0uBrfWIBs5jmM8rUtVz4CcrPb5jdjhHjuu4NxmnFbPlhNoOp8OBUjPP3S-h-fPoaFjxDrUqz_zCdPVzp4S6UTkf6Hu-SiI9CFVFZ8E",
667+
"dq": "WQ44_KTIbIej9qnYUPMA1DoaAF8ImVDIdiOp9c79dC7FvCpN3w-lnuugrYDM1j9Tk5bRrY7-JuE6OaKQgOtajoS1BIxjYHj5xAVPD15CVevOihqeq5Zx0ZAAYmmCKRrfUe0iLx2QnIcoKH1-Azs23OXeeo6nysznZjvv9NVJv60",
668+
"qi": "KSWGH607H1kNG2okjYdmVdNgLxTUB-Wye9a9FNFE49UmQIOJeZYXtDzcjk8IiK3g-EU3CqBeDKVUgHvHFu4_Wj3IrIhKYizS4BeFmOcPDvylDQCmJcC9tXLQgHkxM_MEJ7iLn9FOLRshh7GPgZphXxMhezM26Cz-8r3_mACHu84"
669+
}
670+
""", # noqa: E501,
671+
672+
'JWT_PUBLIC_SIGNING_JWK_SET': """
673+
{
674+
"keys": [
664675
{
665-
"kid":"token-test-wrong-key",
666-
"e": "AQAB",
667-
"kty": "RSA",
668-
"n": "o5cn3ljSRi6FaDEKTn0PS-oL9EFyv1pI7dffgRQLD1qf5D6sprmYfWVokSsrWig8u2y0HChSygR6Jn5KXBqQn6FpM0dDJLnWQDRXHLl3Ey1iPYgDSmOIsIGrV9ZyNCQwk03wAgWbfdBTig3QSDYD-sTNOs3pc4UD_PqAvU2nz_1SS2ZiOwOn5F6gulE1L0iE3KEUEvOIagfHNVhz0oxa_VRZILkzV-zr6R_TW1m97h4H8jXl_VJyQGyhMGGypuDrQ9_vaY_RLEulLCyY0INglHWQ7pckxBtI5q55-Vio2wgewe2_qYcGsnBGaDNbySAsvYcWRrqDiFyzrJYivodqTQ"
676+
"kid": "token-test-sign",
677+
"kty": "RSA",
678+
"n": "o5cn3ljSRi6FaDEKTn0PS-oL9EFyv1pI7dRgffQLD1qf5D6sprmYfWWokSsrWig8u2y0HChSygR6Jn5KXBqQn6FpM0dDJLnWQDRXHLl3Ey1iPYgDSmOIsIGrV9ZyNCQwk03wAgWbfdBTig3QSDYD-sTNOs3pc4UD_PqAvU2nz_1SS2ZiOwOn5F6gulE1L0iE3KEUEvOIagfHNVhz0oxa_VRZILkzV-zr6R_TW1m97h4H8jXl_VJyQGyhMGGypuDrQ9_vaY_RLEulLCyY0INglHWQ7pckxBtI5q55-Vio2wgewe2_qYcGsnBGaDNbySAsvYcWRrqDiFyzrJYivodqTQ",
679+
"e": "AQAB"
669680
},
670681
{
671-
"kid":"token-test-sign",
672-
"e": "AQAB",
673-
"kty": "RSA",
674-
"n": "o5cn3ljSRi6FaDEKTn0PS-oL9EFyv1pI7dRgffQLD1qf5D6sprmYfWWokSsrWig8u2y0HChSygR6Jn5KXBqQn6FpM0dDJLnWQDRXHLl3Ey1iPYgDSmOIsIGrV9ZyNCQwk03wAgWbfdBTig3QSDYD-sTNOs3pc4UD_PqAvU2nz_1SS2ZiOwOn5F6gulE1L0iE3KEUEvOIagfHNVhz0oxa_VRZILkzV-zr6R_TW1m97h4H8jXl_VJyQGyhMGGypuDrQ9_vaY_RLEulLCyY0INglHWQ7pckxBtI5q55-Vio2wgewe2_qYcGsnBGaDNbySAsvYcWRrqDiFyzrJYivodqTQ"
682+
"kid": "token-test-wrong-key",
683+
"kty": "RSA",
684+
"n": "o5cn3ljSRi6FaDEKTn0PS-oL9EFyv1pI7dRgffQLD1qf5D6sprmYfWWokSsrWig8u2y0HChSygR6Jn5KXBqQn6FpM0dDJLnWQDRXHLl3Ey1iPYgDSmOIsIGrV9ZyNCQwk03wAgWbfdBTig3QSDYD-sTNOs3pc4UD_PqAvU2nz_1SS2ZiOwOn5F6gulE1L0iE3KEUEvOIagfHNVhz0oxa_VRZILkzV-zr6R_TW1m97h4H8jXl_VJyQGyhMGGypuDrQ9_vaY_RLEulLCyY0INglHWQ7pckxBtI5q55-Vio2wgewe2_qYcGsnBGaDNbySAsvYcWRrqDiFyzrJYivodqTQ",
685+
"e": "AQAB"
675686
}
676-
]
677-
}''',
687+
]
688+
}
689+
""", # noqa: E501
678690
}

openedx/core/lib/jwt.py

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
JWT Token handling and signing functions.
33
"""
44

5-
import json
5+
import jwt
66
from time import time
77

88
from django.conf import settings
9-
from jwkest import Expired, Invalid, MissingKey, jwk
10-
from jwkest.jws import JWS
9+
from jwt.api_jwk import PyJWK, PyJWKSet
10+
from jwt.exceptions import ExpiredSignatureError, InvalidSignatureError, MissingRequiredClaimError
1111

1212

1313
def create_jwt(lms_user_id, expires_in_seconds, additional_token_claims, now=None):
@@ -40,15 +40,9 @@ def _encode_and_sign(payload):
4040
4141
The signing key and algorithm are pulled from settings.
4242
"""
43-
keys = jwk.KEYS()
44-
45-
serialized_keypair = json.loads(settings.TOKEN_SIGNING['JWT_PRIVATE_SIGNING_JWK'])
46-
keys.add(serialized_keypair)
43+
private_key = PyJWK.from_json(settings.TOKEN_SIGNING['JWT_PRIVATE_SIGNING_JWK'])
4744
algorithm = settings.TOKEN_SIGNING['JWT_SIGNING_ALGORITHM']
48-
49-
data = json.dumps(payload)
50-
jws = JWS(data, alg=algorithm)
51-
return jws.sign_compact(keys=keys)
45+
return jwt.encode(payload, key=private_key.key, algorithm=algorithm)
5246

5347

5448
def unpack_jwt(token, lms_user_id, now=None):
@@ -65,27 +59,40 @@ def unpack_jwt(token, lms_user_id, now=None):
6559
Returns a valid, decoded json payload (string).
6660
"""
6761
now = now or int(time())
68-
payload = _unpack_and_verify(token)
62+
payload = unpack_and_verify(token)
6963

7064
if "lms_user_id" not in payload:
71-
raise MissingKey("LMS user id is missing")
65+
raise MissingRequiredClaimError("LMS user id is missing")
7266
if "exp" not in payload:
73-
raise MissingKey("Expiration is missing")
67+
raise MissingRequiredClaimError("Expiration is missing")
7468
if payload["lms_user_id"] != lms_user_id:
75-
raise Invalid("User does not match")
69+
raise InvalidSignatureError("User does not match")
7670
if payload["exp"] < now:
77-
raise Expired("Token is expired")
71+
raise ExpiredSignatureError("Token is expired")
7872

7973
return payload
8074

8175

82-
def _unpack_and_verify(token):
76+
def unpack_and_verify(token): # pylint: disable=inconsistent-return-statements
8377
"""
8478
Unpack and verify the provided token.
8579
8680
The signing key and algorithm are pulled from settings.
8781
"""
88-
keys = jwk.KEYS()
89-
keys.load_jwks(settings.TOKEN_SIGNING['JWT_PUBLIC_SIGNING_JWK_SET'])
90-
decoded = JWS().verify_compact(token.encode('utf-8'), keys)
91-
return decoded
82+
key_set = []
83+
key_set.extend(
84+
PyJWKSet.from_json(settings.TOKEN_SIGNING["JWT_PUBLIC_SIGNING_JWK_SET"]).keys
85+
)
86+
87+
for i in range(len(key_set)): # pylint: disable=consider-using-enumerate
88+
try:
89+
decoded = jwt.decode(
90+
token,
91+
key=key_set[i].key,
92+
algorithms=["RS256", "RS512"],
93+
options={"verify_signature": True, "verify_aud": False},
94+
)
95+
return decoded
96+
except Exception: # pylint: disable=broad-exception-caught
97+
if i == len(key_set) - 1:
98+
raise

openedx/core/lib/tests/test_jwt.py

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,23 @@
22
Tests for token handling
33
"""
44
import unittest
5+
from time import time
56

6-
from django.conf import settings
7-
from jwkest import BadSignature, Expired, Invalid, MissingKey, jwk
8-
from jwkest.jws import JWS
7+
from jwt.exceptions import ExpiredSignatureError, InvalidSignatureError, MissingRequiredClaimError
98

109
from openedx.core.djangolib.testing.utils import skip_unless_lms
11-
from openedx.core.lib.jwt import _encode_and_sign, create_jwt, unpack_jwt
10+
from openedx.core.lib.jwt import _encode_and_sign, create_jwt, unpack_jwt, unpack_and_verify
1211

1312

1413
test_user_id = 121
1514
invalid_test_user_id = 120
16-
test_timeout = 60
17-
test_now = 1661432902
15+
test_timeout = 1000
16+
test_now = int(time())
1817
test_claims = {"foo": "bar", "baz": "quux", "meaning": 42}
1918
expected_full_token = {
2019
"lms_user_id": test_user_id,
21-
"iat": 1661432902,
22-
"exp": 1661432902 + 60,
20+
"iat": test_now,
21+
"exp": test_now + test_timeout,
2322
"iss": "token-test-issuer", # these lines from test_settings.py
2423
"version": "1.2.0", # these lines from test_settings.py
2524
}
@@ -34,7 +33,7 @@ class TestSign(unittest.TestCase):
3433
def test_create_jwt(self):
3534
token = create_jwt(test_user_id, test_timeout, {}, test_now)
3635

37-
decoded = _verify_jwt(token)
36+
decoded = unpack_and_verify(token)
3837
self.assertEqual(expected_full_token, decoded)
3938

4039
def test_create_jwt_with_claims(self):
@@ -43,7 +42,7 @@ def test_create_jwt_with_claims(self):
4342
expected_token_with_claims = expected_full_token.copy()
4443
expected_token_with_claims.update(test_claims)
4544

46-
decoded = _verify_jwt(token)
45+
decoded = unpack_and_verify(token)
4746
self.assertEqual(expected_token_with_claims, decoded)
4847

4948
def test_malformed_token(self):
@@ -53,19 +52,8 @@ def test_malformed_token(self):
5352
expected_token_with_claims = expected_full_token.copy()
5453
expected_token_with_claims.update(test_claims)
5554

56-
with self.assertRaises(BadSignature):
57-
_verify_jwt(token)
58-
59-
60-
def _verify_jwt(jwt_token):
61-
"""
62-
Helper function which verifies the signature and decodes the token
63-
from string back to claims form
64-
"""
65-
keys = jwk.KEYS()
66-
keys.load_jwks(settings.TOKEN_SIGNING['JWT_PUBLIC_SIGNING_JWK_SET'])
67-
decoded = JWS().verify_compact(jwt_token.encode('utf-8'), keys)
68-
return decoded
55+
with self.assertRaises(InvalidSignatureError):
56+
unpack_and_verify(token)
6957

7058

7159
@skip_unless_lms
@@ -97,33 +85,33 @@ def test_malformed_token(self):
9785
expected_token_with_claims = expected_full_token.copy()
9886
expected_token_with_claims.update(test_claims)
9987

100-
with self.assertRaises(BadSignature):
88+
with self.assertRaises(InvalidSignatureError):
10189
unpack_jwt(token, test_user_id, test_now)
10290

10391
def test_unpack_token_with_invalid_user(self):
10492
token = create_jwt(invalid_test_user_id, test_timeout, {}, test_now)
10593

106-
with self.assertRaises(Invalid):
94+
with self.assertRaises(InvalidSignatureError):
10795
unpack_jwt(token, test_user_id, test_now)
10896

10997
def test_unpack_expired_token(self):
11098
token = create_jwt(test_user_id, test_timeout, {}, test_now)
11199

112-
with self.assertRaises(Expired):
100+
with self.assertRaises(ExpiredSignatureError):
113101
unpack_jwt(token, test_user_id, test_now + test_timeout + 1)
114102

115103
def test_missing_expired_lms_user_id(self):
116104
payload = expected_full_token.copy()
117105
del payload['lms_user_id']
118106
token = _encode_and_sign(payload)
119107

120-
with self.assertRaises(MissingKey):
108+
with self.assertRaises(MissingRequiredClaimError):
121109
unpack_jwt(token, test_user_id, test_now)
122110

123111
def test_missing_expired_key(self):
124112
payload = expected_full_token.copy()
125113
del payload['exp']
126114
token = _encode_and_sign(payload)
127115

128-
with self.assertRaises(MissingKey):
116+
with self.assertRaises(MissingRequiredClaimError):
129117
unpack_jwt(token, test_user_id, test_now)

0 commit comments

Comments
 (0)