Skip to content

Commit 2b00abb

Browse files
Merge pull request #67 from jschlyter/key_bundle_backoff
Ignore KeyBundle errors for a specified duration When fetching remote keys, KeyBundle will retry every time even if errors had recently occurred. This might not be desirable for many applications, as errors like this usually fix themselves within a short amount of time. Until then, additional requests would result in more load, latency, excess logging and key source spamming. The PR adds an `ignore_errors_period` parameter to the KeyBundle object, that is used to specify the amount of time (in seconds) during which fetching is skipped. Default value is `0` (zero), effectively disabling this functionality and resulting in retries every time the bundle is accessed. For most applications, a hold down timer of 60 seconds or more is probably reasonable.
2 parents 8284906 + 09947d4 commit 2b00abb

File tree

2 files changed

+57
-0
lines changed

2 files changed

+57
-0
lines changed

src/cryptojwt/key_bundle.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import logging
55
import os
66
import time
7+
from datetime import datetime
78
from functools import cmp_to_key
89

910
import requests
@@ -156,6 +157,7 @@ def __init__(
156157
keys=None,
157158
source="",
158159
cache_time=300,
160+
ignore_errors_period=0,
159161
fileformat="jwks",
160162
keytype="RSA",
161163
keyusage=None,
@@ -188,6 +190,8 @@ def __init__(
188190
self.remote = False
189191
self.local = False
190192
self.cache_time = cache_time
193+
self.ignore_errors_period = ignore_errors_period
194+
self.ignore_errors_until = None # UNIX timestamp of last error
191195
self.time_out = 0
192196
self.etag = ""
193197
self.source = None
@@ -365,6 +369,14 @@ def do_remote(self):
365369
# if self.verify_ssl is not None:
366370
# self.httpc_params["verify"] = self.verify_ssl
367371

372+
if self.ignore_errors_until and time.time() < self.ignore_errors_until:
373+
LOGGER.warning(
374+
"Not reading remote JWKS from %s (in error holddown until %s)",
375+
self.source,
376+
datetime.fromtimestamp(self.ignore_errors_until),
377+
)
378+
return False
379+
368380
LOGGER.info("Reading remote JWKS from %s", self.source)
369381
try:
370382
LOGGER.debug("KeyBundle fetch keys from: %s", self.source)
@@ -390,6 +402,7 @@ def do_remote(self):
390402
self.do_keys(self.imp_jwks["keys"])
391403
except KeyError:
392404
LOGGER.error("No 'keys' keyword in JWKS")
405+
self.ignore_errors_until = time.time() + self.ignore_errors_period
393406
raise UpdateFailed(MALFORMED.format(self.source))
394407

395408
if hasattr(_http_resp, "headers"):
@@ -406,8 +419,11 @@ def do_remote(self):
406419
_http_resp.status_code,
407420
self.source,
408421
)
422+
self.ignore_errors_until = time.time() + self.ignore_errors_period
409423
raise UpdateFailed(REMOTE_FAILED.format(self.source, _http_resp.status_code))
424+
410425
self.last_updated = time.time()
426+
self.ignore_errors_until = None
411427
return True
412428

413429
def _parse_remote_response(self, response):

tests/test_03_key_bundle.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from cryptojwt.jwk.rsa import import_rsa_key_from_cert_file
1818
from cryptojwt.jwk.rsa import new_rsa_key
1919
from cryptojwt.key_bundle import KeyBundle
20+
from cryptojwt.key_bundle import UpdateFailed
2021
from cryptojwt.key_bundle import build_key_bundle
2122
from cryptojwt.key_bundle import dump_jwks
2223
from cryptojwt.key_bundle import init_key
@@ -1024,3 +1025,43 @@ def test_remote_not_modified():
10241025
assert kb2.httpc_params == {"timeout": (2, 2)}
10251026
assert kb2.imp_jwks
10261027
assert kb2.last_updated
1028+
1029+
1030+
def test_ignore_errors_period():
1031+
source_good = "https://example.com/keys.json"
1032+
source_bad = "https://example.com/keys-bad.json"
1033+
ignore_errors_period = 1
1034+
# Mock response
1035+
with responses.RequestsMock() as rsps:
1036+
rsps.add(method="GET", url=source_good, json=JWKS_DICT, status=200)
1037+
rsps.add(method="GET", url=source_bad, json=JWKS_DICT, status=500)
1038+
httpc_params = {"timeout": (2, 2)} # connect, read timeouts in seconds
1039+
kb = KeyBundle(
1040+
source=source_good,
1041+
httpc=requests.request,
1042+
httpc_params=httpc_params,
1043+
ignore_errors_period=ignore_errors_period,
1044+
)
1045+
res = kb.do_remote()
1046+
assert res == True
1047+
assert kb.ignore_errors_until is None
1048+
1049+
# refetch, but fail by using a bad source
1050+
kb.source = source_bad
1051+
try:
1052+
res = kb.do_remote()
1053+
except UpdateFailed:
1054+
pass
1055+
1056+
# retry should fail silently as we're in holddown
1057+
res = kb.do_remote()
1058+
assert kb.ignore_errors_until is not None
1059+
assert res == False
1060+
1061+
# wait until holddown
1062+
time.sleep(ignore_errors_period + 1)
1063+
1064+
# try again
1065+
kb.source = source_good
1066+
res = kb.do_remote()
1067+
assert res == True

0 commit comments

Comments
 (0)