From 97d4213655b0b78310a474e8ddb672d4cdb5449c Mon Sep 17 00:00:00 2001 From: Ben Bangert Date: Thu, 9 Feb 2017 14:21:19 -0800 Subject: [PATCH] feat: add thorough jwt exp validation We now attempt to coerce the jwt exp value in case it wasn't an int and verify that it is within the next 24 hours and has not already expired. Closes #794 --- autopush/tests/test_web_validation.py | 64 +++++++++++++++++++++++++++ autopush/web/webpush.py | 22 ++++++++- 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/autopush/tests/test_web_validation.py b/autopush/tests/test_web_validation.py index 0ae68b06..5e2634f1 100644 --- a/autopush/tests/test_web_validation.py +++ b/autopush/tests/test_web_validation.py @@ -901,6 +901,70 @@ def test_invalid_vapid_crypto_header(self, mock_jwt): eq_(cm.exception.status_code, 401) eq_(cm.exception.errno, 109) + def test_invalid_too_far_exp_vapid_crypto_header(self): + schema = self._make_fut() + header = {"typ": "JWT", "alg": "ES256"} + payload = {"aud": "https://pusher_origin.example.com", + "exp": int(time.time()) + 86400 + 86400, + "sub": "mailto:admin@example.com"} + + token, crypto_key = self._gen_jwt(header, payload) + auth = "WebPush %s" % token + self.fernet_mock.decrypt.return_value = ('a'*32) + \ + sha256(utils.base64url_decode(crypto_key)).digest() + ckey = 'keyid="a1"; dh="foo";p256ecdsa="%s"' % crypto_key + info = self._make_test_data( + body="asdfasdfasdfasdf", + path_kwargs=dict( + api_ver="v2", + token="asdfasdf", + ), + headers={ + "content-encoding": "aesgcm", + "encryption": "salt=stuff", + "authorization": auth, + "crypto-key": ckey + } + ) + + with assert_raises(InvalidRequest) as cm: + schema.load(info) + + eq_(cm.exception.status_code, 401) + eq_(cm.exception.errno, 109) + + def test_invalid_bad_exp_vapid_crypto_header(self): + schema = self._make_fut() + header = {"typ": "JWT", "alg": "ES256"} + payload = {"aud": "https://pusher_origin.example.com", + "exp": "bleh", + "sub": "mailto:admin@example.com"} + + token, crypto_key = self._gen_jwt(header, payload) + auth = "WebPush %s" % token + self.fernet_mock.decrypt.return_value = ('a'*32) + \ + sha256(utils.base64url_decode(crypto_key)).digest() + ckey = 'keyid="a1"; dh="foo";p256ecdsa="%s"' % crypto_key + info = self._make_test_data( + body="asdfasdfasdfasdf", + path_kwargs=dict( + api_ver="v2", + token="asdfasdf", + ), + headers={ + "content-encoding": "aesgcm", + "encryption": "salt=stuff", + "authorization": auth, + "crypto-key": ckey + } + ) + + with assert_raises(InvalidRequest) as cm: + schema.load(info) + + eq_(cm.exception.status_code, 401) + eq_(cm.exception.errno, 109) + @patch("autopush.web.webpush.extract_jwt") def test_invalid_encryption_header(self, mock_jwt): schema = self._make_fut() diff --git a/autopush/web/webpush.py b/autopush/web/webpush.py index 00b37ae7..090318bb 100644 --- a/autopush/web/webpush.py +++ b/autopush/web/webpush.py @@ -325,10 +325,30 @@ def validate_auth(self, d): raise InvalidRequest("Invalid Authorization Header", status_code=401, errno=109, headers={"www-authenticate": PREF_SCHEME}) - if jwt.get('exp', 0) < time.time(): + if "exp" not in jwt: + raise InvalidRequest("Invalid bearer token: No expiration", + status_code=401, errno=109, + headers={"www-authenticate": PREF_SCHEME}) + + try: + jwt_expires = int(jwt['exp']) + except ValueError: + raise InvalidRequest("Invalid bearer token: Invalid expiration", + status_code=401, errno=109, + headers={"www-authenticate": PREF_SCHEME}) + + now = time.time() + jwt_has_expired = now > jwt_expires + if jwt_has_expired: raise InvalidRequest("Invalid bearer token: Auth expired", status_code=401, errno=109, headers={"www-authenticate": PREF_SCHEME}) + jwt_too_far_in_future = (jwt_expires - now) > (60*60*24) + if jwt_too_far_in_future: + raise InvalidRequest("Invalid bearer token: Auth > 24 hours in " + "the future", + status_code=401, errno=109, + headers={"www-authenticate": PREF_SCHEME}) jwt_crypto_key = base64url_encode(public_key) d["jwt"] = dict(jwt_crypto_key=jwt_crypto_key, jwt_data=jwt)