Skip to content

Commit b7d3871

Browse files
author
Michael Davis
authored
Merge pull request #29 from bjmc/support_jwk_sets
Adds support for JWK Sets
2 parents 048377d + 6ed322d commit b7d3871

File tree

2 files changed

+108
-11
lines changed

2 files changed

+108
-11
lines changed

jose/jws.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import json
44
import six
55

6-
from collections import Mapping
6+
from collections import Mapping, Iterable
77

88
from jose import jwk
99
from jose.constants import ALGORITHMS
@@ -205,6 +205,27 @@ def _load(jwt):
205205
return (header, payload, signing_input, signature)
206206

207207

208+
def _sig_matches_keys(keys, signing_input, signature, alg):
209+
for key in keys:
210+
key = jwk.construct(key, alg)
211+
if key.verify(signing_input, signature):
212+
return True
213+
return False
214+
215+
216+
def _get_keys(key):
217+
if 'keys' in key: # JWK Set per RFC 7517
218+
if not isinstance(key, Mapping): # Caller didn't JSON-decode
219+
key = json.loads(key)
220+
return key['keys']
221+
# Iterable but not text or mapping => list- or tuple-like
222+
elif (isinstance(key, Iterable) and
223+
not (isinstance(key, six.string_types) or isinstance(key, Mapping))):
224+
return key
225+
else: # Scalar value, wrap in list.
226+
return [key]
227+
228+
208229
def _verify_signature(signing_input, header, signature, key='', algorithms=None):
209230

210231
alg = header.get('alg')
@@ -214,12 +235,10 @@ def _verify_signature(signing_input, header, signature, key='', algorithms=None)
214235
if algorithms is not None and alg not in algorithms:
215236
raise JWSError('The specified alg value is not allowed')
216237

238+
keys = _get_keys(key)
217239
try:
218-
key = jwk.construct(key, alg)
219-
220-
if not key.verify(signing_input, signature):
240+
if not _sig_matches_keys(keys, signing_input, signature, alg):
221241
raise JWSSignatureError()
222-
223242
except JWSSignatureError:
224243
raise JWSError('Signature verification failed.')
225244
except JWSError:

tests/test_jws.py

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12

23
from jose import jws
34
from jose.constants import ALGORITHMS
@@ -12,7 +13,7 @@ def payload():
1213
return payload
1314

1415

15-
class TestJWS:
16+
class TestJWS(object):
1617

1718
def test_unicode_token(self):
1819
token = u'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhIjoiYiJ9.jiMyrsmD8AoHWeQgmxZ5yq8z0lXS67_QGs52AzC8Ru8'
@@ -48,7 +49,7 @@ def test_invalid_key(self, payload):
4849
jws.sign(payload, 'secret', algorithm='RS256')
4950

5051

51-
class TestHMAC:
52+
class TestHMAC(object):
5253

5354
def testHMAC256(self, payload):
5455
token = jws.sign(payload, 'secret', algorithm=ALGORITHMS.HS256)
@@ -160,8 +161,85 @@ def test_add_headers(self, payload):
160161
Ks3IHH7tVltM6NsRk3jNdVMCAwEAAQ==
161162
-----END PUBLIC KEY-----"""
162163

163-
164-
class TestRSA:
164+
@pytest.fixture
165+
def jwk_set():
166+
return {u'keys': [{u'alg': u'RS256',
167+
u'e': u'AQAB',
168+
u'kid': u'40aa42edac0614d7ca3f57f97ee866cdfba3b61a',
169+
u'kty': u'RSA',
170+
u'n': u'6lm9AEGLPFpVqnfeVFuTIZsj7vz_kxla6uW1WWtosM_MtIjXkyyiSolxiSOs3bzG66iVm71023QyOzKYFbio0hI-yZauG3g9nH-zb_AHScsjAKagHtrHmTdtq0JcNkQnAaaUwxVbjwMlYAcOh87W5jWj_MAcPvc-qjy8-WJ81UgoOUZNiKByuF4-9igxKZeskGRXuTPX64kWGBmKl-tM7VnCGMKoK3m92NPrktfBoNN_EGGthNfQsKFUdQFJFtpMuiXp9Gib7dcMGabxcG2GUl-PU086kPUyUdUYiMN2auKSOxSUZgDjT7DcI8Sn8kdQ0-tImaHi54JNa1PNNdKRpw',
171+
u'use': u'sig'},
172+
{u'alg': u'RS256',
173+
u'e': u'AQAB',
174+
u'kid': u'8fbbeea40332d2c0d27e37e1904af29b64594e57',
175+
u'kty': u'RSA',
176+
u'n': u'z7h6_rt35-j6NV2iQvYIuR3xvsxmEImgMl8dc8CFl4SzEWrry3QILajKxQZA9YYYfXIcZUG_6R6AghVMJetNIl2AhCoEr3RQjjNsm9PE6h5p2kQ-zIveFeb__4oIkVihYtxtoYBSdVj69nXLUAJP2bxPfU8RDp5X7hT62pKR05H8QLxH8siIQ5qR2LGFw_dJcitAVRRQofuaj_9u0CLZBfinqyRkBc7a0zi7pBxtEiIbn9sRr8Kkb_Boap6BHbnLS-YFBVarcgFBbifRf7NlK5dqE9z4OUb-dx8wCMRIPVAx_hV4Qx2anTgp1sDA6V4vd4NaCOZX-mSctNZqQmKtNw',
177+
u'use': u'sig'},
178+
{u'alg': u'RS256',
179+
u'e': u'AQAB',
180+
u'kid': u'6758b0b8eb341e90454860432d6a1648bf4de03b',
181+
u'kty': u'RSA',
182+
u'n': u'5K0rYaA7xtqSe1nFn_nCA10uUXY81NcohMeFsYLbBlx_NdpsmbpgtXJ6ektYR7rUdtMMLu2IONlNhkWlx-lge91okyacUrWHP88PycilUE-RnyVjbPEm3seR0VefgALfN4y_e77ljq2F7W2_kbUkTvDzriDIWvQT0WwVF5FIOBydfDDs92S-queaKgLBwt50SXJCZryLew5ODrwVsFGI4Et6MLqjS-cgWpCNwzcRqjBRsse6DXnex_zSRII4ODzKIfX4qdFBKZHO_BkTsK9DNkUayrr9cz8rFRK6TEH6XTVabgsyd6LP6PTxhpiII_pTYRSWk7CGMnm2nO0dKxzaFQ',
183+
u'use': u'sig'}]}
184+
185+
google_id_token = (
186+
'eyJhbGciOiJSUzI1NiIsImtpZCI6IjhmYmJlZWE0MDMzMmQyYzBkMjdlMzdlMTkwN'
187+
'GFmMjliNjQ1OTRlNTcifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5'
188+
'jb20iLCJhdF9oYXNoIjoiUUY5RnRjcHlmbUFBanJuMHVyeUQ5dyIsImF1ZCI6IjQw'
189+
'NzQwODcxODE5Mi5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsInN1YiI6IjEwN'
190+
'zkzMjQxNjk2NTIwMzIzNDA3NiIsImF6cCI6IjQwNzQwODcxODE5Mi5hcHBzLmdvb2'
191+
'dsZXVzZXJjb250ZW50LmNvbSIsImlhdCI6MTQ2ODYyMjQ4MCwiZXhwIjoxNDY4NjI'
192+
'2MDgwfQ.Nz6VREh7smvfVRWNHlbKZ6W_DX57akRUGrDTcns06ndAwrslwUlBeFsWY'
193+
'RLon_tDw0QCeQCGvw7l1AT440UQBRP-mtqK_2Yny2JmIQ7Ll6UAIHRhXOD1uj9w5v'
194+
'X0jyI1MbjDtODeDWWn_9EDJRBd4xmwKhAONuWodTgSi7qGe1UVmzseFNNkKdoo54d'
195+
'XhCJiyiRAMnWB_FQDveRJghche131pd9O_E4Wj6hf_zCcMTaDaLDOmElcQe-WsKWA'
196+
'A3YwHFEWOLO_7x6u4uGmhItPGH7zsOTzYxPYhZMSZusgVg9fbE1kSlHVSyQrcp_rR'
197+
'WNz7vOIbvIlBR9Jrq5MIqbkkg'
198+
)
199+
200+
201+
class TestGetKeys(object):
202+
203+
def test_dict(self):
204+
assert [{}] == jws._get_keys({})
205+
206+
def test_custom_object(self):
207+
class MyDict(dict):
208+
pass
209+
mydict = MyDict()
210+
assert [mydict] == jws._get_keys(mydict)
211+
212+
def test_RFC7517_string(self):
213+
key = '{"keys": [{}, {}]}'
214+
assert [{}, {}] == jws._get_keys(key)
215+
216+
def test_RFC7517_mapping(self):
217+
key = {"keys": [{}, {}]}
218+
assert [{}, {}] == jws._get_keys(key)
219+
220+
def test_string(self):
221+
assert ['test'] == jws._get_keys('test')
222+
223+
def test_tuple(self):
224+
assert ('test', 'key') == jws._get_keys(('test', 'key'))
225+
226+
def test_list(self):
227+
assert ['test', 'key'] == jws._get_keys(['test', 'key'])
228+
229+
230+
class TestRSA(object):
231+
232+
def test_jwk_set(self, jwk_set):
233+
# Would raise a JWSError if validation failed.
234+
payload = jws.verify(google_id_token, jwk_set, ALGORITHMS.RS256)
235+
iss = json.loads(payload.decode('utf-8'))['iss']
236+
assert iss == "https://accounts.google.com"
237+
238+
def test_jwk_set_failure(self, jwk_set):
239+
# Remove the key that was used to sign this token.
240+
del jwk_set['keys'][1]
241+
with pytest.raises(JWSError):
242+
payload = jws.verify(google_id_token, jwk_set, ALGORITHMS.RS256)
165243

166244
def test_RSA256(self, payload):
167245
token = jws.sign(payload, rsa_private_key, algorithm=ALGORITHMS.RS256)
@@ -201,7 +279,7 @@ def test_wrong_key(self, payload):
201279
-----END PUBLIC KEY-----"""
202280

203281

204-
class TestEC:
282+
class TestEC(object):
205283

206284
def test_EC256(self, payload):
207285
token = jws.sign(payload, ec_private_key, algorithm=ALGORITHMS.ES256)
@@ -221,7 +299,7 @@ def test_wrong_alg(self, payload):
221299
jws.verify(token, rsa_public_key, ALGORITHMS.ES384)
222300

223301

224-
class TestLoad:
302+
class TestLoad(object):
225303

226304
def test_header_not_mapping(self):
227305
token = 'WyJ0ZXN0Il0.eyJhIjoiYiJ9.jiMyrsmD8AoHWeQgmxZ5yq8z0lXS67_QGs52AzC8Ru8'

0 commit comments

Comments
 (0)