diff --git a/autopush/endpoint.py b/autopush/endpoint.py index a3dac789..e8a0cdf9 100644 --- a/autopush/endpoint.py +++ b/autopush/endpoint.py @@ -36,6 +36,7 @@ ) from cryptography.fernet import InvalidToken from cryptography.hazmat.primitives import constant_time +from jose import JOSEError from twisted.internet.defer import Deferred from twisted.internet.threads import deferToThread @@ -245,6 +246,14 @@ def _overload_err(self, fail): self._write_response(503, errno=201, message="Please slow message send rate") + def _jws_err(self, fail): + """errBack for JWS/JWT exceptions""" + fail.trap(JOSEError) + self.log.info(format="Authorization Failure: %s" % fail.value, + status_code=401, errno=109, **self._client_info) + self._write_response(401, errno=109, + message="Invalid Authorization") + def _router_response(self, response): for name, val in response.headers.items(): self.set_header(name, val) @@ -374,6 +383,7 @@ def _process_auth(self, result): if auth_type.lower() in AUTH_SCHEMES and '.' in token: d = deferToThread(extract_jwt, token, public_key) d.addCallback(self._store_auth, public_key, token, result) + d.addErrback(self._jws_err) d.addErrback(self._invalid_auth) return d # otherwise, it's not, so ignore the VAPID data. @@ -471,6 +481,7 @@ def put(self, api_ver="v0", token=None): crypto_key_header) d.addCallback(self._process_auth) d.addCallback(self._token_valid) + d.addErrback(self._jws_err) d.addErrback(self._auth_err) d.addErrback(self._token_err) d.addErrback(self._response_err) diff --git a/autopush/tests/test_endpoint.py b/autopush/tests/test_endpoint.py index 9cad3e4a..077355e0 100644 --- a/autopush/tests/test_endpoint.py +++ b/autopush/tests/test_endpoint.py @@ -8,6 +8,7 @@ from hashlib import sha256 import ecdsa +import jose import twisted.internet.base from cryptography.fernet import Fernet, InvalidToken from cyclone.web import Application @@ -1677,6 +1678,22 @@ def handle_finish(value): uaid=dummy_uaid, chid=dummy_chid) return self.finish_deferred + @patch('uuid.uuid4', return_value=uuid.UUID(dummy_uaid)) + @patch('jose.jws.verify', side_effect=jose.exceptions.JWTError) + def test_post_bad_jwt(self, *args): + self.reg.request.body = json.dumps(dict( + channelID=dummy_chid, + )) + + def handle_finish(value): + self._check_error(401, 109, 'Unauthorized') + + self.finish_deferred.addCallback(handle_finish) + self.reg.request.headers["Authorization"] = "WebPush Dummy" + self.reg.post(router_type="webpush", + uaid=dummy_uaid, chid=dummy_chid) + return self.finish_deferred + @patch('uuid.uuid4', return_value=uuid.UUID(dummy_uaid)) def test_post_uaid_chid(self, *args): self.reg.request.body = json.dumps(dict( diff --git a/autopush/tests/test_web_validation.py b/autopush/tests/test_web_validation.py index 8dad22f0..c49cc6aa 100644 --- a/autopush/tests/test_web_validation.py +++ b/autopush/tests/test_web_validation.py @@ -7,6 +7,7 @@ ) from cryptography.fernet import InvalidToken from jose import jws +from jose.exceptions import JWTClaimsError from marshmallow import Schema, fields from mock import Mock, patch from moto import mock_dynamodb2 @@ -689,6 +690,41 @@ def test_invalid_encryption_header(self, mock_jwt): eq_(cm.exception.status_code, 401) eq_(cm.exception.errno, 110) + @patch("autopush.web.validation.extract_jwt") + def test_invalid_encryption_jwt(self, mock_jwt): + schema = self._makeFUT() + self.fernet_mock.decrypt.return_value = dummy_token + # use a deeply superclassed error to make sure that it gets picked up. + mock_jwt.side_effect = JWTClaimsError("invalid claim") + + header = {"typ": "JWT", "alg": "ES256"} + payload = {"aud": "https://push.example.com", + "exp": int(time.time()) + 86400, + "sub": "mailto:admin@example.com"} + + token, crypto_key = self._gen_jwt(header, payload) + auth = "Bearer %s" % token + ckey = 'keyid="a1"; dh="foo";p256ecdsa="%s"' % crypto_key + info = self._make_test_data( + body="asdfasdfasdfasdf", + path_kwargs=dict( + api_ver="v0", + token="asdfasdf", + ), + headers={ + "content-encoding": "aes128", + "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.validation.extract_jwt") def test_invalid_crypto_key_header_content(self, mock_jwt): schema = self._makeFUT() diff --git a/autopush/web/validation.py b/autopush/web/validation.py index ef4a29e9..368a15c0 100644 --- a/autopush/web/validation.py +++ b/autopush/web/validation.py @@ -8,6 +8,7 @@ ItemNotFound, ) from cryptography.fernet import InvalidToken +from jose import JOSEError from marshmallow import ( Schema, fields, @@ -320,8 +321,8 @@ def validate_auth(self, d): try: jwt = extract_jwt(token, public_key) - except ValueError: - raise InvalidRequest("Invalid Authorization Header", + except (ValueError, JOSEError) as ex: + raise InvalidRequest("Invalid Authorization Header: %s" % str(ex), status_code=401, errno=109, headers={"www-authenticate": PREF_SCHEME}) if jwt.get('exp', 0) < time.time():