Skip to content

Commit abacb4d

Browse files
committed
add optional error holddown for KeyBundle
1 parent 8284906 commit abacb4d

File tree

2 files changed

+60
-3
lines changed

2 files changed

+60
-3
lines changed

src/cryptojwt/key_bundle.py

Lines changed: 19 additions & 3 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+
error_holddown=0,
159161
fileformat="jwks",
160162
keytype="RSA",
161163
keyusage=None,
@@ -188,6 +190,7 @@ def __init__(
188190
self.remote = False
189191
self.local = False
190192
self.cache_time = cache_time
193+
self.error_holddown = error_holddown
191194
self.time_out = 0
192195
self.etag = ""
193196
self.source = None
@@ -198,6 +201,7 @@ def __init__(
198201
self.last_updated = 0
199202
self.last_remote = None # HTTP Date of last remote update
200203
self.last_local = None # UNIX timestamp of last local update
204+
self.last_error = None # UNIX timestamp of last error
201205

202206
if httpc:
203207
self.httpc = httpc
@@ -365,6 +369,16 @@ def do_remote(self):
365369
# if self.verify_ssl is not None:
366370
# self.httpc_params["verify"] = self.verify_ssl
367371

372+
if self.last_error:
373+
t = self.last_error + self.error_holddown
374+
if time.time() < t:
375+
LOGGER.warning(
376+
"Not reading remote JWKS from %s (in error holddown until %s)",
377+
self.source,
378+
datetime.fromtimestamp(t),
379+
)
380+
return False
381+
368382
LOGGER.info("Reading remote JWKS from %s", self.source)
369383
try:
370384
LOGGER.debug("KeyBundle fetch keys from: %s", self.source)
@@ -390,6 +404,7 @@ def do_remote(self):
390404
self.do_keys(self.imp_jwks["keys"])
391405
except KeyError:
392406
LOGGER.error("No 'keys' keyword in JWKS")
407+
self.last_error = time.time()
393408
raise UpdateFailed(MALFORMED.format(self.source))
394409

395410
if hasattr(_http_resp, "headers"):
@@ -402,12 +417,13 @@ def do_remote(self):
402417

403418
else:
404419
LOGGER.warning(
405-
"HTTP status %d reading remote JWKS from %s",
406-
_http_resp.status_code,
407-
self.source,
420+
"HTTP status %d reading remote JWKS from %s", _http_resp.status_code, self.source,
408421
)
422+
self.last_error = time.time()
409423
raise UpdateFailed(REMOTE_FAILED.format(self.source, _http_resp.status_code))
424+
410425
self.last_updated = time.time()
426+
self.last_error = 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_error_holddown():
1031+
source_good = "https://example.com/keys.json"
1032+
source_bad = "https://example.com/keys-bad.json"
1033+
error_holddown = 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+
error_holddown=error_holddown,
1044+
)
1045+
res = kb.do_remote()
1046+
assert res == True
1047+
assert kb.last_error 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.last_error is not None
1059+
assert res == False
1060+
1061+
# wait until holddown
1062+
time.sleep(error_holddown + 1)
1063+
1064+
# try again
1065+
kb.source = source_good
1066+
res = kb.do_remote()
1067+
assert res == True

0 commit comments

Comments
 (0)