Repository: jpadilla/pyjwt

Issue:
non-numeric expiration claim does not raise an error
I would expect a non-numeric expiration claim to raise an error but it does not. To reproduce, here's a JWT with ``{exp: '<not a number>'}``':

````
>>> import jwt
>>> import calendar, time
>>> iat = calendar.timegm(time.gmtime())
>>> token = jwt.encode({'aud': 'some-aud', 'iss': 'some-iss', 'typ': 'some-typ', 'iat': iat, 'exp': '<not a number>', 'request': {}}, 'secret')
>>> jwt.decode(token, 'secret', verify=True, audience='some-aud')
{u'aud': u'some-aud', u'iss': u'some-iss', u'request': {}, u'exp': u'<not a number>', u'iat': 1427495823, u'typ': u'some-typ'}
````

If a signature checking was somehow bypassed, an expiration like this could maybe allow for replay attacks.

Discussion:

Patch:
diff --git a/jwt/api.py b/jwt/api.py
index 5f2ce6f..201bffd 100644
--- a/jwt/api.py
+++ b/jwt/api.py
@@ -181,16 +181,32 @@ class PyJWT(object):
         except KeyError:
             raise InvalidAlgorithmError('Algorithm not supported')
 
+        if 'iat' in payload:
+            try:
+                int(payload['iat'])
+            except ValueError:
+                raise DecodeError('Issued At claim (iat) must be an integer.')
+
         if 'nbf' in payload and verify_expiration:
+            try:
+                nbf = int(payload['nbf'])
+            except ValueError:
+                raise DecodeError('Not Before claim (nbf) must be an integer.')
+
             utc_timestamp = timegm(datetime.utcnow().utctimetuple())
 
-            if payload['nbf'] > (utc_timestamp + leeway):
+            if nbf > (utc_timestamp + leeway):
                 raise ExpiredSignatureError('Signature not yet valid')
 
         if 'exp' in payload and verify_expiration:
+            try:
+                exp = int(payload['exp'])
+            except ValueError:
+                raise DecodeError('Expiration Time claim (exp) must be an integer.')
+
             utc_timestamp = timegm(datetime.utcnow().utctimetuple())
 
-            if payload['exp'] < (utc_timestamp - leeway):
+            if exp < (utc_timestamp - leeway):
                 raise ExpiredSignatureError('Signature has expired')
 
         if 'aud' in payload:


Test patch:
diff --git a/tests/test_api.py b/tests/test_api.py
index 3b79490..371390d 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -195,6 +195,33 @@ class TestAPI(unittest.TestCase):
         exception = context.exception
         self.assertEquals(str(exception), 'Algorithm not supported')
 
+    def test_decode_raises_exception_if_exp_is_not_int(self):
+        # >>> jwt.encode({'exp': 'not-an-int'}, 'secret')
+        example_jwt = ('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.'
+                       'eyJleHAiOiJub3QtYW4taW50In0.'
+                       'P65iYgoHtBqB07PMtBSuKNUEIPPPfmjfJG217cEE66s')
+
+        with self.assertRaisesRegexp(DecodeError, 'exp'):
+            self.jwt.decode(example_jwt, 'secret')
+
+    def test_decode_raises_exception_if_iat_is_not_int(self):
+        # >>> jwt.encode({'iat': 'not-an-int'}, 'secret')
+        example_jwt = ('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.'
+                       'eyJpYXQiOiJub3QtYW4taW50In0.'
+                       'H1GmcQgSySa5LOKYbzGm--b1OmRbHFkyk8pq811FzZM')
+
+        with self.assertRaisesRegexp(DecodeError, 'iat'):
+            self.jwt.decode(example_jwt, 'secret')
+
+    def test_decode_raises_exception_if_nbf_is_not_int(self):
+        # >>> jwt.encode({'nbf': 'not-an-int'}, 'secret')
+        example_jwt = ('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.'
+                       'eyJuYmYiOiJub3QtYW4taW50In0.'
+                       'c25hldC8G2ZamC8uKpax9sYMTgdZo3cxrmzFHaAAluw')
+
+        with self.assertRaisesRegexp(DecodeError, 'nbf'):
+            self.jwt.decode(example_jwt, 'secret')
+
     def test_encode_datetime(self):
         secret = 'secret'
         current_datetime = datetime.utcnow()