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)

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 = (

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,


tests/test_jwt.py::TestJWT::test_deterministic_headers
tests/test_jwt.py::TestJWT::test_no_alg tests/test_jwt.py::TestJWT::test_non_default_alg tests/test_jwt.py::TestJWT::test_non_default_alg_positional_bwcompat tests/test_jwt.py::TestJWT::test_no_alg_default_headers tests/test_jwt.py::TestJWT::test_non_default_headers tests/test_jwt.py::TestJWT::test_encode tests/test_jwt.py::TestJWT::test_decode tests/test_jwt.py::TestJWT::test_leeway_is_int tests/test_jwt.py::TestJWT::test_leeway_is_timedelta tests/test_jwt.py::TestJWT::test_iat_not_int tests/test_jwt.py::TestJWT::test_nbf_not_int tests/test_jwt.py::TestJWT::test_nbf_datetime tests/test_jwt.py::TestJWT::test_nbf_with_leeway tests/test_jwt.py::TestJWT::test_nbf_in_future tests/test_jwt.py::TestJWT::test_nbf_skip tests/test_jwt.py::TestJWT::test_exp_not_int tests/test_jwt.py::TestJWT::test_exp_datetime tests/test_jwt.py::TestJWT::test_exp_with_leeway tests/test_jwt.py::TestJWT::test_exp_in_past tests/test_jwt.py::TestJWT::test_exp_skip tests/test_jwt.py::TestJWT::test_aud_string tests/test_jwt.py::TestJWT::test_aud_list tests/test_jwt.py::TestJWT::test_aud_list_multiple tests/test_jwt.py::TestJWT::test_aud_list_is_strings tests/test_jwt.py::TestJWT::test_aud_case_sensitive tests/test_jwt.py::TestJWT::test_aud_empty_claim tests/test_jwt.py::TestJWT::test_aud_not_string_or_list tests/test_jwt.py::TestJWT::test_aud_given_number tests/test_jwt.py::TestJWT::test_iss_string tests/test_jwt.py::TestJWT::test_iss_list tests/test_jwt.py::TestJWT::test_iss_tuple tests/test_jwt.py::TestJWT::test_iss_invalid tests/test_jwt.py::TestJWT::test_sub_string tests/test_jwt.py::TestJWT::test_sub_invalid tests/test_jwt.py::TestJWT::test_sub_correct tests/test_jwt.py::TestJWT::test_sub_incorrect tests/test_jwt.py::TestJWT::test_jti_string tests/test_jwt.py::TestJWT::test_jti_invalid tests/test_jwt.py::TestJWT::test_at_hash tests/test_jwt.py::TestJWT::test_at_hash_invalid tests/test_jwt.py::TestJWT::test_at_hash_missing_access_token tests/test_jwt.py::TestJWT::test_at_hash_missing_claim tests/test_jwt.py::TestJWT::test_at_hash_unable_to_calculate tests/test_jwt.py::TestJWT::test_bad_claims tests/test_jwt.py::TestJWT::test_unverified_claims_string tests/test_jwt.py::TestJWT::test_unverified_claims_list tests/test_jwt.py::TestJWT::test_unverified_claims_object