diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4d7654df..c9df8d21 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,6 +17,8 @@ Major `#121 `_ * Make pyca/cryptography backend the preferred backend if multiple backends are present. `#122 `_ +* Allow for headless JWT by sorting headers when serializing. + `#136 `_ Bugfixes """""""" diff --git a/jose/jws.py b/jose/jws.py index b2a75fde..9d2fc0de 100644 --- a/jose/jws.py +++ b/jose/jws.py @@ -144,6 +144,7 @@ def _encode_header(algorithm, additional_headers=None): json_header = json.dumps( header, separators=(',', ':'), + sort_keys=True, ).encode('utf-8') return base64url_encode(json_header) diff --git a/tests/test_jwt.py b/tests/test_jwt.py index 01409545..9ceb2391 100644 --- a/tests/test_jwt.py +++ b/tests/test_jwt.py @@ -107,6 +107,33 @@ def test_non_default_headers(self, claims, key, headers): for k, v in headers.items(): assert all_headers[k] == v + def test_deterministic_headers(self): + from collections import OrderedDict + from jose.utils import base64url_decode + + claims = {"a": "b"} + key = "secret" + + headers1 = OrderedDict(( + ('kid', 'my-key-id'), + ('another_key', 'another_value'), + )) + encoded1 = jwt.encode(claims, key, algorithm='HS256', headers=headers1) + encoded_headers1 = encoded1.split('.', 1)[0] + + headers2 = OrderedDict(( + ('another_key', 'another_value'), + ('kid', 'my-key-id'), + )) + encoded2 = jwt.encode(claims, key, algorithm='HS256', headers=headers2) + encoded_headers2 = encoded2.split('.', 1)[0] + + assert encoded_headers1 == encoded_headers2 + + # manually decode header to compare it to known good + decoded_headers1 = base64url_decode(encoded_headers1.encode('utf-8')) + assert decoded_headers1 == b"""{"alg":"HS256","another_key":"another_value","kid":"my-key-id","typ":"JWT"}""" + def test_encode(self, claims, key): expected = (