Skip to content

Commit d4c437e

Browse files
committed
Merge pull request jpadilla#25 from dystedium/split_decode
refactor decode(), fix setup.py for automated sdist builds
2 parents 3bade27 + c7fb448 commit d4c437e

File tree

3 files changed

+110
-23
lines changed

3 files changed

+110
-23
lines changed

jwt/__init__.py

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,16 @@ def encode(payload, key, algorithm='HS256'):
137137

138138

139139
def decode(jwt, key='', verify=True, verify_expiration=True, leeway=0):
140+
payload, signing_input, header, signature = load(jwt)
141+
142+
if verify:
143+
verify_signature(payload, signing_input, header, signature, key,
144+
verify_expiration, leeway)
145+
146+
return payload
147+
148+
149+
def load(jwt):
140150
if isinstance(jwt, unicode):
141151
jwt = jwt.encode('utf-8')
142152
try:
@@ -168,22 +178,25 @@ def decode(jwt, key='', verify=True, verify_expiration=True, leeway=0):
168178
except (TypeError, binascii.Error):
169179
raise DecodeError("Invalid crypto padding")
170180

171-
if verify:
172-
try:
173-
if isinstance(key, unicode):
174-
key = key.encode('utf-8')
175-
if header['alg'].startswith('HS'):
176-
expected = verify_methods[header['alg']](signing_input, key)
177-
if not constant_time_compare(signature, expected):
178-
raise DecodeError("Signature verification failed")
179-
else:
180-
if not verify_methods[header['alg']](signing_input, key, signature):
181-
raise DecodeError("Signature verification failed")
182-
except KeyError:
183-
raise DecodeError("Algorithm not supported")
184-
185-
if 'exp' in payload and verify_expiration:
186-
utc_timestamp = timegm(datetime.utcnow().utctimetuple())
187-
if payload['exp'] < (utc_timestamp - leeway):
188-
raise ExpiredSignature("Signature has expired")
189-
return payload
181+
return (payload, signing_input, header, signature)
182+
183+
184+
def verify_signature(payload, signing_input, header, signature, key='',
185+
verify_expiration=True, leeway=0):
186+
try:
187+
if isinstance(key, unicode):
188+
key = key.encode('utf-8')
189+
if header['alg'].startswith('HS'):
190+
expected = verify_methods[header['alg']](signing_input, key)
191+
if not constant_time_compare(signature, expected):
192+
raise DecodeError("Signature verification failed")
193+
else:
194+
if not verify_methods[header['alg']](signing_input, key, signature):
195+
raise DecodeError("Signature verification failed")
196+
except KeyError:
197+
raise DecodeError("Algorithm not supported")
198+
199+
if 'exp' in payload and verify_expiration:
200+
utc_timestamp = timegm(datetime.utcnow().utctimetuple())
201+
if payload['exp'] < (utc_timestamp - leeway):
202+
raise ExpiredSignature("Signature has expired")

setup.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
from setuptools import setup
44

55

6-
def read(fname):
7-
return open(os.path.join(os.path.dirname(__file__), fname)).read()
6+
with open(os.path.join(os.path.dirname(__file__), 'README.md')) as readme:
7+
long_description = readme.read()
8+
89

910
setup(
1011
name="PyJWT",
@@ -17,7 +18,7 @@ def read(fname):
1718
url="http://github.com/progrium/pyjwt",
1819
packages=['jwt'],
1920
scripts=['bin/jwt'],
20-
long_description=read('README.md'),
21+
long_description=long_description,
2122
classifiers=[
2223
"Development Status :: 3 - Alpha",
2324
"License :: OSI Approved :: MIT License",

tests/test_jwt.py

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,35 +62,70 @@ def test_decodes_valid_jwt(self):
6262
decoded_payload = jwt.decode(example_jwt, example_secret)
6363
self.assertEqual(decoded_payload, example_payload)
6464

65+
def test_load_verify_valid_jwt(self):
66+
example_payload = {"hello": "world"}
67+
example_secret = "secret"
68+
example_jwt = (
69+
b"eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9"
70+
b".eyJoZWxsbyI6ICJ3b3JsZCJ9"
71+
b".tvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8")
72+
decoded_payload, signing_input, header, signature = jwt.load(example_jwt)
73+
jwt.verify_signature(decoded_payload, signing_input, header, signature, example_secret)
74+
self.assertEqual(decoded_payload, example_payload)
75+
6576
def test_allow_skip_verification(self):
6677
right_secret = 'foo'
6778
jwt_message = jwt.encode(self.payload, right_secret)
6879
decoded_payload = jwt.decode(jwt_message, verify=False)
6980
self.assertEqual(decoded_payload, self.payload)
7081

82+
def test_load_no_verification(self):
83+
right_secret = 'foo'
84+
jwt_message = jwt.encode(self.payload, right_secret)
85+
decoded_payload, signing_input, header, signature = jwt.load(jwt_message)
86+
self.assertEqual(decoded_payload, self.payload)
87+
7188
def test_no_secret(self):
7289
right_secret = 'foo'
7390
jwt_message = jwt.encode(self.payload, right_secret)
7491

7592
with self.assertRaises(jwt.DecodeError):
7693
jwt.decode(jwt_message)
7794

95+
def test_verify_signature_no_secret(self):
96+
right_secret = 'foo'
97+
jwt_message = jwt.encode(self.payload, right_secret)
98+
decoded_payload, signing_input, header, signature = jwt.load(jwt_message)
99+
100+
with self.assertRaises(jwt.DecodeError):
101+
jwt.verify_signature(decoded_payload, signing_input, header, signature)
102+
78103
def test_invalid_crypto_alg(self):
79104
self.assertRaises(NotImplementedError, jwt.encode, self.payload,
80105
"secret", "HS1024")
81106

82107
def test_unicode_secret(self):
83108
secret = u'\xc2'
84109
jwt_message = jwt.encode(self.payload, secret)
110+
85111
decoded_payload = jwt.decode(jwt_message, secret)
86112
self.assertEqual(decoded_payload, self.payload)
87113

114+
decoded_payload, signing_input, header, signature = jwt.load(jwt_message)
115+
jwt.verify_signature(decoded_payload, signing_input, header, signature, secret)
116+
self.assertEqual(decoded_payload, self.payload)
117+
88118
def test_nonascii_secret(self):
89119
secret = '\xc2' # char value that ascii codec cannot decode
90120
jwt_message = jwt.encode(self.payload, secret)
121+
91122
decoded_payload = jwt.decode(jwt_message, secret)
92123
self.assertEqual(decoded_payload, self.payload)
93124

125+
decoded_payload, signing_input, header, signature = jwt.load(jwt_message)
126+
jwt.verify_signature(decoded_payload, signing_input, header, signature, secret)
127+
self.assertEqual(decoded_payload, self.payload)
128+
94129
def test_decode_unicode_value(self):
95130
example_payload = {"hello": "world"}
96131
example_secret = "secret"
@@ -100,13 +135,17 @@ def test_decode_unicode_value(self):
100135
".tvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8")
101136
decoded_payload = jwt.decode(example_jwt, example_secret)
102137
self.assertEqual(decoded_payload, example_payload)
138+
decoded_payload, signing_input, header, signature = jwt.load(example_jwt)
139+
self.assertEqual(decoded_payload, example_payload)
103140

104141
def test_decode_invalid_header_padding(self):
105142
example_jwt = (
106143
"aeyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9"
107144
".eyJoZWxsbyI6ICJ3b3JsZCJ9"
108145
".tvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8")
109146
example_secret = "secret"
147+
with self.assertRaises(jwt.DecodeError):
148+
jwt.load(example_jwt)
110149
with self.assertRaises(jwt.DecodeError):
111150
jwt.decode(example_jwt, example_secret)
112151

@@ -116,6 +155,8 @@ def test_decode_invalid_header_string(self):
116155
".eyJoZWxsbyI6ICJ3b3JsZCJ9"
117156
".tvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8")
118157
example_secret = "secret"
158+
with self.assertRaisesRegexp(jwt.DecodeError, "Invalid header string"):
159+
jwt.load(example_jwt)
119160
with self.assertRaisesRegexp(jwt.DecodeError, "Invalid header string"):
120161
jwt.decode(example_jwt, example_secret)
121162

@@ -125,6 +166,8 @@ def test_decode_invalid_payload_padding(self):
125166
".aeyJoZWxsbyI6ICJ3b3JsZCJ9"
126167
".tvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8")
127168
example_secret = "secret"
169+
with self.assertRaises(jwt.DecodeError):
170+
jwt.load(example_jwt)
128171
with self.assertRaises(jwt.DecodeError):
129172
jwt.decode(example_jwt, example_secret)
130173

@@ -134,6 +177,9 @@ def test_decode_invalid_payload_string(self):
134177
".eyJoZWxsb-kiOiAid29ybGQifQ=="
135178
".tvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8")
136179
example_secret = "secret"
180+
with self.assertRaisesRegexp(jwt.DecodeError,
181+
"Invalid payload string"):
182+
jwt.load(example_jwt)
137183
with self.assertRaisesRegexp(jwt.DecodeError,
138184
"Invalid payload string"):
139185
jwt.decode(example_jwt, example_secret)
@@ -144,34 +190,52 @@ def test_decode_invalid_crypto_padding(self):
144190
".eyJoZWxsbyI6ICJ3b3JsZCJ9"
145191
".aatvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8")
146192
example_secret = "secret"
193+
with self.assertRaises(jwt.DecodeError):
194+
jwt.load(example_jwt)
147195
with self.assertRaises(jwt.DecodeError):
148196
jwt.decode(example_jwt, example_secret)
149197

150198
def test_decode_with_expiration(self):
151199
self.payload['exp'] = utc_timestamp() - 1
152200
secret = 'secret'
153201
jwt_message = jwt.encode(self.payload, secret)
202+
154203
with self.assertRaises(jwt.ExpiredSignature):
155204
jwt.decode(jwt_message, secret)
156205

206+
decoded_payload, signing_input, header, signature = jwt.load(jwt_message)
207+
with self.assertRaises(jwt.ExpiredSignature):
208+
jwt.verify_signature(decoded_payload, signing_input, header, signature, secret)
209+
157210
def test_decode_skip_expiration_verification(self):
158211
self.payload['exp'] = time.time() - 1
159212
secret = 'secret'
160213
jwt_message = jwt.encode(self.payload, secret)
214+
161215
jwt.decode(jwt_message, secret, verify_expiration=False)
162216

217+
decoded_payload, signing_input, header, signature = jwt.load(jwt_message)
218+
jwt.verify_signature(decoded_payload, signing_input, header, signature, secret, verify_expiration=False)
219+
163220
def test_decode_with_expiration_with_leeway(self):
164221
self.payload['exp'] = utc_timestamp() - 2
165222
secret = 'secret'
166223
jwt_message = jwt.encode(self.payload, secret)
167224

225+
decoded_payload, signing_input, header, signature = jwt.load(jwt_message)
226+
168227
# With 3 seconds leeway, should be ok
169228
jwt.decode(jwt_message, secret, leeway=3)
170229

171-
# With 1 secondes, should fail
230+
jwt.verify_signature(decoded_payload, signing_input, header, signature, secret, leeway=3)
231+
232+
# With 1 second, should fail
172233
with self.assertRaises(jwt.ExpiredSignature):
173234
jwt.decode(jwt_message, secret, leeway=1)
174235

236+
with self.assertRaises(jwt.ExpiredSignature):
237+
jwt.verify_signature(decoded_payload, signing_input, header, signature, secret, leeway=1)
238+
175239
def test_encode_decode_with_rsa_sha256(self):
176240
try:
177241
from Crypto.PublicKey import RSA
@@ -183,6 +247,9 @@ def test_encode_decode_with_rsa_sha256(self):
183247
with open('tests/testkey.pub','r') as rsa_pub_file:
184248
pub_rsakey = RSA.importKey(rsa_pub_file.read())
185249
assert jwt.decode(jwt_message, pub_rsakey)
250+
251+
load_output = jwt.load(jwt_message)
252+
jwt.verify_signature(key=pub_rsakey, *load_output)
186253
except ImportError:
187254
pass
188255

@@ -197,6 +264,9 @@ def test_encode_decode_with_rsa_sha384(self):
197264
with open('tests/testkey.pub','r') as rsa_pub_file:
198265
pub_rsakey = RSA.importKey(rsa_pub_file.read())
199266
assert jwt.decode(jwt_message, pub_rsakey)
267+
268+
load_output = jwt.load(jwt_message)
269+
jwt.verify_signature(key=pub_rsakey, *load_output)
200270
except ImportError:
201271
pass
202272

@@ -211,6 +281,9 @@ def test_encode_decode_with_rsa_sha512(self):
211281
with open('tests/testkey.pub','r') as rsa_pub_file:
212282
pub_rsakey = RSA.importKey(rsa_pub_file.read())
213283
assert jwt.decode(jwt_message, pub_rsakey)
284+
285+
load_output = jwt.load(jwt_message)
286+
jwt.verify_signature(key=pub_rsakey, *load_output)
214287
except ImportError:
215288
pass
216289

0 commit comments

Comments
 (0)