Repository: mpdavis/python-jose

Issue:
Use OrderedDict for headers in jwt.decode and jws._encode_header
Hello,

I would implement the "headless JWT" idea I rode in this article https://dev.to/neilmadden/7-best-practices-for-json-web-tokens the section 4 "Consider using "headless" JWTs" .

But the fact python-jose build the headers from dict make this impossible. Indeed, keys don't have an order in a classic dict. Could you consider to use OrderedDict instead, in jws.py l.131 and jwt.py l.114 ?

If this idea interests you, I could make a pull request.

Best,



Discussion:

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 4d7654d..c9df8d2 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -17,6 +17,8 @@ Major
   `#121 <https://github.com/mpdavis/python-jose/pull/121>`_
 * Make pyca/cryptography backend the preferred backend if multiple backends are present.
   `#122 <https://github.com/mpdavis/python-jose/pull/122>`_
+* Allow for headless JWT by sorting headers when serializing.
+  `#136 <https://github.com/mpdavis/python-jose/pull/136>`_
 
 Bugfixes
 """"""""
diff --git a/jose/jws.py b/jose/jws.py
index b2a75fd..9d2fc0d 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)


Test patch:
diff --git a/tests/test_jwt.py b/tests/test_jwt.py
index 0140954..9ceb239 100644
--- a/tests/test_jwt.py
+++ b/tests/test_jwt.py
@@ -107,6 +107,33 @@ class TestJWT:
         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 = (