Skip to content

Commit babfe19

Browse files
author
Michael Davis
committed
Refactor JWS to allow string payloads
1 parent 1139f3b commit babfe19

File tree

9 files changed

+6912
-117
lines changed

9 files changed

+6912
-117
lines changed

jose/jwk.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11

2+
import base64
23
import hashlib
34
import hmac
5+
import struct
46
import six
57

8+
from builtins import int
9+
610
import Crypto.Hash.SHA256
711
import Crypto.Hash.SHA384
812
import Crypto.Hash.SHA512
@@ -60,6 +64,19 @@ def get_algorithm_object(algorithm):
6064
raise JWSError('Algorithm not supported: %s' % algorithm)
6165

6266

67+
def int_arr_to_long(arr):
68+
return int(''.join(["%02x" % byte for byte in arr]), 16)
69+
70+
71+
def base64_to_long(data):
72+
if isinstance(data, six.text_type):
73+
data = data.encode("ascii")
74+
75+
# urlsafe_b64decode will happily convert b64encoded data
76+
_d = base64.urlsafe_b64decode(bytes(data) + b'==')
77+
return int_arr_to_long(struct.unpack('%sB' % len(_d), _d))
78+
79+
6380
class Key(object):
6481
"""
6582
The interface for an JWK used to sign and verify tokens.
@@ -228,22 +245,13 @@ def process_prepare_key(self, key):
228245

229246
def process_jwk(self, jwk):
230247

231-
def urlsafe_b64decode(encoded):
232-
import base64
233-
if not encoded:
234-
return encoded
235-
modulo = len(encoded) % 4
236-
if modulo != 0:
237-
encoded += ('=' * (4 - modulo))
238-
return base64.b64decode(encoded)
239-
240248
if not jwk.get('kty') == 'RSA':
241249
raise JWKError("Incorrect key type. Expected: 'RSA', Recieved: %s" % jwk.get('kty'))
242250

243-
e = bytes(jwk.get('e', 256))
244-
n = bytes(jwk.get('n'))
251+
e = base64_to_long(jwk.get('e', 256))
252+
n = base64_to_long(jwk.get('n'))
245253

246-
return RSA.construct((long(n), long(e)))
254+
return RSA.construct((n, e))
247255

248256
def process_sign(self, msg, key):
249257
return PKCS1_v1_5.new(key).sign(self.hash_alg.new(msg))

jose/jws.py

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313
from jose.utils import base64url_decode
1414

1515

16-
def sign(claims, key, headers=None, algorithm=ALGORITHMS.HS256):
16+
def sign(payload, key, headers=None, algorithm=ALGORITHMS.HS256):
1717
"""Signs a claims set and returns a JWS string.
1818
1919
Args:
20-
claims (dict): A claims set to sign
20+
payload (str): A string to sign
2121
key (str): The key to use for signing the claim set
2222
headers (dict, optional): A set of headers that will be added to
2323
the default headers. Any headers that are added as additional
@@ -42,8 +42,8 @@ def sign(claims, key, headers=None, algorithm=ALGORITHMS.HS256):
4242
raise JWSError('Algorithm %s not supported.' % algorithm)
4343

4444
encoded_header = _encode_header(algorithm, additional_headers=headers)
45-
encoded_claims = _encode_claims(claims)
46-
signed_output = _sign_header_and_claims(encoded_header, encoded_claims, algorithm, key)
45+
encoded_payload = _encode_payload(payload)
46+
signed_output = _sign_header_and_claims(encoded_header, encoded_payload, algorithm, key)
4747

4848
return signed_output
4949

@@ -57,27 +57,27 @@ def verify(token, key, algorithms, verify=True):
5757
algorithms (str or list): Valid algorithms that should be used to verify the JWS.
5858
5959
Returns:
60-
dict: The dict representation of the claims set, assuming the signature is valid.
60+
str: The str representation of the payload, assuming the signature is valid.
6161
6262
Raises:
6363
JWSError: If there is an exception verifying a token.
6464
6565
Examples:
6666
67-
>>> payload = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhIjoiYiJ9.jiMyrsmD8AoHWeQgmxZ5yq8z0lXS67_QGs52AzC8Ru8'
68-
>>> jws.verify(payload, 'secret', algorithms='HS256')
67+
>>> token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhIjoiYiJ9.jiMyrsmD8AoHWeQgmxZ5yq8z0lXS67_QGs52AzC8Ru8'
68+
>>> jws.verify(token, 'secret', algorithms='HS256')
6969
7070
"""
7171

72-
header, claims, signing_input, signature = _load(token)
72+
header, payload, signing_input, signature = _load(token)
7373

7474
if verify:
75-
_verify_signature(claims, signing_input, header, signature, key, algorithms)
75+
_verify_signature(payload, signing_input, header, signature, key, algorithms)
7676

77-
return claims
77+
return payload
7878

7979

80-
def get_unverified_headers(token):
80+
def get_unverified_header(token):
8181
"""Returns the decoded headers without verification of any kind.
8282
8383
Args:
@@ -93,6 +93,24 @@ def get_unverified_headers(token):
9393
return header
9494

9595

96+
def get_unverified_headers(token):
97+
"""Returns the decoded headers without verification of any kind.
98+
99+
This is simply a wrapper of get_unverified_header() for backwards
100+
compatibility.
101+
102+
Args:
103+
token (str): A signed JWS to decode the headers from.
104+
105+
Returns:
106+
dict: The dict representation of the token headers.
107+
108+
Raises:
109+
JWSError: If there is an exception decoding the token.
110+
"""
111+
return get_unverified_header(token)
112+
113+
96114
def get_unverified_claims(token):
97115
"""Returns the decoded claims without verification of any kind.
98116
@@ -126,13 +144,17 @@ def _encode_header(algorithm, additional_headers=None):
126144
return base64url_encode(json_header)
127145

128146

129-
def _encode_claims(claims):
130-
json_payload = json.dumps(
131-
claims,
132-
separators=(',', ':'),
133-
).encode('utf-8')
147+
def _encode_payload(payload):
148+
if isinstance(payload, Mapping):
149+
try:
150+
payload = json.dumps(
151+
payload,
152+
separators=(',', ':'),
153+
).encode('utf-8')
154+
except ValueError:
155+
pass
134156

135-
return base64url_encode(json_payload)
157+
return base64url_encode(payload)
136158

137159

138160
def _sign_header_and_claims(encoded_header, encoded_claims, algorithm, key):
@@ -172,22 +194,16 @@ def _load(jwt):
172194
raise JWSError('Invalid header string: must be a json object')
173195

174196
try:
175-
claims_data = base64url_decode(claims_segment)
176-
claims = json.loads(claims_data.decode('utf-8'))
197+
payload = base64url_decode(claims_segment)
177198
except (TypeError, binascii.Error):
178199
raise JWSError('Invalid payload padding')
179-
except ValueError as e:
180-
raise JWSError('Invalid payload string: %s' % e)
181-
182-
if not isinstance(claims, Mapping):
183-
raise JWSError('Invalid payload string: must be a json object')
184200

185201
try:
186202
signature = base64url_decode(crypto_segment)
187203
except (TypeError, binascii.Error):
188204
raise JWSError('Invalid crypto padding')
189205

190-
return (header, claims, signing_input, signature)
206+
return (header, payload, signing_input, signature)
191207

192208

193209
def _verify_signature(payload, signing_input, header, signature, key='', algorithms=None):

jose/jwt.py

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11

2+
import binascii
3+
import json
4+
25
from calendar import timegm
6+
from collections import Mapping
37
from datetime import datetime
48
from datetime import timedelta
59
from six import string_types
@@ -108,14 +112,24 @@ def decode(token, key, algorithms=None, options=None, audience=None, issuer=None
108112
defaults.update(options)
109113

110114
verify_signature = defaults.get('verify_signature', True)
111-
token_info = jws.verify(token, key, algorithms, verify=verify_signature)
115+
payload = jws.verify(token, key, algorithms, verify=verify_signature)
112116

113-
_validate_claims(token_info, audience=audience, issuer=issuer, options=defaults)
117+
try:
118+
claims = json.loads(payload.decode('utf-8'))
119+
except (TypeError, binascii.Error):
120+
raise JWTError('Invalid payload padding')
121+
except ValueError as e:
122+
raise JWTError('Invalid payload string: %s' % e)
114123

115-
return token_info
124+
if not isinstance(claims, Mapping):
125+
raise JWTError('Invalid payload string: must be a json object')
116126

127+
_validate_claims(claims, audience=audience, issuer=issuer, options=defaults)
117128

118-
def get_unverified_headers(token):
129+
return claims
130+
131+
132+
def get_unverified_header(token):
119133
"""Returns the decoded headers without verification of any kind.
120134
121135
Args:
@@ -135,6 +149,24 @@ def get_unverified_headers(token):
135149
return headers
136150

137151

152+
def get_unverified_headers(token):
153+
"""Returns the decoded headers without verification of any kind.
154+
155+
This is simply a wrapper of get_unverified_header() for backwards
156+
compatibility.
157+
158+
Args:
159+
token (str): A signed JWT to decode the headers from.
160+
161+
Returns:
162+
dict: The dict representation of the token headers.
163+
164+
Raises:
165+
JWTError: If there is an exception decoding the token.
166+
"""
167+
return get_unverified_header(token)
168+
169+
138170
def get_unverified_claims(token):
139171
"""Returns the decoded claims without verification of any kind.
140172

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
pycrypto
22
six
3+
future

tests/algorithms/test_RSA.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -67,17 +67,6 @@ def alg():
6767

6868
class TestRSAAlgorithm:
6969

70-
def test_cookbook_access_token(self, alg):
71-
token = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZXhhbXBsZSJ9" \
72-
".SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IHlvdXIgZG9" \
73-
"vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdSBkb24ndCBrZWVwIHlvdXI" \
74-
"gZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcmUgeW91IG1pZ2h0IGJlIHN3ZXB0IG9" \
75-
"mZiB0by4.MRjdkly7_-oTPTS3AXP41iQIGKa80A0ZmTuV5MEaHoxnW2e5CZ5NlKtainoFmKZ" \
76-
"opdHM1O2U4mwzJdQx996ivp83xuglII7PNDi84wnB-BDkoBwA78185hX-Es4JIwmDLJK3lfW" \
77-
"Ra-XtL0RnltuYv746iYTh_qHRD68BNt1uSNCrUCTJDt5aAE6x8wW1Kt9eRo4QPocSadnHXFx" \
78-
"nt8Is9UzpERV0ePPQdLuW3IS_de3xyIrDaLGdjluPxUAhb6L2aXic1U12podGU0KLUQSE_oI" \
79-
"-ZnmKJ3F4uOZDnd6QZWJushZ41Axf_fcIe8u9ipH84ogoree7vjbU5y18kDquDg"
80-
8170
def test_RSA_key(self, alg):
8271
key = RSA.importKey(private_key)
8372
alg.prepare_key(key)

tests/rfc/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)