From d5ed9300c2a3291265a8bce84d01238923f83765 Mon Sep 17 00:00:00 2001 From: Steven Pitts Date: Tue, 2 Feb 2021 16:33:14 -0500 Subject: [PATCH 01/10] Cache the result of get_signing_key --- jwt/jwks_client.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/jwt/jwks_client.py b/jwt/jwks_client.py index a6c0d9fb..1fd9aeac 100644 --- a/jwt/jwks_client.py +++ b/jwt/jwks_client.py @@ -1,6 +1,6 @@ import json import urllib.request -from typing import Any, List +from typing import Any, Dict, List from .api_jwk import PyJWK, PyJWKSet from .api_jwt import decode_complete as decode_token @@ -10,6 +10,7 @@ class PyJWKClient: def __init__(self, uri: str): self.uri = uri + self._known_signing_keys: Dict[str, PyJWK] = {} def fetch_data(self) -> Any: with urllib.request.urlopen(self.uri) as response: @@ -33,10 +34,13 @@ def get_signing_keys(self) -> List[PyJWK]: return signing_keys def get_signing_key(self, kid: str) -> PyJWK: + if kid in self._known_signing_keys: + return self._known_signing_keys[kid] signing_keys = self.get_signing_keys() signing_key = None for key in signing_keys: + self._known_signing_keys[key.key_id] = key if key.key_id == kid: signing_key = key break From 22e59a2fbfde42c0bf6cd554ddfd4a4284cf1f03 Mon Sep 17 00:00:00 2001 From: Steven Pitts Date: Tue, 2 Feb 2021 16:40:28 -0500 Subject: [PATCH 02/10] Include URI in key for getting known signing keys --- jwt/jwks_client.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/jwt/jwks_client.py b/jwt/jwks_client.py index 1fd9aeac..5279fd25 100644 --- a/jwt/jwks_client.py +++ b/jwt/jwks_client.py @@ -1,6 +1,6 @@ import json import urllib.request -from typing import Any, Dict, List +from typing import Any, Dict, List, Tuple from .api_jwk import PyJWK, PyJWKSet from .api_jwt import decode_complete as decode_token @@ -10,7 +10,8 @@ class PyJWKClient: def __init__(self, uri: str): self.uri = uri - self._known_signing_keys: Dict[str, PyJWK] = {} + # Map (uri, kid) to signing keys + self._known_signing_keys: Dict[Tuple[str, str], PyJWK] = {} def fetch_data(self) -> Any: with urllib.request.urlopen(self.uri) as response: @@ -34,13 +35,13 @@ def get_signing_keys(self) -> List[PyJWK]: return signing_keys def get_signing_key(self, kid: str) -> PyJWK: - if kid in self._known_signing_keys: - return self._known_signing_keys[kid] + if (self.uri, kid) in self._known_signing_keys: + return self._known_signing_keys[(self.uri, kid)] signing_keys = self.get_signing_keys() signing_key = None for key in signing_keys: - self._known_signing_keys[key.key_id] = key + self._known_signing_keys[(self.uri, key.key_id)] = key if key.key_id == kid: signing_key = key break From cbf7a98321f3f9339af551eddd7295e84ef9968a Mon Sep 17 00:00:00 2001 From: Steven Pitts Date: Tue, 2 Feb 2021 16:51:29 -0500 Subject: [PATCH 03/10] Add test_get_signing_key_caches_result test --- tests/test_jwks_client.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/test_jwks_client.py b/tests/test_jwks_client.py index 7c3dde44..7640438c 100644 --- a/tests/test_jwks_client.py +++ b/tests/test_jwks_client.py @@ -37,7 +37,7 @@ def mocked_response(data): response.__exit__ = mock.Mock() response.read.side_effect = [json.dumps(data)] urlopen_mock.return_value = response - yield + yield urlopen_mock @crypto_required @@ -88,6 +88,17 @@ def test_get_signing_key(self): assert signing_key.key_id == kid assert signing_key.public_key_use == "sig" + def test_get_signing_key_caches_result(self): + url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" + kid = "NEE1QURBOTM4MzI5RkFDNTYxOTU1MDg2ODgwQ0UzMTk1QjYyRkRFQw" + + with mocked_response(RESPONSE_DATA) as network_response: + jwks_client = PyJWKClient(url) + jwks_client.get_signing_key(kid) + jwks_client.get_signing_key(kid) + + assert network_response.call_count == 1 + def test_get_signing_key_from_jwt(self): token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik5FRTFRVVJCT1RNNE16STVSa0ZETlRZeE9UVTFNRGcyT0Rnd1EwVXpNVGsxUWpZeVJrUkZRdyJ9.eyJpc3MiOiJodHRwczovL2Rldi04N2V2eDlydS5hdXRoMC5jb20vIiwic3ViIjoiYVc0Q2NhNzl4UmVMV1V6MGFFMkg2a0QwTzNjWEJWdENAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vZXhwZW5zZXMtYXBpIiwiaWF0IjoxNTcyMDA2OTU0LCJleHAiOjE1NzIwMDY5NjQsImF6cCI6ImFXNENjYTc5eFJlTFdVejBhRTJINmtEME8zY1hCVnRDIiwiZ3R5IjoiY2xpZW50LWNyZWRlbnRpYWxzIn0.PUxE7xn52aTCohGiWoSdMBZGiYAHwE5FYie0Y1qUT68IHSTXwXVd6hn02HTah6epvHHVKA2FqcFZ4GGv5VTHEvYpeggiiZMgbxFrmTEY0csL6VNkX1eaJGcuehwQCRBKRLL3zKmA5IKGy5GeUnIbpPHLHDxr-GXvgFzsdsyWlVQvPX2xjeaQ217r2PtxDeqjlf66UYl6oY6AqNS8DH3iryCvIfCcybRZkc_hdy-6ZMoKT6Piijvk_aXdm7-QQqKJFHLuEqrVSOuBqqiNfVrG27QzAPuPOxvfXTVLXL2jek5meH6n-VWgrBdoMFH93QEszEDowDAEhQPHVs0xj7SIzA" url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" From 77a44f4c5724aaad4ed85bf8314695d24f33304e Mon Sep 17 00:00:00 2001 From: Steven Pitts Date: Tue, 2 Feb 2021 17:07:12 -0500 Subject: [PATCH 04/10] Add test to make sure multiple uris are being distinguished in key caching --- tests/test_jwks_client.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_jwks_client.py b/tests/test_jwks_client.py index 7640438c..c5065b74 100644 --- a/tests/test_jwks_client.py +++ b/tests/test_jwks_client.py @@ -99,6 +99,24 @@ def test_get_signing_key_caches_result(self): assert network_response.call_count == 1 + def test_get_signing_key_cache_distinguishes_uris(self): + url_1 = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" + url_2 = "https://dev-88evx9ru.auth0.com/.well-known/jwks.json" + kid = "NEE1QURBOTM4MzI5RkFDNTYxOTU1MDg2ODgwQ0UzMTk1QjYyRkRFQw" + + with mocked_response(RESPONSE_DATA): + jwks_client = PyJWKClient(url_1) + jwks_client.get_signing_key(kid) + + # mocked_response does not allow urllib.request.urlopen to be called twice + # so a second mock is needed + with mocked_response(RESPONSE_DATA): + jwks_client.uri = url_2 + jwks_client.get_signing_key(kid) + + for uri_kid_pair in ((url_1, kid), (url_2, kid)): + assert uri_kid_pair in jwks_client._known_signing_keys + def test_get_signing_key_from_jwt(self): token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik5FRTFRVVJCT1RNNE16STVSa0ZETlRZeE9UVTFNRGcyT0Rnd1EwVXpNVGsxUWpZeVJrUkZRdyJ9.eyJpc3MiOiJodHRwczovL2Rldi04N2V2eDlydS5hdXRoMC5jb20vIiwic3ViIjoiYVc0Q2NhNzl4UmVMV1V6MGFFMkg2a0QwTzNjWEJWdENAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vZXhwZW5zZXMtYXBpIiwiaWF0IjoxNTcyMDA2OTU0LCJleHAiOjE1NzIwMDY5NjQsImF6cCI6ImFXNENjYTc5eFJlTFdVejBhRTJINmtEME8zY1hCVnRDIiwiZ3R5IjoiY2xpZW50LWNyZWRlbnRpYWxzIn0.PUxE7xn52aTCohGiWoSdMBZGiYAHwE5FYie0Y1qUT68IHSTXwXVd6hn02HTah6epvHHVKA2FqcFZ4GGv5VTHEvYpeggiiZMgbxFrmTEY0csL6VNkX1eaJGcuehwQCRBKRLL3zKmA5IKGy5GeUnIbpPHLHDxr-GXvgFzsdsyWlVQvPX2xjeaQ217r2PtxDeqjlf66UYl6oY6AqNS8DH3iryCvIfCcybRZkc_hdy-6ZMoKT6Piijvk_aXdm7-QQqKJFHLuEqrVSOuBqqiNfVrG27QzAPuPOxvfXTVLXL2jek5meH6n-VWgrBdoMFH93QEszEDowDAEhQPHVs0xj7SIzA" url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" From 9d5e070620887bb768253f84074d1b9aaa160e23 Mon Sep 17 00:00:00 2001 From: Steven Pitts Date: Tue, 16 Feb 2021 11:24:02 -0500 Subject: [PATCH 05/10] Ignore URI in caching --- jwt/jwks_client.py | 12 ++++++------ tests/test_jwks_client.py | 18 ------------------ 2 files changed, 6 insertions(+), 24 deletions(-) diff --git a/jwt/jwks_client.py b/jwt/jwks_client.py index 5279fd25..46cb05e8 100644 --- a/jwt/jwks_client.py +++ b/jwt/jwks_client.py @@ -1,6 +1,6 @@ import json import urllib.request -from typing import Any, Dict, List, Tuple +from typing import Any, Dict, List from .api_jwk import PyJWK, PyJWKSet from .api_jwt import decode_complete as decode_token @@ -10,8 +10,8 @@ class PyJWKClient: def __init__(self, uri: str): self.uri = uri - # Map (uri, kid) to signing keys - self._known_signing_keys: Dict[Tuple[str, str], PyJWK] = {} + # Map kid to signing key + self._known_signing_keys: Dict[str, PyJWK] = {} def fetch_data(self) -> Any: with urllib.request.urlopen(self.uri) as response: @@ -35,13 +35,13 @@ def get_signing_keys(self) -> List[PyJWK]: return signing_keys def get_signing_key(self, kid: str) -> PyJWK: - if (self.uri, kid) in self._known_signing_keys: - return self._known_signing_keys[(self.uri, kid)] + if kid in self._known_signing_keys: + return self._known_signing_keys[kid] signing_keys = self.get_signing_keys() signing_key = None for key in signing_keys: - self._known_signing_keys[(self.uri, key.key_id)] = key + self._known_signing_keys[key.key_id] = key if key.key_id == kid: signing_key = key break diff --git a/tests/test_jwks_client.py b/tests/test_jwks_client.py index c5065b74..7640438c 100644 --- a/tests/test_jwks_client.py +++ b/tests/test_jwks_client.py @@ -99,24 +99,6 @@ def test_get_signing_key_caches_result(self): assert network_response.call_count == 1 - def test_get_signing_key_cache_distinguishes_uris(self): - url_1 = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" - url_2 = "https://dev-88evx9ru.auth0.com/.well-known/jwks.json" - kid = "NEE1QURBOTM4MzI5RkFDNTYxOTU1MDg2ODgwQ0UzMTk1QjYyRkRFQw" - - with mocked_response(RESPONSE_DATA): - jwks_client = PyJWKClient(url_1) - jwks_client.get_signing_key(kid) - - # mocked_response does not allow urllib.request.urlopen to be called twice - # so a second mock is needed - with mocked_response(RESPONSE_DATA): - jwks_client.uri = url_2 - jwks_client.get_signing_key(kid) - - for uri_kid_pair in ((url_1, kid), (url_2, kid)): - assert uri_kid_pair in jwks_client._known_signing_keys - def test_get_signing_key_from_jwt(self): token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik5FRTFRVVJCT1RNNE16STVSa0ZETlRZeE9UVTFNRGcyT0Rnd1EwVXpNVGsxUWpZeVJrUkZRdyJ9.eyJpc3MiOiJodHRwczovL2Rldi04N2V2eDlydS5hdXRoMC5jb20vIiwic3ViIjoiYVc0Q2NhNzl4UmVMV1V6MGFFMkg2a0QwTzNjWEJWdENAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vZXhwZW5zZXMtYXBpIiwiaWF0IjoxNTcyMDA2OTU0LCJleHAiOjE1NzIwMDY5NjQsImF6cCI6ImFXNENjYTc5eFJlTFdVejBhRTJINmtEME8zY1hCVnRDIiwiZ3R5IjoiY2xpZW50LWNyZWRlbnRpYWxzIn0.PUxE7xn52aTCohGiWoSdMBZGiYAHwE5FYie0Y1qUT68IHSTXwXVd6hn02HTah6epvHHVKA2FqcFZ4GGv5VTHEvYpeggiiZMgbxFrmTEY0csL6VNkX1eaJGcuehwQCRBKRLL3zKmA5IKGy5GeUnIbpPHLHDxr-GXvgFzsdsyWlVQvPX2xjeaQ217r2PtxDeqjlf66UYl6oY6AqNS8DH3iryCvIfCcybRZkc_hdy-6ZMoKT6Piijvk_aXdm7-QQqKJFHLuEqrVSOuBqqiNfVrG27QzAPuPOxvfXTVLXL2jek5meH6n-VWgrBdoMFH93QEszEDowDAEhQPHVs0xj7SIzA" url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" From 9aa263e0bec9d8f7ee5930351067402f256a2df7 Mon Sep 17 00:00:00 2001 From: Steven Pitts Date: Tue, 16 Feb 2021 11:38:52 -0500 Subject: [PATCH 06/10] Use functools.lru_cache to cache signing keys --- jwt/jwks_client.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/jwt/jwks_client.py b/jwt/jwks_client.py index 46cb05e8..001cec7b 100644 --- a/jwt/jwks_client.py +++ b/jwt/jwks_client.py @@ -1,6 +1,7 @@ import json import urllib.request -from typing import Any, Dict, List +from functools import lru_cache +from typing import Any, List from .api_jwk import PyJWK, PyJWKSet from .api_jwt import decode_complete as decode_token @@ -10,8 +11,9 @@ class PyJWKClient: def __init__(self, uri: str): self.uri = uri - # Map kid to signing key - self._known_signing_keys: Dict[str, PyJWK] = {} + # Cache signing keys + # Ignore mypy (https://github.com/python/mypy/issues/2427) + self.get_signing_key = lru_cache(maxsize=16)(self.get_signing_key) # type: ignore def fetch_data(self) -> Any: with urllib.request.urlopen(self.uri) as response: @@ -35,13 +37,10 @@ def get_signing_keys(self) -> List[PyJWK]: return signing_keys def get_signing_key(self, kid: str) -> PyJWK: - if kid in self._known_signing_keys: - return self._known_signing_keys[kid] signing_keys = self.get_signing_keys() signing_key = None for key in signing_keys: - self._known_signing_keys[key.key_id] = key if key.key_id == kid: signing_key = key break From 9579fe0d58178b5c23a46fbe00bc903c67fcdee7 Mon Sep 17 00:00:00 2001 From: Steven Pitts Date: Tue, 16 Feb 2021 11:53:21 -0500 Subject: [PATCH 07/10] Allow opting out of key caching --- jwt/jwks_client.py | 9 +++++---- tests/test_jwks_client.py | 27 ++++++++++++++++++++++++--- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/jwt/jwks_client.py b/jwt/jwks_client.py index 001cec7b..71442843 100644 --- a/jwt/jwks_client.py +++ b/jwt/jwks_client.py @@ -9,11 +9,12 @@ class PyJWKClient: - def __init__(self, uri: str): + def __init__(self, uri: str, cache_keys: bool = True): self.uri = uri - # Cache signing keys - # Ignore mypy (https://github.com/python/mypy/issues/2427) - self.get_signing_key = lru_cache(maxsize=16)(self.get_signing_key) # type: ignore + if cache_keys: + # Cache signing keys + # Ignore mypy (https://github.com/python/mypy/issues/2427) + self.get_signing_key = lru_cache(maxsize=16)(self.get_signing_key) # type: ignore def fetch_data(self) -> Any: with urllib.request.urlopen(self.uri) as response: diff --git a/tests/test_jwks_client.py b/tests/test_jwks_client.py index 7640438c..a512200c 100644 --- a/tests/test_jwks_client.py +++ b/tests/test_jwks_client.py @@ -92,12 +92,33 @@ def test_get_signing_key_caches_result(self): url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" kid = "NEE1QURBOTM4MzI5RkFDNTYxOTU1MDg2ODgwQ0UzMTk1QjYyRkRFQw" - with mocked_response(RESPONSE_DATA) as network_response: - jwks_client = PyJWKClient(url) + jwks_client = PyJWKClient(url) + + with mocked_response(RESPONSE_DATA): + jwks_client.get_signing_key(kid) + + # mocked_response does not allow urllib.request.urlopen to be called twice + # so a second mock is needed + with mocked_response(RESPONSE_DATA) as repeated_call: + jwks_client.get_signing_key(kid) + + assert repeated_call.call_count == 0 + + def test_get_signing_key_does_not_cache_opt_out(self): + url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" + kid = "NEE1QURBOTM4MzI5RkFDNTYxOTU1MDg2ODgwQ0UzMTk1QjYyRkRFQw" + + jwks_client = PyJWKClient(url, cache_keys=False) + + with mocked_response(RESPONSE_DATA): jwks_client.get_signing_key(kid) + + # mocked_response does not allow urllib.request.urlopen to be called twice + # so a second mock is needed + with mocked_response(RESPONSE_DATA) as repeated_call: jwks_client.get_signing_key(kid) - assert network_response.call_count == 1 + assert repeated_call.call_count == 1 def test_get_signing_key_from_jwt(self): token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik5FRTFRVVJCT1RNNE16STVSa0ZETlRZeE9UVTFNRGcyT0Rnd1EwVXpNVGsxUWpZeVJrUkZRdyJ9.eyJpc3MiOiJodHRwczovL2Rldi04N2V2eDlydS5hdXRoMC5jb20vIiwic3ViIjoiYVc0Q2NhNzl4UmVMV1V6MGFFMkg2a0QwTzNjWEJWdENAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vZXhwZW5zZXMtYXBpIiwiaWF0IjoxNTcyMDA2OTU0LCJleHAiOjE1NzIwMDY5NjQsImF6cCI6ImFXNENjYTc5eFJlTFdVejBhRTJINmtEME8zY1hCVnRDIiwiZ3R5IjoiY2xpZW50LWNyZWRlbnRpYWxzIn0.PUxE7xn52aTCohGiWoSdMBZGiYAHwE5FYie0Y1qUT68IHSTXwXVd6hn02HTah6epvHHVKA2FqcFZ4GGv5VTHEvYpeggiiZMgbxFrmTEY0csL6VNkX1eaJGcuehwQCRBKRLL3zKmA5IKGy5GeUnIbpPHLHDxr-GXvgFzsdsyWlVQvPX2xjeaQ217r2PtxDeqjlf66UYl6oY6AqNS8DH3iryCvIfCcybRZkc_hdy-6ZMoKT6Piijvk_aXdm7-QQqKJFHLuEqrVSOuBqqiNfVrG27QzAPuPOxvfXTVLXL2jek5meH6n-VWgrBdoMFH93QEszEDowDAEhQPHVs0xj7SIzA" From 46e9ea18c10e7312155e16457f098328830063cc Mon Sep 17 00:00:00 2001 From: Steven Pitts Date: Thu, 18 Feb 2021 18:01:48 -0500 Subject: [PATCH 08/10] Allow adjusting max cached keys --- jwt/jwks_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jwt/jwks_client.py b/jwt/jwks_client.py index 71442843..dc86c3be 100644 --- a/jwt/jwks_client.py +++ b/jwt/jwks_client.py @@ -9,12 +9,12 @@ class PyJWKClient: - def __init__(self, uri: str, cache_keys: bool = True): + def __init__(self, uri: str, cache_keys: bool = True, max_cached_keys: int = 16): self.uri = uri if cache_keys: # Cache signing keys # Ignore mypy (https://github.com/python/mypy/issues/2427) - self.get_signing_key = lru_cache(maxsize=16)(self.get_signing_key) # type: ignore + self.get_signing_key = lru_cache(maxsize=max_cached_keys)(self.get_signing_key) # type: ignore def fetch_data(self) -> Any: with urllib.request.urlopen(self.uri) as response: From 414b317dec6e3a0fe43d8d8909e2a6eac6956b83 Mon Sep 17 00:00:00 2001 From: Steven Pitts Date: Mon, 22 Feb 2021 09:37:31 -0500 Subject: [PATCH 09/10] Add #611 change to CHANGELOG.rst --- CHANGELOG.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4712e2e5..0b8b370e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,8 @@ Fixed Added ~~~~~ +- Add caching `#611 `__ + `v2.0.1 `__ -------------------------------------------------------------------- From 8166ac9f9b9251baeccfe071c289c80ce4fdd23e Mon Sep 17 00:00:00 2001 From: Steven Pitts <25968054+makusu2@users.noreply.github.com> Date: Mon, 22 Feb 2021 23:21:44 -0500 Subject: [PATCH 10/10] Update CHANGELOG.rst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Padilla --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0b8b370e..b9588804 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,7 +16,7 @@ Fixed Added ~~~~~ -- Add caching `#611 `__ +- Add caching by default to PyJWKClient `#611 `__ `v2.0.1 `__ --------------------------------------------------------------------