Skip to content

Commit fef427b

Browse files
authored
Refactor jwt/jwks_client.py without requests dependency (jpadilla#546)
Allows dropping a dependency that isn't very necessary. The requests library was used for a single line of code. This same code is just as easily expressible using the stdlib, thus alllows removing a dependency. Tests were adjusted to mock this new approach.
1 parent 3f65aa4 commit fef427b

File tree

3 files changed

+45
-48
lines changed

3 files changed

+45
-48
lines changed

jwt/jwks_client.py

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,18 @@
1+
import json
2+
import urllib.request
3+
14
from .api_jwk import PyJWKSet
25
from .api_jwt import decode as decode_token
36
from .exceptions import PyJWKClientError
47

5-
try:
6-
import requests
7-
8-
has_requests = True
9-
except ImportError:
10-
has_requests = False
11-
128

139
class PyJWKClient:
1410
def __init__(self, uri):
15-
if not has_requests:
16-
raise PyJWKClientError(
17-
"Missing dependencies for `PyJWKClient`. Run `pip install pyjwt[jwks-client]` to install dependencies."
18-
)
19-
2011
self.uri = uri
2112

2213
def fetch_data(self):
23-
r = requests.get(self.uri)
24-
return r.json()
14+
with urllib.request.urlopen(self.uri) as response:
15+
return json.load(response)
2516

2617
def get_jwk_set(self):
2718
data = self.fetch_data()

setup.cfg

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,19 +48,15 @@ crypto =
4848
tests =
4949
pytest>=6.0.0,<7.0.0
5050
coverage[toml]==5.0.4
51-
requests-mock>=1.7.0,<2.0.0
5251
dev =
5352
sphinx
5453
sphinx-rtd-theme
5554
zope.interface
5655
cryptography>=2.6,<4.0.0
5756
pytest>=6.0.0,<7.0.0
5857
coverage[toml]==5.0.4
59-
requests
6058
mypy
6159
pre-commit
62-
jwks-client =
63-
requests
6460

6561
[options.packages.find]
6662
exclude =

tests/test_jwks_client.py

Lines changed: 40 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import contextlib
2+
import json
3+
from unittest import mock
4+
15
import pytest
2-
import requests_mock
36

47
import jwt
58
from jwt import PyJWKClient
@@ -8,25 +11,37 @@
811

912
from .test_algorithms import has_crypto
1013

14+
RESPONSE_DATA = {
15+
"keys": [
16+
{
17+
"alg": "RS256",
18+
"kty": "RSA",
19+
"use": "sig",
20+
"n": "0wtlJRY9-ru61LmOgieeI7_rD1oIna9QpBMAOWw8wTuoIhFQFwcIi7MFB7IEfelCPj08vkfLsuFtR8cG07EE4uvJ78bAqRjMsCvprWp4e2p7hqPnWcpRpDEyHjzirEJle1LPpjLLVaSWgkbrVaOD0lkWkP1T1TkrOset_Obh8BwtO-Ww-UfrEwxTyz1646AGkbT2nL8PX0trXrmira8GnrCkFUgTUS61GoTdb9bCJ19PLX9Gnxw7J0BtR0GubopXq8KlI0ThVql6ZtVGN2dvmrCPAVAZleM5TVB61m0VSXvGWaF6_GeOhbFoyWcyUmFvzWhBm8Q38vWgsSI7oHTkEw",
21+
"e": "AQAB",
22+
"kid": "NEE1QURBOTM4MzI5RkFDNTYxOTU1MDg2ODgwQ0UzMTk1QjYyRkRFQw",
23+
"x5t": "NEE1QURBOTM4MzI5RkFDNTYxOTU1MDg2ODgwQ0UzMTk1QjYyRkRFQw",
24+
"x5c": [
25+
"MIIDBzCCAe+gAwIBAgIJNtD9Ozi6j2jJMA0GCSqGSIb3DQEBCwUAMCExHzAdBgNVBAMTFmRldi04N2V2eDlydS5hdXRoMC5jb20wHhcNMTkwNjIwMTU0NDU4WhcNMzMwMjI2MTU0NDU4WjAhMR8wHQYDVQQDExZkZXYtODdldng5cnUuYXV0aDAuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0wtlJRY9+ru61LmOgieeI7/rD1oIna9QpBMAOWw8wTuoIhFQFwcIi7MFB7IEfelCPj08vkfLsuFtR8cG07EE4uvJ78bAqRjMsCvprWp4e2p7hqPnWcpRpDEyHjzirEJle1LPpjLLVaSWgkbrVaOD0lkWkP1T1TkrOset/Obh8BwtO+Ww+UfrEwxTyz1646AGkbT2nL8PX0trXrmira8GnrCkFUgTUS61GoTdb9bCJ19PLX9Gnxw7J0BtR0GubopXq8KlI0ThVql6ZtVGN2dvmrCPAVAZleM5TVB61m0VSXvGWaF6/GeOhbFoyWcyUmFvzWhBm8Q38vWgsSI7oHTkEwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQlGXpmYaXFB7Q3eG69Uhjd4cFp/jAOBgNVHQ8BAf8EBAMCAoQwDQYJKoZIhvcNAQELBQADggEBAIzQOF/h4T5WWAdjhcIwdNS7hS2Deq+UxxkRv+uavj6O9mHLuRG1q5onvSFShjECXaYT6OGibn7Ufw/JSm3+86ZouMYjBEqGh4OvWRkwARy1YTWUVDGpT2HAwtIq3lfYvhe8P4VfZByp1N4lfn6X2NcJflG+Q+mfXNmRFyyft3Oq51PCZyyAkU7bTun9FmMOyBtmJvQjZ8RXgBLvu9nUcZB8yTVoeUEg4cLczQlli/OkiFXhWgrhVr8uF0/9klslMFXtm78iYSgR8/oC+k1pSNd1+ESSt7n6+JiAQ2Co+ZNKta7LTDGAjGjNDymyoCrZpeuYQwwnHYEHu/0khjAxhXo="
26+
],
27+
}
28+
]
29+
}
30+
1131

1232
@pytest.fixture
1333
def mocked_response():
14-
return {
15-
"keys": [
16-
{
17-
"alg": "RS256",
18-
"kty": "RSA",
19-
"use": "sig",
20-
"n": "0wtlJRY9-ru61LmOgieeI7_rD1oIna9QpBMAOWw8wTuoIhFQFwcIi7MFB7IEfelCPj08vkfLsuFtR8cG07EE4uvJ78bAqRjMsCvprWp4e2p7hqPnWcpRpDEyHjzirEJle1LPpjLLVaSWgkbrVaOD0lkWkP1T1TkrOset_Obh8BwtO-Ww-UfrEwxTyz1646AGkbT2nL8PX0trXrmira8GnrCkFUgTUS61GoTdb9bCJ19PLX9Gnxw7J0BtR0GubopXq8KlI0ThVql6ZtVGN2dvmrCPAVAZleM5TVB61m0VSXvGWaF6_GeOhbFoyWcyUmFvzWhBm8Q38vWgsSI7oHTkEw",
21-
"e": "AQAB",
22-
"kid": "NEE1QURBOTM4MzI5RkFDNTYxOTU1MDg2ODgwQ0UzMTk1QjYyRkRFQw",
23-
"x5t": "NEE1QURBOTM4MzI5RkFDNTYxOTU1MDg2ODgwQ0UzMTk1QjYyRkRFQw",
24-
"x5c": [
25-
"MIIDBzCCAe+gAwIBAgIJNtD9Ozi6j2jJMA0GCSqGSIb3DQEBCwUAMCExHzAdBgNVBAMTFmRldi04N2V2eDlydS5hdXRoMC5jb20wHhcNMTkwNjIwMTU0NDU4WhcNMzMwMjI2MTU0NDU4WjAhMR8wHQYDVQQDExZkZXYtODdldng5cnUuYXV0aDAuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0wtlJRY9+ru61LmOgieeI7/rD1oIna9QpBMAOWw8wTuoIhFQFwcIi7MFB7IEfelCPj08vkfLsuFtR8cG07EE4uvJ78bAqRjMsCvprWp4e2p7hqPnWcpRpDEyHjzirEJle1LPpjLLVaSWgkbrVaOD0lkWkP1T1TkrOset/Obh8BwtO+Ww+UfrEwxTyz1646AGkbT2nL8PX0trXrmira8GnrCkFUgTUS61GoTdb9bCJ19PLX9Gnxw7J0BtR0GubopXq8KlI0ThVql6ZtVGN2dvmrCPAVAZleM5TVB61m0VSXvGWaF6/GeOhbFoyWcyUmFvzWhBm8Q38vWgsSI7oHTkEwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQlGXpmYaXFB7Q3eG69Uhjd4cFp/jAOBgNVHQ8BAf8EBAMCAoQwDQYJKoZIhvcNAQELBQADggEBAIzQOF/h4T5WWAdjhcIwdNS7hS2Deq+UxxkRv+uavj6O9mHLuRG1q5onvSFShjECXaYT6OGibn7Ufw/JSm3+86ZouMYjBEqGh4OvWRkwARy1YTWUVDGpT2HAwtIq3lfYvhe8P4VfZByp1N4lfn6X2NcJflG+Q+mfXNmRFyyft3Oq51PCZyyAkU7bTun9FmMOyBtmJvQjZ8RXgBLvu9nUcZB8yTVoeUEg4cLczQlli/OkiFXhWgrhVr8uF0/9klslMFXtm78iYSgR8/oC+k1pSNd1+ESSt7n6+JiAQ2Co+ZNKta7LTDGAjGjNDymyoCrZpeuYQwwnHYEHu/0khjAxhXo="
26-
],
27-
}
28-
]
29-
}
34+
@contextlib.contextmanager
35+
def _mocked_response(data):
36+
with mock.patch("urllib.request.urlopen") as urlopen_mock:
37+
response = mock.Mock()
38+
response.__enter__ = mock.Mock(return_value=response)
39+
response.__exit__ = mock.Mock()
40+
response.read.side_effect = [json.dumps(data)]
41+
urlopen_mock.return_value = response
42+
yield
43+
44+
return _mocked_response
3045

3146

3247
@pytest.mark.skipif(
@@ -36,8 +51,7 @@ class TestPyJWKClient:
3651
def test_get_jwk_set(self, mocked_response):
3752
url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json"
3853

39-
with requests_mock.mock() as m:
40-
m.get(url, json=mocked_response)
54+
with mocked_response(RESPONSE_DATA):
4155
jwks_client = PyJWKClient(url)
4256
jwk_set = jwks_client.get_jwk_set()
4357

@@ -46,8 +60,7 @@ def test_get_jwk_set(self, mocked_response):
4660
def test_get_signing_keys(self, mocked_response):
4761
url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json"
4862

49-
with requests_mock.mock() as m:
50-
m.get(url, json=mocked_response)
63+
with mocked_response(RESPONSE_DATA):
5164
jwks_client = PyJWKClient(url)
5265
signing_keys = jwks_client.get_signing_keys()
5366

@@ -57,11 +70,10 @@ def test_get_signing_keys(self, mocked_response):
5770
def test_get_signing_keys_raises_if_none_found(self, mocked_response):
5871
url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json"
5972

60-
with requests_mock.mock() as m:
61-
mocked_key = mocked_response["keys"][0].copy()
62-
mocked_key["use"] = "enc"
63-
response = {"keys": [mocked_key]}
64-
m.get(url, json=response)
73+
mocked_key = RESPONSE_DATA["keys"][0].copy()
74+
mocked_key["use"] = "enc"
75+
response = {"keys": [mocked_key]}
76+
with mocked_response(response):
6577
jwks_client = PyJWKClient(url)
6678

6779
with pytest.raises(PyJWKClientError) as exc:
@@ -75,8 +87,7 @@ def test_get_signing_key(self, mocked_response):
7587
url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json"
7688
kid = "NEE1QURBOTM4MzI5RkFDNTYxOTU1MDg2ODgwQ0UzMTk1QjYyRkRFQw"
7789

78-
with requests_mock.mock() as m:
79-
m.get(url, json=mocked_response)
90+
with mocked_response(RESPONSE_DATA):
8091
jwks_client = PyJWKClient(url)
8192
signing_key = jwks_client.get_signing_key(kid)
8293

@@ -89,8 +100,7 @@ def test_get_signing_key_from_jwt(self, mocked_response):
89100
token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik5FRTFRVVJCT1RNNE16STVSa0ZETlRZeE9UVTFNRGcyT0Rnd1EwVXpNVGsxUWpZeVJrUkZRdyJ9.eyJpc3MiOiJodHRwczovL2Rldi04N2V2eDlydS5hdXRoMC5jb20vIiwic3ViIjoiYVc0Q2NhNzl4UmVMV1V6MGFFMkg2a0QwTzNjWEJWdENAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vZXhwZW5zZXMtYXBpIiwiaWF0IjoxNTcyMDA2OTU0LCJleHAiOjE1NzIwMDY5NjQsImF6cCI6ImFXNENjYTc5eFJlTFdVejBhRTJINmtEME8zY1hCVnRDIiwiZ3R5IjoiY2xpZW50LWNyZWRlbnRpYWxzIn0.PUxE7xn52aTCohGiWoSdMBZGiYAHwE5FYie0Y1qUT68IHSTXwXVd6hn02HTah6epvHHVKA2FqcFZ4GGv5VTHEvYpeggiiZMgbxFrmTEY0csL6VNkX1eaJGcuehwQCRBKRLL3zKmA5IKGy5GeUnIbpPHLHDxr-GXvgFzsdsyWlVQvPX2xjeaQ217r2PtxDeqjlf66UYl6oY6AqNS8DH3iryCvIfCcybRZkc_hdy-6ZMoKT6Piijvk_aXdm7-QQqKJFHLuEqrVSOuBqqiNfVrG27QzAPuPOxvfXTVLXL2jek5meH6n-VWgrBdoMFH93QEszEDowDAEhQPHVs0xj7SIzA"
90101
url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json"
91102

92-
with requests_mock.mock() as m:
93-
m.get(url, json=mocked_response)
103+
with mocked_response(RESPONSE_DATA):
94104
jwks_client = PyJWKClient(url)
95105
signing_key = jwks_client.get_signing_key_from_jwt(token)
96106

0 commit comments

Comments
 (0)