diff --git a/requirements/prod.txt b/requirements/prod.txt index e7ca09cb8f25..fe7e2f488736 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -35,9 +35,9 @@ Pillow==4.1.0 \ --hash=sha256:7b769f1115c6c4a6a567a2e4e8406f0469fb4043b20239778aafbdf3d4ff49f5 \ --hash=sha256:d3499d67551b3699e5478e80c8132cf60180bb78839ed18fafbff968f858cfeb # PyJWT is required by djangorestframework-jwt -PyJWT==1.4.2 \ - --hash=sha256:99fe612dbe5f41e07124d9002c118c14f3ee703574ffa9779fee78135b8b94b6 \ - --hash=sha256:87a831b7a3bfa8351511961469ed0462a769724d4da48a501cb8c96d1e17f570 +PyJWT==1.5.0 \ + --hash=sha256:ad60a3fb9b393667864ed4b8abc9c3b570747f80bf77a113ead2fbaf0f0cedf3 \ + --hash=sha256:fd182b728d13f04c289d9b2623d09256d356c9b4a6778018001454a954d7c54b SQLAlchemy==0.7.5 \ --hash=sha256:7e31190a15753694dcb6f4399ce7d02091b0bccf825272d6254e56144debfd18 # pyup: ==0.7.5 amo-validator==1.10.59 \ diff --git a/src/olympia/api/jwt_auth.py b/src/olympia/api/jwt_auth.py index 76287d75449a..878041a04f53 100644 --- a/src/olympia/api/jwt_auth.py +++ b/src/olympia/api/jwt_auth.py @@ -12,6 +12,9 @@ See https://github.com/GetBlimp/django-rest-framework-jwt/ for more info. """ +from calendar import timegm +from datetime import datetime + from django.conf import settings from django.core.exceptions import ObjectDoesNotExist @@ -63,7 +66,10 @@ def jwt_decode_handler(token, get_api_key=APIKey.get_jwt_key): 'require_iat': True, 'require_nbf': False, } + try: + now = timegm(datetime.utcnow().utctimetuple()) + payload = jwt.decode( token, api_key.secret, @@ -71,6 +77,14 @@ def jwt_decode_handler(token, get_api_key=APIKey.get_jwt_key): leeway=api_settings.JWT_LEEWAY, algorithms=[api_settings.JWT_ALGORITHM] ) + + # Verify clock skew for future iat-values pyjwt removed that check in + # https://github.com/jpadilla/pyjwt/pull/252/ + # `verify_iat` is still in options because pyjwt still validates + # that `iat` is a proper number. + if int(payload['iat']) > (now + api_settings.JWT_LEEWAY): + raise jwt.InvalidIssuedAtError( + 'Issued At claim (iat) cannot be in the future.') except jwt.MissingRequiredClaimError, exc: log.info(u'Missing required claim during JWT authentication: ' u'{e.__class__.__name__}: {e}'.format(e=exc)) diff --git a/src/olympia/api/tests/test_jwt_auth.py b/src/olympia/api/tests/test_jwt_auth.py index fed8b62ac8dd..0ebf4e0c0b6b 100644 --- a/src/olympia/api/tests/test_jwt_auth.py +++ b/src/olympia/api/tests/test_jwt_auth.py @@ -116,7 +116,8 @@ def test_missing_issued_at_time(self): def test_invalid_issued_at_time(self): api_key = self.create_api_key(self.user) payload = self.auth_token_payload(self.user, api_key.key) - # Simulate clock skew: + + # Simulate clock skew... payload['iat'] = ( datetime.utcnow() + timedelta(seconds=settings.JWT_AUTH['JWT_LEEWAY'] + 10)) @@ -128,6 +129,20 @@ def test_invalid_issued_at_time(self): assert ctx.exception.detail.startswith( 'JWT iat (issued at time) is invalid.') + def test_invalid_issued_at_time_not_number(self): + api_key = self.create_api_key(self.user) + payload = self.auth_token_payload(self.user, api_key.key) + + # Simulate clock skew... + payload['iat'] = 'thisisnotanumber' + token = self.encode_token_payload(payload, api_key.secret) + + with self.assertRaises(AuthenticationFailed) as ctx: + jwt_auth.jwt_decode_handler(token) + + assert ctx.exception.detail.startswith( + 'JWT iat (issued at time) is invalid.') + def test_missing_expiration(self): api_key = self.create_api_key(self.user) payload = self.auth_token_payload(self.user, api_key.key) diff --git a/tests/ui/requirements.txt b/tests/ui/requirements.txt index 22bec606af71..1d2d4be63635 100644 --- a/tests/ui/requirements.txt +++ b/tests/ui/requirements.txt @@ -1,5 +1,5 @@ fxapom==1.10.0 -PyJWT==1.4.2 +PyJWT==1.5.0 PyPOM==1.1.1 pytest==3.0.7 pytest-instafail==0.3.0