From 47a74617dd641ac53a38c1c1ac91aaafd495df3c Mon Sep 17 00:00:00 2001 From: Karl Sutt Date: Thu, 4 Feb 2021 20:31:46 +0200 Subject: [PATCH 1/9] Fix tests for pyjwt>=2.0.0 --- requirements.txt | 4 ++-- setup.py | 2 +- tests/unit/jwt/test_jwt.py | 5 ++--- twilio/jwt/__init__.py | 3 ++- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/requirements.txt b/requirements.txt index 7c00eeefde..b43d372b3e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,5 +3,5 @@ mock nose six requests>=2.0.0 -PyJWT==1.7.1 -twine \ No newline at end of file +PyJWT>=2.0.0 +twine diff --git a/setup.py b/setup.py index 1385e5e804..3a39d82bca 100755 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ install_requires=[ "six", "pytz", - "PyJWT == 1.7.1", + "PyJWT >= 2.0.0", ], extras_require={ ':python_version<"3.0"': [ diff --git a/tests/unit/jwt/test_jwt.py b/tests/unit/jwt/test_jwt.py index 053bb610bf..1ef8504448 100644 --- a/tests/unit/jwt/test_jwt.py +++ b/tests/unit/jwt/test_jwt.py @@ -43,7 +43,7 @@ def assertJwtsEqual(self, jwt, key, expected_payload=None, expected_headers=None expected_headers = expected_headers or {} expected_payload = expected_payload or {} - decoded_payload = jwt_lib.decode(jwt, key, verify=False) + decoded_payload = jwt_lib.decode(jwt, key, algorithms=["HS256"], options={"verify_signature":False}) decoded_headers = jwt_lib.get_unverified_header(jwt) self.assertEqual(expected_headers, decoded_headers) @@ -242,9 +242,8 @@ def test_decode_bad_secret(self): def test_decode_modified_jwt_fails(self): jwt = DummyJwt('secret_key', 'issuer') - example_jwt = jwt.to_jwt().decode('utf-8') + example_jwt = jwt.to_jwt() example_jwt = 'ABC' + example_jwt[3:] - example_jwt = example_jwt.encode('utf-8') self.assertRaises(JwtDecodeError, Jwt.from_jwt, example_jwt, 'secret_key') diff --git a/twilio/jwt/__init__.py b/twilio/jwt/__init__.py index 32aeb2d544..c74ecf7505 100644 --- a/twilio/jwt/__init__.py +++ b/twilio/jwt/__init__.py @@ -140,7 +140,8 @@ def from_jwt(cls, jwt, key=''): verify = True if key else False try: - payload = jwt_lib.decode(bytes(jwt), key, options={ + alg = jwt_lib.get_unverified_header(jwt).get("alg", "HS256") + payload = jwt_lib.decode(jwt, key, algorithms=[alg], options={ 'verify_signature': verify, 'verify_exp': True, 'verify_nbf': True, From 0b9127a4587c9024f8b16e8002de7ec7f153ce4d Mon Sep 17 00:00:00 2001 From: Karl Sutt Date: Thu, 4 Feb 2021 21:38:59 +0200 Subject: [PATCH 2/9] Whitespace --- tests/unit/jwt/test_jwt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/jwt/test_jwt.py b/tests/unit/jwt/test_jwt.py index 1ef8504448..051aeedfcf 100644 --- a/tests/unit/jwt/test_jwt.py +++ b/tests/unit/jwt/test_jwt.py @@ -43,7 +43,7 @@ def assertJwtsEqual(self, jwt, key, expected_payload=None, expected_headers=None expected_headers = expected_headers or {} expected_payload = expected_payload or {} - decoded_payload = jwt_lib.decode(jwt, key, algorithms=["HS256"], options={"verify_signature":False}) + decoded_payload = jwt_lib.decode(jwt, key, algorithms=["HS256"], options={"verify_signature": False}) decoded_headers = jwt_lib.get_unverified_header(jwt) self.assertEqual(expected_headers, decoded_headers) From 92f3c624c1eee8a691a1d2e58dca5ad4ad2d136f Mon Sep 17 00:00:00 2001 From: Karl Sutt Date: Mon, 15 Feb 2021 21:15:57 +0200 Subject: [PATCH 3/9] Set an upper bound for PyJWT version --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index b43d372b3e..f5389d9337 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,5 +3,5 @@ mock nose six requests>=2.0.0 -PyJWT>=2.0.0 +PyJWT>=2.0.0, <3.0.0 twine diff --git a/setup.py b/setup.py index 3a39d82bca..03ef63abe4 100755 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ install_requires=[ "six", "pytz", - "PyJWT >= 2.0.0", + "PyJWT >= 2.0.0, < 3.0.0", ], extras_require={ ':python_version<"3.0"': [ From 19babebdd0af7a828dc3f477dc69bf21cd680750 Mon Sep 17 00:00:00 2001 From: Karl Sutt Date: Wed, 17 Feb 2021 19:07:30 +0200 Subject: [PATCH 4/9] Drop support for Python 2.7, 3.0-3.5 --- .deepsource.toml | 5 +---- .travis.yml | 3 --- Makefile | 2 +- README.md | 3 --- requirements.txt | 1 - setup.py | 15 ++------------- tests/unit/test_request_validator.py | 14 ++------------ tests/unit/twiml/__init__.py | 3 +-- tests/unit/twiml/test_voice_response.py | 3 +-- tox.ini | 2 +- twilio/base/exceptions.py | 10 ++++------ twilio/base/values.py | 3 +-- twilio/compat.py | 17 ----------------- twilio/http/http_client.py | 2 +- twilio/http/request.py | 2 +- twilio/http/validation_client.py | 2 +- twilio/jwt/__init__.py | 6 ------ twilio/jwt/client/__init__.py | 5 ++--- twilio/jwt/compat.py | 25 ------------------------- twilio/jwt/validation/__init__.py | 3 +-- twilio/request_validator.py | 14 +++++--------- twilio/rest/__init__.py | 5 +---- 22 files changed, 26 insertions(+), 119 deletions(-) delete mode 100644 twilio/compat.py delete mode 100644 twilio/jwt/compat.py diff --git a/.deepsource.toml b/.deepsource.toml index 1fc2a332af..a352c02e4c 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -2,14 +2,11 @@ version = 1 exclude_patterns = [ 'examples/**', - + # auto-generated files 'twilio/rest/**', 'twilio/twiml/**', 'tests/integration/**', - - # compat files - 'twilio/compat.py', ] test_patterns = [ diff --git a/.travis.yml b/.travis.yml index 2f09d37edb..181ed7d350 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,6 @@ dist: xenial # required for Python >= 3.7 language: python python: - - "2.7" - - "3.4" - - "3.5" - "3.6" - "3.7" - "3.8" diff --git a/Makefile b/Makefile index 42ecfb4c79..b97801fd13 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: clean install analysis test test-install develop docs docs-install venv: - @python --version || (echo "Python is not installed, please install Python 2 or Python 3"; exit 1); + @python --version || (echo "Python is not installed, Python 3.6+"; exit 1); virtualenv --python=python venv install: venv diff --git a/README.md b/README.md index 1501843496..efde81b1ae 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,6 @@ Please consult the [official migration guide](https://www.twilio.com/docs/librar This library supports the following Python implementations: -* Python 2.7 -* Python 3.4 -* Python 3.5 * Python 3.6 * Python 3.7 * Python 3.8 diff --git a/requirements.txt b/requirements.txt index f5389d9337..29174d1c0c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,6 @@ flake8 mock nose -six requests>=2.0.0 PyJWT>=2.0.0, <3.0.0 twine diff --git a/setup.py b/setup.py index 03ef63abe4..b260cc277d 100755 --- a/setup.py +++ b/setup.py @@ -1,4 +1,3 @@ -from __future__ import with_statement from setuptools import setup, find_packages __version__ = None @@ -24,19 +23,12 @@ author_email="help@twilio.com", url="https://github.com/twilio/twilio-python/", keywords=["twilio", "twiml"], + python_requires='>=3.6.0', install_requires=[ - "six", "pytz", + "requests >= 2.0.0", "PyJWT >= 2.0.0, < 3.0.0", ], - extras_require={ - ':python_version<"3.0"': [ - "requests[security] >= 2.0.0", - ], - ':python_version>="3.0"': [ - "requests >= 2.0.0" - ], - }, packages=find_packages(exclude=['tests', 'tests.*']), include_package_data=True, classifiers=[ @@ -45,9 +37,6 @@ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", diff --git a/tests/unit/test_request_validator.py b/tests/unit/test_request_validator.py index 245c5ae43a..86478d6746 100644 --- a/tests/unit/test_request_validator.py +++ b/tests/unit/test_request_validator.py @@ -2,7 +2,6 @@ import unittest from nose.tools import assert_equal, assert_true -from six import b, u from twilio.request_validator import RequestValidator @@ -26,22 +25,13 @@ def setUp(self): self.bodyHash = "0a1ff7634d9ab3b95db5c9a2dfe9416e41502b283a80c7cf19632632f96e6620" self.uriWithBody = self.uri + "&bodySHA256=" + self.bodyHash - def test_compute_signature_bytecode(self): - expected = b(self.expected) - signature = self.validator.compute_signature(self.uri, - self.params, - utf=False) - assert_equal(signature, expected) - def test_compute_signature(self): expected = (self.expected) - signature = self.validator.compute_signature(self.uri, - self.params, - utf=True) + signature = self.validator.compute_signature(self.uri, self.params) assert_equal(signature, expected) def test_compute_hash_unicode(self): - expected = u(self.bodyHash) + expected = self.bodyHash body_hash = self.validator.compute_hash(self.body) assert_equal(expected, body_hash) diff --git a/tests/unit/twiml/__init__.py b/tests/unit/twiml/__init__.py index b589cf460e..70b95d781e 100644 --- a/tests/unit/twiml/__init__.py +++ b/tests/unit/twiml/__init__.py @@ -1,7 +1,6 @@ import unittest from nose.tools import raises -from six import text_type from twilio.twiml import ( format_language, @@ -13,7 +12,7 @@ class TwilioTest(unittest.TestCase): def strip(self, xml): - return text_type(xml) + return str(xml) @raises(TwiMLException) def test_append_fail(self): diff --git a/tests/unit/twiml/test_voice_response.py b/tests/unit/twiml/test_voice_response.py index 7f7e478556..6a90112585 100644 --- a/tests/unit/twiml/test_voice_response.py +++ b/tests/unit/twiml/test_voice_response.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- from nose.tools import assert_equal -from six import u from tests.unit.twiml import TwilioTest from twilio.twiml.voice_response import VoiceResponse, Dial, Enqueue, Gather @@ -82,7 +81,7 @@ def test_say_hello_world(self): def test_say_french(self): """ should say hello monkey """ r = VoiceResponse() - r.say(u('n\xe9cessaire et d\'autres')) + r.say('n\xe9cessaire et d\'autres') assert_equal( self.strip(r), diff --git a/tox.ini b/tox.ini index 772c099d75..93895226e7 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27, py3{4,5,6,7,8}, pypy +envlist = py3{6,7,8}, pypy skip_missing_interpreters = true [testenv] diff --git a/twilio/base/exceptions.py b/twilio/base/exceptions.py index fdf9903d8d..ec14747f02 100644 --- a/twilio/base/exceptions.py +++ b/twilio/base/exceptions.py @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- import sys -from six import u - class TwilioException(Exception): pass @@ -32,16 +30,16 @@ def __str__(self): """ Try to pretty-print the exception, if this is going on screen. """ def red(words): - return u("\033[31m\033[49m%s\033[0m") % words + return "\033[31m\033[49m%s\033[0m" % words def white(words): - return u("\033[37m\033[49m%s\033[0m") % words + return "\033[37m\033[49m%s\033[0m" % words def blue(words): - return u("\033[34m\033[49m%s\033[0m") % words + return "\033[34m\033[49m%s\033[0m" % words def teal(words): - return u("\033[36m\033[49m%s\033[0m") % words + return "\033[36m\033[49m%s\033[0m" % words def get_uri(code): return "https://www.twilio.com/docs/errors/{0}".format(code) diff --git a/twilio/base/values.py b/twilio/base/values.py index f2421c3a7f..b05f3390b5 100644 --- a/twilio/base/values.py +++ b/twilio/base/values.py @@ -1,4 +1,3 @@ -from six import iteritems unset = object() @@ -9,4 +8,4 @@ def of(d): :param dict d: A dict to strip. :return dict: A dict with unset values removed. """ - return {k: v for k, v in iteritems(d) if v != unset} + return {k: v for k, v in d.items() if v != unset} diff --git a/twilio/compat.py b/twilio/compat.py deleted file mode 100644 index de40b70089..0000000000 --- a/twilio/compat.py +++ /dev/null @@ -1,17 +0,0 @@ -# Those are not supported by the six library and needs to be done manually -try: - # python 3 - from urllib.parse import urlencode, urlparse, urljoin, urlunparse, parse_qs -except ImportError: - # python 2 backward compatibility - # noinspection PyUnresolvedReferences - from urllib import urlencode - # noinspection PyUnresolvedReferences - from urlparse import urlparse, urljoin, urlunparse, parse_qs - -try: - # python 2 - from itertools import izip -except ImportError: - # python 3 - izip = zip diff --git a/twilio/http/http_client.py b/twilio/http/http_client.py index a83493dd16..a9ce024a2a 100644 --- a/twilio/http/http_client.py +++ b/twilio/http/http_client.py @@ -2,7 +2,7 @@ from requests import Request, Session, hooks from requests.adapters import HTTPAdapter -from twilio.compat import urlencode +from urllib.parse import urlencode from twilio.http import HttpClient from twilio.http.request import Request as TwilioRequest from twilio.http.response import Response diff --git a/twilio/http/request.py b/twilio/http/request.py index e96528dcff..49ec6e8faa 100644 --- a/twilio/http/request.py +++ b/twilio/http/request.py @@ -1,4 +1,4 @@ -from twilio.compat import urlencode +from urllib.parse import urlencode class Request(object): diff --git a/twilio/http/validation_client.py b/twilio/http/validation_client.py index 78abe1cbbc..e14ca7325a 100644 --- a/twilio/http/validation_client.py +++ b/twilio/http/validation_client.py @@ -3,7 +3,7 @@ from requests import Request, Session from twilio.base.exceptions import TwilioRestException -from twilio.compat import urlparse +from urllib.parse import urlparse from twilio.http import HttpClient from twilio.http.response import Response from twilio.jwt.validation import ClientValidationJwt diff --git a/twilio/jwt/__init__.py b/twilio/jwt/__init__.py index bec6aa3f63..5e23100d52 100644 --- a/twilio/jwt/__init__.py +++ b/twilio/jwt/__init__.py @@ -1,12 +1,6 @@ import hmac import sys -from twilio.jwt import compat - -if sys.version_info[0] == 3 and sys.version_info[1] == 2: - # PyJWT expects hmac.compare_digest to exist even under python 3.2 - hmac.compare_digest = compat.compare_digest - import jwt as jwt_lib try: diff --git a/twilio/jwt/client/__init__.py b/twilio/jwt/client/__init__.py index be0ede910b..d1f8fe77ce 100644 --- a/twilio/jwt/client/__init__.py +++ b/twilio/jwt/client/__init__.py @@ -1,7 +1,6 @@ from twilio.jwt import Jwt -from six import iteritems -from twilio.compat import urlencode +from urllib.parse import urlencode class ClientCapabilityToken(Jwt): @@ -94,7 +93,7 @@ def add_param(self, key, value): def to_payload(self): if self.params: - sorted_params = sorted([(k, v) for k, v in iteritems(self.params)]) + sorted_params = sorted([(k, v) for k, v in self.params.items()]) encoded_params = urlencode(sorted_params) param_string = '?{}'.format(encoded_params) else: diff --git a/twilio/jwt/compat.py b/twilio/jwt/compat.py deleted file mode 100644 index f0237c2f72..0000000000 --- a/twilio/jwt/compat.py +++ /dev/null @@ -1,25 +0,0 @@ -def compare_digest(a, b): - """ - PyJWT expects hmac.compare_digest to exist for all Python 3.x, however it was added in Python > 3.3 - It has a fallback for Python 2.x but not for Pythons between 2.x and 3.3 - Copied from: https://github.com/python/cpython/commit/6cea65555caf2716b4633827715004ab0291a282#diff-c49659257ec1b129707ce47a98adc96eL16 - - Returns the equivalent of 'a == b', but avoids content based short - circuiting to reduce the vulnerability to timing attacks. - """ - # Consistent timing matters more here than data type flexibility - if not (isinstance(a, bytes) and isinstance(b, bytes)): - raise TypeError("inputs must be bytes instances") - - # We assume the length of the expected digest is public knowledge, - # thus this early return isn't leaking anything an attacker wouldn't - # already know - if len(a) != len(b): - return False - - # We assume that integers in the bytes range are all cached, - # thus timing shouldn't vary much due to integer object creation - result = 0 - for x, y in zip(a, b): - result |= x ^ y - return result == 0 diff --git a/twilio/jwt/validation/__init__.py b/twilio/jwt/validation/__init__.py index 63b9292bcf..216b0015a6 100644 --- a/twilio/jwt/validation/__init__.py +++ b/twilio/jwt/validation/__init__.py @@ -1,5 +1,4 @@ from hashlib import sha256 -from six import string_types from twilio.jwt import Jwt @@ -73,7 +72,7 @@ def _generate_payload(self): @classmethod def _sort_and_join(cls, values, joiner): - if isinstance(values, string_types): + if isinstance(values, str): return values return joiner.join(sorted(values)) diff --git a/twilio/request_validator.py b/twilio/request_validator.py index 7e57923f5a..9f11925d98 100644 --- a/twilio/request_validator.py +++ b/twilio/request_validator.py @@ -2,9 +2,7 @@ import hmac from hashlib import sha1, sha256 -from six import PY3, string_types - -from twilio.compat import izip, urlparse, parse_qs +from urllib.parse import urlparse, parse_qs def compare(string1, string2): @@ -19,7 +17,7 @@ def compare(string1, string2): if len(string1) != len(string2): return False result = True - for c1, c2 in izip(string1, string2): + for c1, c2 in zip(string1, string2): result &= c1 == c2 return result @@ -65,12 +63,11 @@ class RequestValidator(object): def __init__(self, token): self.token = token.encode("utf-8") - def compute_signature(self, uri, params, utf=PY3): + def compute_signature(self, uri, params): """Compute the signature for a given request :param uri: full URI that Twilio requested on your server :param params: post vars that Twilio sent with the request - :param utf: whether return should be bytestring or unicode (python3) :returns: The computed signature """ @@ -82,8 +79,7 @@ def compute_signature(self, uri, params, utf=PY3): # compute signature and compare signatures mac = hmac.new(self.token, s.encode("utf-8"), sha1) computed = base64.b64encode(mac.digest()) - if utf: - computed = computed.decode('utf-8') + computed = computed.decode('utf-8') return computed.strip() @@ -113,7 +109,7 @@ def validate(self, uri, params, signature): valid_body_hash = True # May not receive body hash, so default succeed query = parse_qs(parsed_uri.query) - if "bodySHA256" in query and isinstance(params, string_types): + if "bodySHA256" in query and isinstance(params, str): valid_body_hash = compare(self.compute_hash(params), query["bodySHA256"][0]) params = {} diff --git a/twilio/rest/__init__.py b/twilio/rest/__init__.py index 4d4d24e71d..6c02af6f7c 100644 --- a/twilio/rest/__init__.py +++ b/twilio/rest/__init__.py @@ -8,13 +8,10 @@ import os import platform +from urllib.parse import urlparse,urlunparse from twilio import __version__ from twilio.base.exceptions import TwilioException from twilio.base.obsolete import obsolete_client -from twilio.compat import ( - urlparse, - urlunparse, -) from twilio.http.http_client import TwilioHttpClient From ff01b3e1442938e081bfe6f8b99725c8ba1205e3 Mon Sep 17 00:00:00 2001 From: Karl Sutt Date: Wed, 17 Feb 2021 21:33:02 +0200 Subject: [PATCH 5/9] Add Python 3.9 to build configuration --- .travis.yml | 1 + README.md | 1 + setup.py | 1 + 3 files changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 181ed7d350..65d11fc533 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ python: - "3.6" - "3.7" - "3.8" + - "3.9" services: - docker install: diff --git a/README.md b/README.md index efde81b1ae..e76c0b3835 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ This library supports the following Python implementations: * Python 3.6 * Python 3.7 * Python 3.8 +* Python 3.9 ## Installation diff --git a/setup.py b/setup.py index b260cc277d..e4d260fa02 100755 --- a/setup.py +++ b/setup.py @@ -40,6 +40,7 @@ "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Communications :: Telephony", From dab158f429015e0894217d6503f55b517c27c474 Mon Sep 17 00:00:00 2001 From: Karl Sutt Date: Sat, 17 Apr 2021 15:16:54 +0300 Subject: [PATCH 6/9] Prevent overriding `algorithm` in `to_jwt` --- tests/unit/jwt/test_client_validation.py | 4 +-- tests/unit/jwt/test_jwt.py | 44 +++++------------------- twilio/jwt/__init__.py | 25 ++++---------- twilio/jwt/access_token/__init__.py | 5 ++- twilio/jwt/client/__init__.py | 4 ++- twilio/jwt/taskrouter/__init__.py | 4 +-- twilio/jwt/validation/__init__.py | 3 +- 7 files changed, 27 insertions(+), 62 deletions(-) diff --git a/tests/unit/jwt/test_client_validation.py b/tests/unit/jwt/test_client_validation.py index 5e0ddb8995..60c183fc8f 100644 --- a/tests/unit/jwt/test_client_validation.py +++ b/tests/unit/jwt/test_client_validation.py @@ -266,7 +266,7 @@ def test_jwt_signing(self): private_key = private_key.private_bytes(Encoding.PEM, PrivateFormat.PKCS8, NoEncryption()) jwt = ClientValidationJwt('AC123', 'SK123', 'CR123', private_key, vp) - decoded = Jwt.from_jwt(jwt.to_jwt(), public_key) + decoded = ClientValidationJwt.from_jwt(jwt.to_jwt(), public_key) self.assertDictContainsSubset({ 'hrh': 'authorization;host', @@ -282,5 +282,3 @@ def test_jwt_signing(self): 'cty': 'twilio-pkrv;v=1', 'kid': 'CR123' }, decoded.headers) - - diff --git a/tests/unit/jwt/test_jwt.py b/tests/unit/jwt/test_jwt.py index 051aeedfcf..0af17b4d17 100644 --- a/tests/unit/jwt/test_jwt.py +++ b/tests/unit/jwt/test_jwt.py @@ -10,13 +10,17 @@ class DummyJwt(Jwt): """Jwt implementation that allows setting arbitrary payload and headers for testing.""" - def __init__(self, secret_key, issuer, subject=None, algorithm='HS256', nbf=Jwt.GENERATE, - ttl=3600, valid_until=None, headers=None, payload=None): + + ALGORITHM = 'HS256' + + def __init__(self, secret_key, issuer, subject=None, algorithm=None, + nbf=Jwt.GENERATE, ttl=3600, valid_until=None, headers=None, + payload=None): super(DummyJwt, self).__init__( secret_key=secret_key, issuer=issuer, subject=subject, - algorithm=algorithm, + algorithm=algorithm or self.ALGORITHM, nbf=nbf, ttl=ttl, valid_until=valid_until @@ -146,37 +150,11 @@ def test_encode_custom_nbf(self, time_mock): expected_payload={'iss': 'issuer', 'exp': 10, 'nbf': 5}, ) - @patch('time.time') - def test_encode_custom_algorithm(self, time_mock): - time_mock.return_value = 0.0 - - jwt = DummyJwt('secret_key', 'issuer', algorithm='HS512', headers={}, payload={}) - - self.assertJwtsEqual( - jwt.to_jwt(), 'secret_key', - expected_headers={'typ': 'JWT', 'alg': 'HS512'}, - expected_payload={'iss': 'issuer', 'exp': 3600, 'nbf': 0}, - ) - - @patch('time.time') - def test_encode_override_algorithm(self, time_mock): - time_mock.return_value = 0.0 - - jwt = DummyJwt('secret_key', 'issuer', algorithm='HS256', headers={}, payload={}) - - self.assertJwtsEqual( - jwt.to_jwt(algorithm='HS512'), - 'secret_key', - expected_headers={'typ': 'JWT', 'alg': 'HS512'}, - expected_payload={'iss': 'issuer', 'exp': 3600, 'nbf': 0}, - ) - @patch('time.time') def test_encode_with_headers(self, time_mock): time_mock.return_value = 0.0 - jwt = DummyJwt('secret_key', 'issuer', algorithm='HS256', headers={'sooper': 'secret'}, - payload={}) + jwt = DummyJwt('secret_key', 'issuer', headers={'sooper': 'secret'}, payload={}) self.assertJwtsEqual( jwt.to_jwt(), 'secret_key', @@ -188,7 +166,7 @@ def test_encode_with_headers(self, time_mock): def test_encode_with_payload(self, time_mock): time_mock.return_value = 0.0 - jwt = DummyJwt('secret_key', 'issuer', algorithm='HS256', payload={'root': 'true'}) + jwt = DummyJwt('secret_key', 'issuer', payload={'root': 'true'}) self.assertJwtsEqual( jwt.to_jwt(), 'secret_key', @@ -208,10 +186,6 @@ def test_encode_with_payload_and_headers(self, time_mock): expected_payload={'iss': 'issuer', 'exp': 3600, 'nbf': 0, 'pay': 'me'}, ) - def test_encode_invalid_crypto_alg_fails(self): - jwt = DummyJwt('secret_key', 'issuer', algorithm='PlzDontTouchAlgorithm') - self.assertRaises(NotImplementedError, jwt.to_jwt) - def test_encode_no_key_fails(self): jwt = DummyJwt(None, 'issuer') self.assertRaises(ValueError, jwt.to_jwt) diff --git a/twilio/jwt/__init__.py b/twilio/jwt/__init__.py index 5e23100d52..b692470a9f 100644 --- a/twilio/jwt/__init__.py +++ b/twilio/jwt/__init__.py @@ -1,13 +1,4 @@ -import hmac -import sys - import jwt as jwt_lib - -try: - import json -except ImportError: - import simplejson as json - import time @@ -21,8 +12,9 @@ class JwtDecodeError(Exception): class Jwt(object): """Base class for building a Json Web Token""" GENERATE = object() + ALGORITHM = 'HS256' - def __init__(self, secret_key, issuer, subject=None, algorithm='HS256', nbf=GENERATE, + def __init__(self, secret_key, issuer, subject=None, algorithm=None, nbf=GENERATE, ttl=3600, valid_until=None): self.secret_key = secret_key """:type str: The secret used to encode the JWT""" @@ -30,7 +22,7 @@ def __init__(self, secret_key, issuer, subject=None, algorithm='HS256', nbf=GENE """:type str: The issuer of this JWT""" self.subject = subject """:type str: The subject of this JWT, omitted from payload by default""" - self.algorithm = algorithm + self.algorithm = algorithm or self.ALGORITHM """:type str: The algorithm used to encode the JWT, defaults to 'HS256'""" self.nbf = nbf """:type int: Time in secs since epoch before which this JWT is invalid. Defaults to now.""" @@ -99,10 +91,9 @@ def headers(self): headers['alg'] = self.algorithm return headers - def to_jwt(self, algorithm=None, ttl=None): + def to_jwt(self, ttl=None): """ Encode this JWT object into a JWT string - :param str algorithm: override the algorithm used to encode the JWT :param int ttl: override the ttl configured in the constructor :rtype: str The JWT string """ @@ -111,15 +102,12 @@ def to_jwt(self, algorithm=None, ttl=None): raise ValueError('JWT does not have a signing key configured.') headers = self.headers.copy() - if algorithm: - headers['alg'] = algorithm - algorithm = algorithm or self.algorithm payload = self.payload.copy() if ttl: payload['exp'] = int(time.time()) + ttl - return jwt_lib.encode(payload, self.secret_key, algorithm=algorithm, headers=headers) + return jwt_lib.encode(payload, self.secret_key, algorithm=self.algorithm, headers=headers) @classmethod def from_jwt(cls, jwt, key=''): @@ -134,8 +122,7 @@ def from_jwt(cls, jwt, key=''): verify = True if key else False try: - alg = jwt_lib.get_unverified_header(jwt).get("alg", "HS256") - payload = jwt_lib.decode(jwt, key, algorithms=[alg], options={ + payload = jwt_lib.decode(jwt, key, algorithms=[cls.ALGORITHM], options={ 'verify_signature': verify, 'verify_exp': True, 'verify_nbf': True, diff --git a/twilio/jwt/access_token/__init__.py b/twilio/jwt/access_token/__init__.py index f59f5cac0f..ce544e6684 100644 --- a/twilio/jwt/access_token/__init__.py +++ b/twilio/jwt/access_token/__init__.py @@ -20,6 +20,9 @@ def __str__(self): class AccessToken(Jwt): """Access Token containing one or more AccessTokenGrants used to access Twilio Resources""" + + ALGORITHM = 'HS256' + def __init__(self, account_sid, signing_key_sid, secret, grants=None, identity=None, nbf=Jwt.GENERATE, ttl=3600, valid_until=None, region=None): grants = grants or [] @@ -33,7 +36,7 @@ def __init__(self, account_sid, signing_key_sid, secret, grants=None, self.grants = grants super(AccessToken, self).__init__( secret_key=secret, - algorithm='HS256', + algorithm=self.ALGORITHM, issuer=signing_key_sid, subject=self.account_sid, nbf=nbf, diff --git a/twilio/jwt/client/__init__.py b/twilio/jwt/client/__init__.py index d1f8fe77ce..13520e21e9 100644 --- a/twilio/jwt/client/__init__.py +++ b/twilio/jwt/client/__init__.py @@ -6,6 +6,8 @@ class ClientCapabilityToken(Jwt): """A token to control permissions with Twilio Client""" + ALGORITHM = 'HS256' + def __init__(self, account_sid, auth_token, nbf=Jwt.GENERATE, ttl=3600, valid_until=None, **kwargs): """ @@ -20,7 +22,7 @@ def __init__(self, account_sid, auth_token, nbf=Jwt.GENERATE, ttl=3600, valid_un :returns: A new CapabilityToken with zero permissions """ super(ClientCapabilityToken, self).__init__( - algorithm='HS256', + algorithm=self.ALGORITHM, secret_key=auth_token, issuer=account_sid, nbf=nbf, diff --git a/twilio/jwt/taskrouter/__init__.py b/twilio/jwt/taskrouter/__init__.py index 3095ae73a7..bace17e437 100644 --- a/twilio/jwt/taskrouter/__init__.py +++ b/twilio/jwt/taskrouter/__init__.py @@ -5,6 +5,7 @@ class TaskRouterCapabilityToken(Jwt): VERSION = 'v1' DOMAIN = 'https://taskrouter.twilio.com' EVENTS_BASE_URL = 'https://event-bridge.twilio.com/v1/wschannels' + ALGORITHM = 'HS256' def __init__(self, account_sid, auth_token, workspace_sid, channel_id, **kwargs): """ @@ -28,7 +29,7 @@ def __init__(self, account_sid, auth_token, workspace_sid, channel_id, **kwargs) super(TaskRouterCapabilityToken, self).__init__( secret_key=auth_token, issuer=account_sid, - algorithm='HS256', + algorithm=self.ALGORITHM, nbf=kwargs.get('nbf', Jwt.GENERATE), ttl=kwargs.get('ttl', 3600), valid_until=kwargs.get('valid_until', None), @@ -131,4 +132,3 @@ def _validate_inputs(self, account_sid, workspace_sid, channel_id): def __str__(self): return ''.format(self.to_jwt()) - diff --git a/twilio/jwt/validation/__init__.py b/twilio/jwt/validation/__init__.py index 216b0015a6..a5536ebd49 100644 --- a/twilio/jwt/validation/__init__.py +++ b/twilio/jwt/validation/__init__.py @@ -6,6 +6,7 @@ class ClientValidationJwt(Jwt): """A JWT included on requests so that Twilio can verify request authenticity""" __CTY = 'twilio-pkrv;v=1' + ALGORITHM = 'RS256' def __init__(self, account_sid, api_key_sid, credential_sid, private_key, validation_payload): """ @@ -21,7 +22,7 @@ def __init__(self, account_sid, api_key_sid, credential_sid, private_key, valida secret_key=private_key, issuer=api_key_sid, subject=account_sid, - algorithm='RS256', + algorithm=self.ALGORITHM, ttl=300 # 5 minute ttl ) self.credential_sid = credential_sid From 2a323793568d322ad7fc0cb5793ebd05931e8d4a Mon Sep 17 00:00:00 2001 From: Karl Sutt Date: Sat, 17 Apr 2021 15:22:21 +0300 Subject: [PATCH 7/9] Ensure decoding and encoding algorithms match --- tests/unit/jwt/test_jwt.py | 4 ++++ twilio/jwt/__init__.py | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/unit/jwt/test_jwt.py b/tests/unit/jwt/test_jwt.py index 0af17b4d17..e9ac4f0c1b 100644 --- a/tests/unit/jwt/test_jwt.py +++ b/tests/unit/jwt/test_jwt.py @@ -210,6 +210,10 @@ def test_encode_decode(self): 'sick': 'sick', }, decoded_jwt.payload) + def test_encode_decode_mismatched_algorithms(self): + jwt = DummyJwt('secret_key', 'issuer', algorithm='HS512', subject='hey', payload={'sick': 'sick'}) + self.assertRaises(JwtDecodeError, Jwt.from_jwt, jwt.to_jwt()) + def test_decode_bad_secret(self): jwt = DummyJwt('secret_key', 'issuer') self.assertRaises(JwtDecodeError, Jwt.from_jwt, jwt.to_jwt(), 'letmeinplz') diff --git a/twilio/jwt/__init__.py b/twilio/jwt/__init__.py index b692470a9f..e8793865d2 100644 --- a/twilio/jwt/__init__.py +++ b/twilio/jwt/__init__.py @@ -122,12 +122,18 @@ def from_jwt(cls, jwt, key=''): verify = True if key else False try: + headers = jwt_lib.get_unverified_header(jwt) + + alg = headers.get('alg') + if alg != cls.ALGORITHM: + raise ValueError(f"Incorrect decoding algorithm {alg}, " + f"expecting {cls.ALGORITHM}.") + payload = jwt_lib.decode(jwt, key, algorithms=[cls.ALGORITHM], options={ 'verify_signature': verify, 'verify_exp': True, 'verify_nbf': True, }) - headers = jwt_lib.get_unverified_header(jwt) except Exception as e: raise JwtDecodeError(getattr(e, 'message', str(e))) From b7facc22d8355bcc2ddd533277fdf7d1c2d4d9c9 Mon Sep 17 00:00:00 2001 From: Jennifer Mah Date: Wed, 14 Jul 2021 16:17:43 -0700 Subject: [PATCH 8/9] rerun tests From fb847755d2ec943a4fd84f0c33ae9f7f4999095a Mon Sep 17 00:00:00 2001 From: Jennifer Mah Date: Thu, 15 Jul 2021 10:20:01 -0700 Subject: [PATCH 9/9] rerun PR tests