prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  OpenIdMetadata signing keys should refresh every 24 hours, and once per hour if a key is missing
  ## Describe the bug
  Every 24 hours, and when a cert is cycled (a key is not found in the cache), the OpenIdMetadata cache within the sdk should gracefully refresh.  
  
  javascript implementation reference:
  https://github.com/microsoft/botbuilder-js/pull/2466
  
  python code to change:
  https://github.com/microsoft/botbuilder-python/blob/master/libraries/botframework-connector/botframework/connector/auth/jwt_token_extractor.py#L129
  
  [bug]
  
  
  --- END ISSUE ---
  
  Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
  --- BEGIN FILES ---
  Path: `libraries/botframework-connector/botframework/connector/auth/jwt_token_extractor.py`
  Content:
  ```
  1 # Copyright (c) Microsoft Corporation. All rights reserved.
  2 # Licensed under the MIT License.
  3 
  4 import json
  5 from datetime import datetime, timedelta
  6 from typing import List
  7 import requests
  8 from jwt.algorithms import RSAAlgorithm
  9 import jwt
  10 from .claims_identity import ClaimsIdentity
  11 from .verify_options import VerifyOptions
  12 from .endorsements_validator import EndorsementsValidator
  13 
  14 
  15 class JwtTokenExtractor:
  16     metadataCache = {}
  17 
  18     def __init__(
  19         self,
  20         validation_params: VerifyOptions,
  21         metadata_url: str,
  22         allowed_algorithms: list,
  23     ):
  24         self.validation_parameters = validation_params
  25         self.validation_parameters.algorithms = allowed_algorithms
  26         self.open_id_metadata = JwtTokenExtractor.get_open_id_metadata(metadata_url)
  27 
  28     @staticmethod
  29     def get_open_id_metadata(metadata_url: str):
  30         metadata = JwtTokenExtractor.metadataCache.get(metadata_url, None)
  31         if metadata is None:
  32             metadata = _OpenIdMetadata(metadata_url)
  33             JwtTokenExtractor.metadataCache.setdefault(metadata_url, metadata)
  34         return metadata
  35 
  36     async def get_identity_from_auth_header(
  37         self, auth_header: str, channel_id: str, required_endorsements: List[str] = None
  38     ) -> ClaimsIdentity:
  39         if not auth_header:
  40             return None
  41         parts = auth_header.split(" ")
  42         if len(parts) == 2:
  43             return await self.get_identity(
  44                 parts[0], parts[1], channel_id, required_endorsements
  45             )
  46         return None
  47 
  48     async def get_identity(
  49         self,
  50         schema: str,
  51         parameter: str,
  52         channel_id: str,
  53         required_endorsements: List[str] = None,
  54     ) -> ClaimsIdentity:
  55         # No header in correct scheme or no token
  56         if schema != "Bearer" or not parameter:
  57             return None
  58 
  59         # Issuer isn't allowed? No need to check signature
  60         if not self._has_allowed_issuer(parameter):
  61             return None
  62 
  63         try:
  64             return await self._validate_token(
  65                 parameter, channel_id, required_endorsements
  66             )
  67         except Exception as error:
  68             raise error
  69 
  70     def _has_allowed_issuer(self, jwt_token: str) -> bool:
  71         decoded = jwt.decode(jwt_token, verify=False)
  72         issuer = decoded.get("iss", None)
  73         if issuer in self.validation_parameters.issuer:
  74             return True
  75 
  76         return issuer == self.validation_parameters.issuer
  77 
  78     async def _validate_token(
  79         self, jwt_token: str, channel_id: str, required_endorsements: List[str] = None
  80     ) -> ClaimsIdentity:
  81         required_endorsements = required_endorsements or []
  82         headers = jwt.get_unverified_header(jwt_token)
  83 
  84         # Update the signing tokens from the last refresh
  85         key_id = headers.get("kid", None)
  86         metadata = await self.open_id_metadata.get(key_id)
  87 
  88         if key_id and metadata.endorsements:
  89             # Verify that channelId is included in endorsements
  90             if not EndorsementsValidator.validate(channel_id, metadata.endorsements):
  91                 raise Exception("Could not validate endorsement key")
  92 
  93             # Verify that additional endorsements are satisfied.
  94             # If no additional endorsements are expected, the requirement is satisfied as well
  95             for endorsement in required_endorsements:
  96                 if not EndorsementsValidator.validate(
  97                     endorsement, metadata.endorsements
  98                 ):
  99                     raise Exception("Could not validate endorsement key")
  100 
  101         if headers.get("alg", None) not in self.validation_parameters.algorithms:
  102             raise Exception("Token signing algorithm not in allowed list")
  103 
  104         options = {
  105             "verify_aud": False,
  106             "verify_exp": not self.validation_parameters.ignore_expiration,
  107         }
  108 
  109         decoded_payload = jwt.decode(
  110             jwt_token,
  111             metadata.public_key,
  112             leeway=self.validation_parameters.clock_tolerance,
  113             options=options,
  114         )
  115 
  116         claims = ClaimsIdentity(decoded_payload, True)
  117 
  118         return claims
  119 
  120 
  121 class _OpenIdMetadata:
  122     def __init__(self, url):
  123         self.url = url
  124         self.keys = []
  125         self.last_updated = datetime.min
  126 
  127     async def get(self, key_id: str):
  128         # If keys are more than 5 days old, refresh them
  129         if self.last_updated < (datetime.now() - timedelta(days=5)):
  130             await self._refresh()
  131         return self._find(key_id)
  132 
  133     async def _refresh(self):
  134         response = requests.get(self.url)
  135         response.raise_for_status()
  136         keys_url = response.json()["jwks_uri"]
  137         response_keys = requests.get(keys_url)
  138         response_keys.raise_for_status()
  139         self.last_updated = datetime.now()
  140         self.keys = response_keys.json()["keys"]
  141 
  142     def _find(self, key_id: str):
  143         if not self.keys:
  144             return None
  145         key = [x for x in self.keys if x["kid"] == key_id][0]
  146         public_key = RSAAlgorithm.from_jwk(json.dumps(key))
  147         endorsements = key.get("endorsements", [])
  148         return _OpenIdConfig(public_key, endorsements)
  149 
  150 
  151 class _OpenIdConfig:
  152     def __init__(self, public_key, endorsements):
  153         self.public_key = public_key
  154         self.endorsements = endorsements
  155
  ```
  --- END FILES ---
  
  Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.
  
  Here is an example:
  
  ```diff
  diff --git a/examples/server_async.py b/examples/server_async.py
  --- a/examples/server_async.py
  +++ b/examples/server_async.py
  @@ -313,4 +313,4 @@
   
   
   if __name__ == "__main__":
  -    asyncio.run(run_async_server("."), debug=True)
  +    asyncio.run(run_async_server(), debug=True)
  diff --git a/examples/server_sync.py b/examples/server_sync.py
  --- a/examples/server_sync.py
  +++ b/examples/server_sync.py
  @@ -313,5 +313,5 @@
   
   
   if __name__ == "__main__":
  -    server = run_sync_server(".")
  +    server = run_sync_server()
       server.shutdown()
  
  ```
  
golden_diff: diff --git a/libraries/botframework-connector/botframework/connector/auth/jwt_token_extractor.py b/libraries/botframework-connector/botframework/connector/auth/jwt_token_extractor.py
  --- a/libraries/botframework-connector/botframework/connector/auth/jwt_token_extractor.py
  +++ b/libraries/botframework-connector/botframework/connector/auth/jwt_token_extractor.py
  @@ -125,10 +125,16 @@
           self.last_updated = datetime.min
   
       async def get(self, key_id: str):
  -        # If keys are more than 5 days old, refresh them
  -        if self.last_updated < (datetime.now() - timedelta(days=5)):
  +        # If keys are more than 1 day old, refresh them
  +        if self.last_updated < (datetime.now() - timedelta(days=1)):
               await self._refresh()
  -        return self._find(key_id)
  +
  +        key = self._find(key_id)
  +        if not key and self.last_updated < (datetime.now() - timedelta(hours=1)):
  +            # Refresh the cache if a key is not found (max once per hour)
  +            await self._refresh()
  +            key = self._find(key_id)
  +        return key
   
       async def _refresh(self):
           response = requests.get(self.url)
  
verification_info: {"golden_diff": "diff --git a/libraries/botframework-connector/botframework/connector/auth/jwt_token_extractor.py b/libraries/botframework-connector/botframework/connector/auth/jwt_token_extractor.py\n--- a/libraries/botframework-connector/botframework/connector/auth/jwt_token_extractor.py\n+++ b/libraries/botframework-connector/botframework/connector/auth/jwt_token_extractor.py\n@@ -125,10 +125,16 @@\n         self.last_updated = datetime.min\n \n     async def get(self, key_id: str):\n-        # If keys are more than 5 days old, refresh them\n-        if self.last_updated < (datetime.now() - timedelta(days=5)):\n+        # If keys are more than 1 day old, refresh them\n+        if self.last_updated < (datetime.now() - timedelta(days=1)):\n             await self._refresh()\n-        return self._find(key_id)\n+\n+        key = self._find(key_id)\n+        if not key and self.last_updated < (datetime.now() - timedelta(hours=1)):\n+            # Refresh the cache if a key is not found (max once per hour)\n+            await self._refresh()\n+            key = self._find(key_id)\n+        return key\n \n     async def _refresh(self):\n         response = requests.get(self.url)\n", "issue": "OpenIdMetadata signing keys should refresh every 24 hours, and once per hour if a key is missing\n## Describe the bug\r\nEvery 24 hours, and when a cert is cycled (a key is not found in the cache), the OpenIdMetadata cache within the sdk should gracefully refresh.  \r\n\r\njavascript implementation reference:\r\nhttps://github.com/microsoft/botbuilder-js/pull/2466\r\n\r\npython code to change:\r\nhttps://github.com/microsoft/botbuilder-python/blob/master/libraries/botframework-connector/botframework/connector/auth/jwt_token_extractor.py#L129\r\n\r\n[bug]\r\n\n", "before_files": [{"content": "# Copyright (c) Microsoft Corporation. All rights reserved.\n# Licensed under the MIT License.\n\nimport json\nfrom datetime import datetime, timedelta\nfrom typing import List\nimport requests\nfrom jwt.algorithms import RSAAlgorithm\nimport jwt\nfrom .claims_identity import ClaimsIdentity\nfrom .verify_options import VerifyOptions\nfrom .endorsements_validator import EndorsementsValidator\n\n\nclass JwtTokenExtractor:\n    metadataCache = {}\n\n    def __init__(\n        self,\n        validation_params: VerifyOptions,\n        metadata_url: str,\n        allowed_algorithms: list,\n    ):\n        self.validation_parameters = validation_params\n        self.validation_parameters.algorithms = allowed_algorithms\n        self.open_id_metadata = JwtTokenExtractor.get_open_id_metadata(metadata_url)\n\n    @staticmethod\n    def get_open_id_metadata(metadata_url: str):\n        metadata = JwtTokenExtractor.metadataCache.get(metadata_url, None)\n        if metadata is None:\n            metadata = _OpenIdMetadata(metadata_url)\n            JwtTokenExtractor.metadataCache.setdefault(metadata_url, metadata)\n        return metadata\n\n    async def get_identity_from_auth_header(\n        self, auth_header: str, channel_id: str, required_endorsements: List[str] = None\n    ) -> ClaimsIdentity:\n        if not auth_header:\n            return None\n        parts = auth_header.split(\" \")\n        if len(parts) == 2:\n            return await self.get_identity(\n                parts[0], parts[1], channel_id, required_endorsements\n            )\n        return None\n\n    async def get_identity(\n        self,\n        schema: str,\n        parameter: str,\n        channel_id: str,\n        required_endorsements: List[str] = None,\n    ) -> ClaimsIdentity:\n        # No header in correct scheme or no token\n        if schema != \"Bearer\" or not parameter:\n            return None\n\n        # Issuer isn't allowed? No need to check signature\n        if not self._has_allowed_issuer(parameter):\n            return None\n\n        try:\n            return await self._validate_token(\n                parameter, channel_id, required_endorsements\n            )\n        except Exception as error:\n            raise error\n\n    def _has_allowed_issuer(self, jwt_token: str) -> bool:\n        decoded = jwt.decode(jwt_token, verify=False)\n        issuer = decoded.get(\"iss\", None)\n        if issuer in self.validation_parameters.issuer:\n            return True\n\n        return issuer == self.validation_parameters.issuer\n\n    async def _validate_token(\n        self, jwt_token: str, channel_id: str, required_endorsements: List[str] = None\n    ) -> ClaimsIdentity:\n        required_endorsements = required_endorsements or []\n        headers = jwt.get_unverified_header(jwt_token)\n\n        # Update the signing tokens from the last refresh\n        key_id = headers.get(\"kid\", None)\n        metadata = await self.open_id_metadata.get(key_id)\n\n        if key_id and metadata.endorsements:\n            # Verify that channelId is included in endorsements\n            if not EndorsementsValidator.validate(channel_id, metadata.endorsements):\n                raise Exception(\"Could not validate endorsement key\")\n\n            # Verify that additional endorsements are satisfied.\n            # If no additional endorsements are expected, the requirement is satisfied as well\n            for endorsement in required_endorsements:\n                if not EndorsementsValidator.validate(\n                    endorsement, metadata.endorsements\n                ):\n                    raise Exception(\"Could not validate endorsement key\")\n\n        if headers.get(\"alg\", None) not in self.validation_parameters.algorithms:\n            raise Exception(\"Token signing algorithm not in allowed list\")\n\n        options = {\n            \"verify_aud\": False,\n            \"verify_exp\": not self.validation_parameters.ignore_expiration,\n        }\n\n        decoded_payload = jwt.decode(\n            jwt_token,\n            metadata.public_key,\n            leeway=self.validation_parameters.clock_tolerance,\n            options=options,\n        )\n\n        claims = ClaimsIdentity(decoded_payload, True)\n\n        return claims\n\n\nclass _OpenIdMetadata:\n    def __init__(self, url):\n        self.url = url\n        self.keys = []\n        self.last_updated = datetime.min\n\n    async def get(self, key_id: str):\n        # If keys are more than 5 days old, refresh them\n        if self.last_updated < (datetime.now() - timedelta(days=5)):\n            await self._refresh()\n        return self._find(key_id)\n\n    async def _refresh(self):\n        response = requests.get(self.url)\n        response.raise_for_status()\n        keys_url = response.json()[\"jwks_uri\"]\n        response_keys = requests.get(keys_url)\n        response_keys.raise_for_status()\n        self.last_updated = datetime.now()\n        self.keys = response_keys.json()[\"keys\"]\n\n    def _find(self, key_id: str):\n        if not self.keys:\n            return None\n        key = [x for x in self.keys if x[\"kid\"] == key_id][0]\n        public_key = RSAAlgorithm.from_jwk(json.dumps(key))\n        endorsements = key.get(\"endorsements\", [])\n        return _OpenIdConfig(public_key, endorsements)\n\n\nclass _OpenIdConfig:\n    def __init__(self, public_key, endorsements):\n        self.public_key = public_key\n        self.endorsements = endorsements\n", "path": "libraries/botframework-connector/botframework/connector/auth/jwt_token_extractor.py"}], "after_files": [{"content": "# Copyright (c) Microsoft Corporation. All rights reserved.\n# Licensed under the MIT License.\n\nimport json\nfrom datetime import datetime, timedelta\nfrom typing import List\nimport requests\nfrom jwt.algorithms import RSAAlgorithm\nimport jwt\nfrom .claims_identity import ClaimsIdentity\nfrom .verify_options import VerifyOptions\nfrom .endorsements_validator import EndorsementsValidator\n\n\nclass JwtTokenExtractor:\n    metadataCache = {}\n\n    def __init__(\n        self,\n        validation_params: VerifyOptions,\n        metadata_url: str,\n        allowed_algorithms: list,\n    ):\n        self.validation_parameters = validation_params\n        self.validation_parameters.algorithms = allowed_algorithms\n        self.open_id_metadata = JwtTokenExtractor.get_open_id_metadata(metadata_url)\n\n    @staticmethod\n    def get_open_id_metadata(metadata_url: str):\n        metadata = JwtTokenExtractor.metadataCache.get(metadata_url, None)\n        if metadata is None:\n            metadata = _OpenIdMetadata(metadata_url)\n            JwtTokenExtractor.metadataCache.setdefault(metadata_url, metadata)\n        return metadata\n\n    async def get_identity_from_auth_header(\n        self, auth_header: str, channel_id: str, required_endorsements: List[str] = None\n    ) -> ClaimsIdentity:\n        if not auth_header:\n            return None\n        parts = auth_header.split(\" \")\n        if len(parts) == 2:\n            return await self.get_identity(\n                parts[0], parts[1], channel_id, required_endorsements\n            )\n        return None\n\n    async def get_identity(\n        self,\n        schema: str,\n        parameter: str,\n        channel_id: str,\n        required_endorsements: List[str] = None,\n    ) -> ClaimsIdentity:\n        # No header in correct scheme or no token\n        if schema != \"Bearer\" or not parameter:\n            return None\n\n        # Issuer isn't allowed? No need to check signature\n        if not self._has_allowed_issuer(parameter):\n            return None\n\n        try:\n            return await self._validate_token(\n                parameter, channel_id, required_endorsements\n            )\n        except Exception as error:\n            raise error\n\n    def _has_allowed_issuer(self, jwt_token: str) -> bool:\n        decoded = jwt.decode(jwt_token, verify=False)\n        issuer = decoded.get(\"iss\", None)\n        if issuer in self.validation_parameters.issuer:\n            return True\n\n        return issuer == self.validation_parameters.issuer\n\n    async def _validate_token(\n        self, jwt_token: str, channel_id: str, required_endorsements: List[str] = None\n    ) -> ClaimsIdentity:\n        required_endorsements = required_endorsements or []\n        headers = jwt.get_unverified_header(jwt_token)\n\n        # Update the signing tokens from the last refresh\n        key_id = headers.get(\"kid\", None)\n        metadata = await self.open_id_metadata.get(key_id)\n\n        if key_id and metadata.endorsements:\n            # Verify that channelId is included in endorsements\n            if not EndorsementsValidator.validate(channel_id, metadata.endorsements):\n                raise Exception(\"Could not validate endorsement key\")\n\n            # Verify that additional endorsements are satisfied.\n            # If no additional endorsements are expected, the requirement is satisfied as well\n            for endorsement in required_endorsements:\n                if not EndorsementsValidator.validate(\n                    endorsement, metadata.endorsements\n                ):\n                    raise Exception(\"Could not validate endorsement key\")\n\n        if headers.get(\"alg\", None) not in self.validation_parameters.algorithms:\n            raise Exception(\"Token signing algorithm not in allowed list\")\n\n        options = {\n            \"verify_aud\": False,\n            \"verify_exp\": not self.validation_parameters.ignore_expiration,\n        }\n\n        decoded_payload = jwt.decode(\n            jwt_token,\n            metadata.public_key,\n            leeway=self.validation_parameters.clock_tolerance,\n            options=options,\n        )\n\n        claims = ClaimsIdentity(decoded_payload, True)\n\n        return claims\n\n\nclass _OpenIdMetadata:\n    def __init__(self, url):\n        self.url = url\n        self.keys = []\n        self.last_updated = datetime.min\n\n    async def get(self, key_id: str):\n        # If keys are more than 1 day old, refresh them\n        if self.last_updated < (datetime.now() - timedelta(days=1)):\n            await self._refresh()\n\n        key = self._find(key_id)\n        if not key and self.last_updated < (datetime.now() - timedelta(hours=1)):\n            # Refresh the cache if a key is not found (max once per hour)\n            await self._refresh()\n            key = self._find(key_id)\n        return key\n\n    async def _refresh(self):\n        response = requests.get(self.url)\n        response.raise_for_status()\n        keys_url = response.json()[\"jwks_uri\"]\n        response_keys = requests.get(keys_url)\n        response_keys.raise_for_status()\n        self.last_updated = datetime.now()\n        self.keys = response_keys.json()[\"keys\"]\n\n    def _find(self, key_id: str):\n        if not self.keys:\n            return None\n        key = [x for x in self.keys if x[\"kid\"] == key_id][0]\n        public_key = RSAAlgorithm.from_jwk(json.dumps(key))\n        endorsements = key.get(\"endorsements\", [])\n        return _OpenIdConfig(public_key, endorsements)\n\n\nclass _OpenIdConfig:\n    def __init__(self, public_key, endorsements):\n        self.public_key = public_key\n        self.endorsements = endorsements\n", "path": "libraries/botframework-connector/botframework/connector/auth/jwt_token_extractor.py"}]}