Repository: jpadilla/pyjwt

Issue:
very unhelpfull / missleading error messages on get_signing_key_from_jwt
Hello,

I spend a few hours today for a bug that shouldn't have been there. I set up my python application where I recieve a jwt token from oauth-proxy, validate, decode and echo the contents.
Running locally, everything went perfect. Running in a container I got the message "The JWK Set did not contain any usable keys". I checked, oauth configs, credentials, my code, put a few hours into trying to debug the python code while running in podman.

Then after a few hours my colleague helped and ran the project on his machine. He got the same error.
So what does his machine has not, that mine has? Dependencies. 

After another hour of debuging, we found out, that my jwt token is encoded with RSA. For pyjwt to decode the key we need to use RSA.  To use RSA the cryptography is nessecary. I got this installed in my global dependencies and forgot to use an env.

The thing is:
Alls this could have been prevented, if the raised error message would be "found RSA - cannot decode because dependency cryptography is missing"

Instead the error handling is as follows:

*api_jwk.py* - PyJWKSet:
```
class PyJWKSet:
    def __init__(self, keys: list[dict]) -> None:
        self.keys = []

        if not keys:
            raise PyJWKSetError("The JWK Set did not contain any keys")

        if not isinstance(keys, list):
            raise PyJWKSetError("Invalid JWK Set value")

        for key in keys:
            try:
                *self.keys.append(PyJWK(key))*
            *except PyJWKError:*
                # skip unusable keys
                *continue*

        if len(self.keys) == 0:
            raise PyJWKSetError("The JWK Set did not contain any usable keys")
```

->

```
class PyJWK:
    def __init__(self, jwk_data, algorithm=None):
        self._algorithms = get_default_algorithms()
```

->

```
ef get_default_algorithms():
    """
    Returns the algorithms that are implemented by the library.
    """
    default_algorithms = {
        "none": NoneAlgorithm(),
        "HS256": HMACAlgorithm(HMACAlgorithm.SHA256),
        "HS384": HMACAlgorithm(HMACAlgorithm.SHA384),
        "HS512": HMACAlgorithm(HMACAlgorithm.SHA512),
    }

    if has_crypto:
        default_algorithms.update({
                "RS256": RSAAlgorithm(RSAAlgorithm.SHA256),
   ...
```

and has_crypto comes from *algorithms.py*
``` 
try:
    import cryptography.exceptions
....
except ModuleNotFoundError:
    has_crypto = False
```


*As summary:*
You iterate through every key, looking if there is an algorithm, find that a dependecie is missing and skip this key, and buring the error message. Afterwards the length of the list of keys is 0 - you get the message of no usable keys and are none the wiser, why this is the case.
please rework you try catch error flow, so that a developer knows when a dependency is missing for the current key!


Discussion:

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 3c69338..12117ec 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -16,6 +16,7 @@ Fixed
 ~~~~~
 
 - Add classifier for Python 3.11 by @eseifert in `#818 <https://github.com/jpadilla/pyjwt/pull/818>`_
+- Fix ``_validate_iat`` validation by @Viicos in `#847 <https://github.com/jpadilla/pyjwt/pull/847>`_
 
 Added
 ~~~~~
diff --git a/jwt/api_jwk.py b/jwt/api_jwk.py
index ef8cce8..fdcde21 100644
--- a/jwt/api_jwk.py
+++ b/jwt/api_jwk.py
@@ -4,7 +4,7 @@ import json
 import time
 from typing import Any, Optional
 
-from .algorithms import get_default_algorithms
+from .algorithms import get_default_algorithms, has_crypto, requires_cryptography
 from .exceptions import InvalidKeyError, PyJWKError, PyJWKSetError
 from .types import JWKDict
 
@@ -49,10 +49,13 @@ class PyJWK:
             else:
                 raise InvalidKeyError(f"Unsupported kty: {kty}")
 
+        if not has_crypto and algorithm in requires_cryptography:
+            raise PyJWKError(f"{algorithm} requires 'cryptography' to be installed.")
+
         self.Algorithm = self._algorithms.get(algorithm)
 
         if not self.Algorithm:
-            raise PyJWKError(f"Unable to find a algorithm for key: {self._jwk_data}")
+            raise PyJWKError(f"Unable to find an algorithm for key: {self._jwk_data}")
 
         self.key = self.Algorithm.from_jwk(self._jwk_data)
 
@@ -66,11 +69,11 @@ class PyJWK:
         return PyJWK.from_dict(obj, algorithm)
 
     @property
-    def key_type(self) -> str:
+    def key_type(self) -> Optional[str]:
         return self._jwk_data.get("kty", None)
 
     @property
-    def key_id(self) -> str:
+    def key_id(self) -> Optional[str]:
         return self._jwk_data.get("kid", None)
 
     @property
@@ -96,7 +99,9 @@ class PyJWKSet:
                 continue
 
         if len(self.keys) == 0:
-            raise PyJWKSetError("The JWK Set did not contain any usable keys")
+            raise PyJWKSetError(
+                "The JWK Set did not contain any usable keys. Perhaps 'cryptography' is not installed?"
+            )
 
     @staticmethod
     def from_dict(obj: dict[str, Any]) -> "PyJWKSet":
diff --git a/jwt/api_jwt.py b/jwt/api_jwt.py
index fd8a8bf..5664949 100644
--- a/jwt/api_jwt.py
+++ b/jwt/api_jwt.py
@@ -265,9 +265,8 @@ class PyJWT:
         now: float,
         leeway: float,
     ) -> None:
-        iat = payload["iat"]
         try:
-            int(iat)
+            iat = int(payload["iat"])
         except ValueError:
             raise InvalidIssuedAtError("Issued At claim (iat) must be an integer.")
         if iat > (now + leeway):


Test patch:
diff --git a/tests/test_api_jwk.py b/tests/test_api_jwk.py
index 3f40fc5..f04f575 100644
--- a/tests/test_api_jwk.py
+++ b/tests/test_api_jwk.py
@@ -6,7 +6,7 @@ from jwt.algorithms import has_crypto
 from jwt.api_jwk import PyJWK, PyJWKSet
 from jwt.exceptions import InvalidKeyError, PyJWKError, PyJWKSetError
 
-from .utils import crypto_required, key_path
+from .utils import crypto_required, key_path, no_crypto_required
 
 if has_crypto:
     from jwt.algorithms import ECAlgorithm, HMACAlgorithm, OKPAlgorithm, RSAAlgorithm
@@ -207,6 +207,12 @@ class TestPyJWK:
         with pytest.raises(InvalidKeyError):
             PyJWK.from_dict(v)
 
+    @no_crypto_required
+    def test_missing_crypto_library_good_error_message(self):
+        with pytest.raises(PyJWKError) as exc:
+            PyJWK({"kty": "dummy"}, algorithm="RS256")
+            assert "cryptography" in str(exc.value)
+
 
 @crypto_required
 class TestPyJWKSet:
diff --git a/tests/test_api_jwt.py b/tests/test_api_jwt.py
index d74973d..24ed240 100644
--- a/tests/test_api_jwt.py
+++ b/tests/test_api_jwt.py
@@ -227,6 +227,13 @@ class TestJWT:
         with pytest.raises(ImmatureSignatureError):
             jwt.decode(jwt_message, secret, algorithms=["HS256"])
 
+    def test_decode_works_if_iat_is_str_of_a_number(self, jwt, payload):
+        payload["iat"] = "1638202770"
+        secret = "secret"
+        jwt_message = jwt.encode(payload, secret)
+        data = jwt.decode(jwt_message, secret, algorithms=["HS256"])
+        assert data["iat"] == "1638202770"
+
     def test_decode_raises_exception_if_nbf_is_not_int(self, jwt):
         # >>> jwt.encode({'nbf': 'not-an-int'}, 'secret')
         example_jwt = (