Skip to content

Commit 51e9c96

Browse files
Remove ecdsa dependency (#403)
* refactored jwt module to remove python-jose * updated tests for new jwt module --------- Co-authored-by: Bryan Apellanes <63638027+bryanapellanes-okta@users.noreply.github.com>
1 parent 4227d17 commit 51e9c96

File tree

6 files changed

+58
-41
lines changed

6 files changed

+58
-41
lines changed

okta/jwt.py

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import json
2-
from Cryptodome.PublicKey import RSA
3-
from ast import literal_eval
4-
import jose.jwk as jwk
5-
import jose.jwt as jwt
2+
import os
63
import time
74
import uuid
8-
import os
5+
6+
from ast import literal_eval
7+
from Cryptodome.PublicKey import RSA
8+
from jwcrypto.jwk import JWK, InvalidJWKType
9+
from jwt import encode as jwt_encode
910

1011

1112
class JWT():
@@ -63,32 +64,36 @@ def get_PEM_JWK(private_key):
6364
# if string repr, convert to dict object
6465
if isinstance(private_key, str):
6566
private_key = literal_eval(private_key)
66-
# Create JWK using dict obj
67-
my_jwk = jwk.construct(private_key, JWT.HASH_ALGORITHM)
67+
# remove whitespace from key vaules
68+
private_key = {k: ''.join(private_key[k].split()) for k in private_key}
69+
# ensure private_key is JSON formatted
70+
try:
71+
json.loads(private_key)
72+
except TypeError:
73+
private_key = json.dumps(private_key)
74+
try:
75+
my_jwk = JWK.from_json(private_key)
76+
except InvalidJWKType:
77+
raise ValueError(
78+
"JWK given is of the wrong type")
6879
else: # it's a PEM
6980
# check for filepath or explicit private key
7081
if isinstance(private_key, (str, bytes, os.PathLike)) and os.path.exists(private_key):
71-
# open file if exists and import key
82+
# open file if exists and read
7283
pem_file = open(private_key, 'r')
73-
my_pem = RSA.import_key(pem_file.read())
84+
private_key = pem_file.read()
7485
pem_file.close()
75-
else:
76-
# convert given string to bytes and import key
77-
private_key_bytes = bytes(private_key, 'ascii')
78-
my_pem = RSA.import_key(private_key_bytes)
79-
80-
if not my_pem:
81-
# return error if import failed
82-
return (None, ValueError(
83-
"RSA Private Key given is of the wrong type"))
84-
85-
if my_jwk: # was JWK provided
86-
# get PEM using JWK
87-
pem_bytes = my_jwk.to_pem(JWT.PEM_FORMAT)
88-
my_pem = RSA.import_key(pem_bytes)
89-
else: # was pem provided
90-
# get JWK using PEM
91-
my_jwk = jwk.construct(my_pem.export_key(), JWT.HASH_ALGORITHM)
86+
# remove leading whitespaces from each line
87+
my_pem = '\n'.join([line.strip() for line in private_key.splitlines()])
88+
my_pem = bytes(my_pem, 'ascii')
89+
try:
90+
my_jwk = JWK.from_pem(my_pem)
91+
except ValueError:
92+
raise ValueError(
93+
"RSA Private Key given is of the wrong type")
94+
95+
my_pem = my_jwk.export_to_pem(private_key=True, password=None)
96+
my_pem = RSA.import_key(my_pem)
9297

9398
return (my_pem, my_jwk)
9499

@@ -108,7 +113,7 @@ def create_token(org_url, client_id, private_key, kid=None):
108113
str: Generated JWT
109114
"""
110115
# Generate PEM and JWK
111-
my_pem, my_jwk = JWT.get_PEM_JWK(private_key)
116+
my_pem, _ = JWT.get_PEM_JWK(private_key)
112117
# Get current time and expiry time for token
113118
issued_time = int(time.time())
114119
expiry_time = issued_time + JWT.ONE_HOUR
@@ -142,5 +147,5 @@ def create_token(org_url, client_id, private_key, kid=None):
142147
if "kid" in headers:
143148
del headers["kid"]
144149

145-
token = jwt.encode(claims, my_jwk.to_dict(), JWT.HASH_ALGORITHM, headers=headers)
150+
token = jwt_encode(claims, my_pem.export_key(), JWT.HASH_ALGORITHM, headers)
146151
return token

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ pyyaml
44
xmltodict
55
yarl
66
pycryptodomex
7-
python-jose[cryptography]
7+
jwcrypto
8+
pyjwt
89
aenum
910
pydash
1011
flake8

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ def get_version():
4242
"xmltodict",
4343
"yarl",
4444
"pycryptodomex",
45-
"python-jose",
45+
"jwcrypto",
46+
"pyjwt",
4647
"aenum==3.1.11",
4748
"pydash"
4849
]

tests/mocks.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,3 +416,6 @@ def mock_next_link(self_url: URL):
416416
KLElmMvzocvFaWKvup_a3vPaBi6y4K5kBiq60o-IDMGQ''',
417417
"kid": "5ashWt3LP1zkYwMGbfMsVizRfx52QTyky4GTHd9MykE"
418418
}
419+
420+
SAMPLE_INVALID_JWK = {'foo':'bar'}
421+
SAMPLE_INVALID_RSA = 'foobar'

tests/unit/test_jwt.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,20 @@
77

88

99
def test_private_key_with_kid_in_private_key(mocker):
10-
mocked_encode = mocker.patch('jose.jwt.encode')
10+
mocked_encode = mocker.patch('okta.jwt.jwt_encode')
1111
JWT.create_token("test.com", "test-client-id", mocks.SAMPLE_JWK_WITH_KID)
1212
expected_kid = mocks.SAMPLE_JWK_WITH_KID["kid"]
13-
_, kwargs = mocked_encode.call_args
13+
args = mocked_encode.call_args.args
1414
mocked_encode.assert_called_once()
15-
assert "kid" in kwargs["headers"]
16-
assert kwargs["headers"]["kid"] == expected_kid
15+
assert "kid" in args[-1]
16+
assert args[-1]["kid"] == expected_kid
1717

1818

1919
def test_private_key_with_kid_in_config(mocker):
20-
mocked_encode = mocker.patch('jose.jwt.encode')
20+
mocked_encode = mocker.patch('okta.jwt.jwt_encode')
2121
expected_kid = "test-kid"
2222
JWT.create_token("test.com", "test-client-id", mocks.SAMPLE_JWK, kid=expected_kid)
23-
_, kwargs = mocked_encode.call_args
23+
args = mocked_encode.call_args.args
2424
mocked_encode.assert_called_once()
25-
assert "kid" in kwargs["headers"]
26-
assert kwargs["headers"]["kid"] == expected_kid
25+
assert "kid" in args[-1]
26+
assert args[-1]["kid"] == expected_kid

tests/unit/test_oauth.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def test_private_key_PEM_JWK_dict(jwk_input):
1414
generated_pem, generated_jwk = JWT.get_PEM_JWK(jwk_input)
1515

1616
assert generated_pem is not None and generated_jwk is not None
17-
assert not generated_jwk.is_public()
17+
assert generated_jwk.has_private
1818

1919

2020
def test_private_key_PEM_JWK_file(fs):
@@ -24,11 +24,18 @@ def test_private_key_PEM_JWK_file(fs):
2424
generated_pem, generated_jwk = JWT.get_PEM_JWK(file_path)
2525

2626
assert generated_pem is not None and generated_jwk is not None
27-
assert not generated_jwk.is_public()
27+
assert generated_jwk.has_private
2828

2929

3030
def test_private_key_PEM_JWK_explicit_string():
3131
generated_pem, generated_jwk = JWT.get_PEM_JWK(mocks.SAMPLE_RSA)
3232

3333
assert generated_pem is not None and generated_jwk is not None
34-
assert not generated_jwk.is_public()
34+
assert generated_jwk.has_private
35+
36+
37+
@pytest.mark.parametrize("private_key",
38+
[mocks.SAMPLE_INVALID_JWK, str(mocks.SAMPLE_INVALID_JWK), mocks.SAMPLE_INVALID_RSA])
39+
def test_invalid_private_key_PEM_JWK(private_key):
40+
with pytest.raises(ValueError):
41+
generated_pem, generated_jwk = JWT.get_PEM_JWK(private_key)

0 commit comments

Comments
 (0)