Skip to content

Ignore KeyBundle errors for a specified duration #67

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Sep 10, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/cryptojwt/key_bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import logging
import os
import time
from datetime import datetime
from functools import cmp_to_key

import requests
Expand Down Expand Up @@ -156,6 +157,7 @@ def __init__(
keys=None,
source="",
cache_time=300,
error_holddown=0,
fileformat="jwks",
keytype="RSA",
keyusage=None,
Expand Down Expand Up @@ -188,6 +190,7 @@ def __init__(
self.remote = False
self.local = False
self.cache_time = cache_time
self.error_holddown = error_holddown
self.time_out = 0
self.etag = ""
self.source = None
Expand All @@ -198,6 +201,7 @@ def __init__(
self.last_updated = 0
self.last_remote = None # HTTP Date of last remote update
self.last_local = None # UNIX timestamp of last local update
self.last_error = None # UNIX timestamp of last error

if httpc:
self.httpc = httpc
Expand Down Expand Up @@ -365,6 +369,16 @@ def do_remote(self):
# if self.verify_ssl is not None:
# self.httpc_params["verify"] = self.verify_ssl

if self.last_error:
t = self.last_error + self.error_holddown
if time.time() < t:
LOGGER.warning(
"Not reading remote JWKS from %s (in error holddown until %s)",
self.source,
datetime.fromtimestamp(t),
)
return False

LOGGER.info("Reading remote JWKS from %s", self.source)
try:
LOGGER.debug("KeyBundle fetch keys from: %s", self.source)
Expand All @@ -390,6 +404,7 @@ def do_remote(self):
self.do_keys(self.imp_jwks["keys"])
except KeyError:
LOGGER.error("No 'keys' keyword in JWKS")
self.last_error = time.time()
raise UpdateFailed(MALFORMED.format(self.source))

if hasattr(_http_resp, "headers"):
Expand All @@ -406,8 +421,11 @@ def do_remote(self):
_http_resp.status_code,
self.source,
)
self.last_error = time.time()
raise UpdateFailed(REMOTE_FAILED.format(self.source, _http_resp.status_code))

self.last_updated = time.time()
self.last_error = None
return True

def _parse_remote_response(self, response):
Expand Down
41 changes: 41 additions & 0 deletions tests/test_03_key_bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from cryptojwt.jwk.rsa import import_rsa_key_from_cert_file
from cryptojwt.jwk.rsa import new_rsa_key
from cryptojwt.key_bundle import KeyBundle
from cryptojwt.key_bundle import UpdateFailed
from cryptojwt.key_bundle import build_key_bundle
from cryptojwt.key_bundle import dump_jwks
from cryptojwt.key_bundle import init_key
Expand Down Expand Up @@ -1024,3 +1025,43 @@ def test_remote_not_modified():
assert kb2.httpc_params == {"timeout": (2, 2)}
assert kb2.imp_jwks
assert kb2.last_updated


def test_error_holddown():
source_good = "https://example.com/keys.json"
source_bad = "https://example.com/keys-bad.json"
error_holddown = 1
# Mock response
with responses.RequestsMock() as rsps:
rsps.add(method="GET", url=source_good, json=JWKS_DICT, status=200)
rsps.add(method="GET", url=source_bad, json=JWKS_DICT, status=500)
httpc_params = {"timeout": (2, 2)} # connect, read timeouts in seconds
kb = KeyBundle(
source=source_good,
httpc=requests.request,
httpc_params=httpc_params,
error_holddown=error_holddown,
)
res = kb.do_remote()
assert res == True
assert kb.last_error is None

# refetch, but fail by using a bad source
kb.source = source_bad
try:
res = kb.do_remote()
except UpdateFailed:
pass

# retry should fail silently as we're in holddown
res = kb.do_remote()
assert kb.last_error is not None
assert res == False

# wait until holddown
time.sleep(error_holddown + 1)

# try again
kb.source = source_good
res = kb.do_remote()
assert res == True