From 0d95d225a17d3ad39e26600999b7f8abbded759e Mon Sep 17 00:00:00 2001 From: "Ajitomi, Daisuke" Date: Wed, 7 Apr 2021 21:25:42 +0900 Subject: [PATCH 01/10] Make typ optional. --- jwt/api_jws.py | 9 ++++---- jwt/api_jwt.py | 3 ++- tests/test_api_jws.py | 49 +++++++++++++++++++++++++++++++++++++++++++ tests/test_api_jwt.py | 25 ++++++++++++++++++++++ 4 files changed, 81 insertions(+), 5 deletions(-) diff --git a/jwt/api_jws.py b/jwt/api_jws.py index 6d136999..c5ecb8eb 100644 --- a/jwt/api_jws.py +++ b/jwt/api_jws.py @@ -19,8 +19,6 @@ class PyJWS: - header_typ = "JWT" - def __init__(self, algorithms=None, options=None): self._algorithms = get_default_algorithms() self._valid_algs = ( @@ -78,6 +76,7 @@ def encode( payload: bytes, key: str, algorithm: str = "HS256", + typ: str = "JWT", headers: Optional[Dict] = None, json_encoder: Optional[Type[json.JSONEncoder]] = None, ) -> str: @@ -90,8 +89,10 @@ def encode( pass # Header - header = {"typ": self.header_typ, "alg": algorithm} - + header = {"alg": algorithm} + # if isinstance(typ, str) and len(typ) > 0: + if typ: + header["typ"] = typ if headers: self._validate_headers(headers) header.update(headers) diff --git a/jwt/api_jwt.py b/jwt/api_jwt.py index 70a5e537..aaa979ec 100644 --- a/jwt/api_jwt.py +++ b/jwt/api_jwt.py @@ -39,6 +39,7 @@ def encode( payload: Dict[str, Any], key: str, algorithm: str = "HS256", + typ: str = "JWT", headers: Optional[Dict] = None, json_encoder: Optional[Type[json.JSONEncoder]] = None, ) -> str: @@ -60,7 +61,7 @@ def encode( payload, separators=(",", ":"), cls=json_encoder ).encode("utf-8") - return api_jws.encode(json_payload, key, algorithm, headers, json_encoder) + return api_jws.encode(json_payload, key, algorithm, typ, headers, json_encoder) def decode_complete( self, diff --git a/tests/test_api_jws.py b/tests/test_api_jws.py index cbebb1f2..e8448ed8 100644 --- a/tests/test_api_jws.py +++ b/tests/test_api_jws.py @@ -624,6 +624,55 @@ def test_encode_headers_parameter_adds_headers(self, jws, payload): assert "testheader" in header_obj assert header_obj["testheader"] == headers["testheader"] + def test_encode_with_typ(self, jws): + payload = """ + { + "iss": "https://scim.example.com", + "iat": 1458496404, + "jti": "4d3559ec67504aaba65d40b0363faad8", + "aud": [ + "https://scim.example.com/Feeds/98d52461fa5bbc879593b7754", + "https://scim.example.com/Feeds/5d7604516b1d08641d7676ee7" + ], + "events": { + "urn:ietf:params:scim:event:create": { + "ref": + "https://scim.example.com/Users/44f6142df96bd6ab61e7521d9", + "attributes": ["id", "name", "userName", "password", "emails"] + } + } + } + """ + token = jws.encode(payload.encode("utf-8"), "secret", typ="secevent+jwt") + + header = token[0 : token.index(".")].encode() + header = base64url_decode(header) + header_obj = json.loads(header) + + assert "typ" in header_obj + assert header_obj["typ"] == "secevent+jwt" + + def test_encode_with_typ_empty_string(self, jws, payload): + token = jws.encode(payload, "secret", typ="") + + header = token[0 : token.index(".")].encode() + header = base64url_decode(header) + header_obj = json.loads(header) + + assert "typ" not in header_obj + + def test_encode_with_typ_and_headers_include_typ(self, jws, payload): + headers = {"typ": "a"} + token = jws.encode(payload, "secret", typ="b", headers=headers) + + header = token[0 : token.index(".")].encode() + header = base64url_decode(header) + header_obj = json.loads(header) + + assert "typ" in header_obj + # typ in headers overwrites typ parameter. + assert header_obj["typ"] == "a" + def test_encode_fails_on_invalid_kid_types(self, jws, payload): with pytest.raises(InvalidTokenError) as exc: jws.encode(payload, "secret", headers={"kid": 123}) diff --git a/tests/test_api_jwt.py b/tests/test_api_jwt.py index 05cb714d..96889218 100644 --- a/tests/test_api_jwt.py +++ b/tests/test_api_jwt.py @@ -16,6 +16,7 @@ InvalidIssuerError, MissingRequiredClaimError, ) +from jwt.utils import base64url_decode from .utils import crypto_required, key_path, utc_timestamp @@ -167,6 +168,30 @@ def test_encode_bad_type(self, jwt): lambda: jwt.encode(t, "secret", algorithms=["HS256"]), ) + def test_encode_with_typ(self, jwt): + payload = { + "iss": "https://scim.example.com", + "iat": 1458496404, + "jti": "4d3559ec67504aaba65d40b0363faad8", + "aud": [ + "https://scim.example.com/Feeds/98d52461fa5bbc879593b7754", + "https://scim.example.com/Feeds/5d7604516b1d08641d7676ee7", + ], + "events": { + "urn:ietf:params:scim:event:create": { + "ref": "https://scim.example.com/Users/44f6142df96bd6ab61e7521d9", + "attributes": ["id", "name", "userName", "password", "emails"], + } + }, + } + token = jwt.encode(payload, "secret", algorithm="HS256", typ="secevent+jwt") + header = token[0 : token.index(".")].encode() + header = base64url_decode(header) + header_obj = json.loads(header) + + assert "typ" in header_obj + assert header_obj["typ"] == "secevent+jwt" + def test_decode_raises_exception_if_exp_is_not_int(self, jwt): # >>> jwt.encode({'exp': 'not-an-int'}, 'secret') example_jwt = ( From 0237d31a4d6af51eacad97a033e5c7204799f859 Mon Sep 17 00:00:00 2001 From: "Ajitomi, Daisuke" Date: Wed, 7 Apr 2021 21:36:00 +0900 Subject: [PATCH 02/10] Update doc. --- docs/api.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index 266082ae..c000b7d1 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -3,7 +3,7 @@ API Reference .. module:: jwt -.. function:: encode(payload, key, algorithm="HS256", headers=None, json_encoder=None) +.. function:: encode(payload, key, algorithm="HS256", typ="JWT", headers=None, json_encoder=None) Encode the ``payload`` as JSON Web Token. @@ -14,6 +14,7 @@ API Reference * for **symmetric algorithms**: plain string, sufficiently long for security :param str algorithm: algorithm to sign the token with, e.g. ``"ES256"`` + :param str typ: media type of the payload, e.g. ``"secevent+jwt"`` :param dict headers: additional JWT header fields, e.g. ``dict(kid="my-key-id")`` :param json.JSONEncoder json_encoder: custom JSON encoder for ``payload`` and ``headers`` :rtype: str From 2f0d4104bbf4351ade4ce849cd17265c012ec270 Mon Sep 17 00:00:00 2001 From: "Ajitomi, Daisuke" Date: Wed, 7 Apr 2021 22:00:27 +0900 Subject: [PATCH 03/10] Update CHANGELOG. --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e67381fb..5656c9b6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,7 @@ Fixed - Remove padding from JWK test data. `#628 `__ - Make `kty` mandatory in JWK to be compliant with RFC7517. `#624 `__ - Allow JWK without `alg` to be compliant with RFC7517. `#624 `__ +- Make `typ` optional in JWT to be compliant with RFC7519. `#644 `__ Added ~~~~~ From 1e35042310c115d71a8b9a951917d49bd0e50380 Mon Sep 17 00:00:00 2001 From: "Ajitomi, Daisuke" Date: Thu, 8 Apr 2021 08:21:43 +0900 Subject: [PATCH 04/10] Refine parameter order of for backward compatibility. --- docs/api.rst | 4 ++-- jwt/api_jws.py | 2 +- jwt/api_jwt.py | 4 ++-- tests/test_api_jws.py | 13 +++++++++++++ 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index c000b7d1..1d347ee0 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -3,7 +3,7 @@ API Reference .. module:: jwt -.. function:: encode(payload, key, algorithm="HS256", typ="JWT", headers=None, json_encoder=None) +.. function:: encode(payload, key, algorithm="HS256", headers=None, json_encoder=None, typ="JWT") Encode the ``payload`` as JSON Web Token. @@ -14,9 +14,9 @@ API Reference * for **symmetric algorithms**: plain string, sufficiently long for security :param str algorithm: algorithm to sign the token with, e.g. ``"ES256"`` - :param str typ: media type of the payload, e.g. ``"secevent+jwt"`` :param dict headers: additional JWT header fields, e.g. ``dict(kid="my-key-id")`` :param json.JSONEncoder json_encoder: custom JSON encoder for ``payload`` and ``headers`` + :param str typ: media type of the payload, e.g. ``"secevent+jwt"`` :rtype: str :returns: a JSON Web Token diff --git a/jwt/api_jws.py b/jwt/api_jws.py index c5ecb8eb..22ffd8ce 100644 --- a/jwt/api_jws.py +++ b/jwt/api_jws.py @@ -76,9 +76,9 @@ def encode( payload: bytes, key: str, algorithm: str = "HS256", - typ: str = "JWT", headers: Optional[Dict] = None, json_encoder: Optional[Type[json.JSONEncoder]] = None, + typ: str = "JWT", ) -> str: segments = [] diff --git a/jwt/api_jwt.py b/jwt/api_jwt.py index aaa979ec..3b005fec 100644 --- a/jwt/api_jwt.py +++ b/jwt/api_jwt.py @@ -39,9 +39,9 @@ def encode( payload: Dict[str, Any], key: str, algorithm: str = "HS256", - typ: str = "JWT", headers: Optional[Dict] = None, json_encoder: Optional[Type[json.JSONEncoder]] = None, + typ: str = "JWT", ) -> str: # Check that we get a mapping if not isinstance(payload, Mapping): @@ -61,7 +61,7 @@ def encode( payload, separators=(",", ":"), cls=json_encoder ).encode("utf-8") - return api_jws.encode(json_payload, key, algorithm, typ, headers, json_encoder) + return api_jws.encode(json_payload, key, algorithm, headers, json_encoder, typ) def decode_complete( self, diff --git a/tests/test_api_jws.py b/tests/test_api_jws.py index e8448ed8..a3245b1c 100644 --- a/tests/test_api_jws.py +++ b/tests/test_api_jws.py @@ -673,6 +673,19 @@ def test_encode_with_typ_and_headers_include_typ(self, jws, payload): # typ in headers overwrites typ parameter. assert header_obj["typ"] == "a" + def test_encode_with_typ_without_keywords(self, jws, payload): + headers = {"foo": "bar"} + token = jws.encode(payload, "secret", "HS256", headers, None, "baz") + + header = token[0 : token.index(".")].encode() + header = base64url_decode(header) + header_obj = json.loads(header) + + assert "foo" in header_obj + assert header_obj["foo"] == "bar" + assert "typ" in header_obj + assert header_obj["typ"] == "baz" + def test_encode_fails_on_invalid_kid_types(self, jws, payload): with pytest.raises(InvalidTokenError) as exc: jws.encode(payload, "secret", headers={"kid": 123}) From 14af3fff387cf11b12cc85f25f01a2fe4b1c1608 Mon Sep 17 00:00:00 2001 From: "Ajitomi, Daisuke" Date: Thu, 8 Apr 2021 08:27:10 +0900 Subject: [PATCH 05/10] Remove comment. --- jwt/api_jws.py | 1 - 1 file changed, 1 deletion(-) diff --git a/jwt/api_jws.py b/jwt/api_jws.py index 22ffd8ce..bb570801 100644 --- a/jwt/api_jws.py +++ b/jwt/api_jws.py @@ -90,7 +90,6 @@ def encode( # Header header = {"alg": algorithm} - # if isinstance(typ, str) and len(typ) > 0: if typ: header["typ"] = typ if headers: From 75a10188532d777ebbc71905403bbc4133c8d823 Mon Sep 17 00:00:00 2001 From: "Ajitomi, Daisuke" Date: Thu, 8 Apr 2021 08:37:01 +0900 Subject: [PATCH 06/10] Add Optional to typ. --- jwt/api_jws.py | 2 +- jwt/api_jwt.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/jwt/api_jws.py b/jwt/api_jws.py index bb570801..8b8f92ff 100644 --- a/jwt/api_jws.py +++ b/jwt/api_jws.py @@ -78,7 +78,7 @@ def encode( algorithm: str = "HS256", headers: Optional[Dict] = None, json_encoder: Optional[Type[json.JSONEncoder]] = None, - typ: str = "JWT", + typ: Optional[str] = "JWT", ) -> str: segments = [] diff --git a/jwt/api_jwt.py b/jwt/api_jwt.py index 3b005fec..fcbc876d 100644 --- a/jwt/api_jwt.py +++ b/jwt/api_jwt.py @@ -41,7 +41,7 @@ def encode( algorithm: str = "HS256", headers: Optional[Dict] = None, json_encoder: Optional[Type[json.JSONEncoder]] = None, - typ: str = "JWT", + typ: Optional[str] = "JWT", ) -> str: # Check that we get a mapping if not isinstance(payload, Mapping): From 854be509588d52ee8ead14edb9049e343e682067 Mon Sep 17 00:00:00 2001 From: "Ajitomi, Daisuke" Date: Thu, 8 Apr 2021 20:47:39 +0900 Subject: [PATCH 07/10] Keep order of JWT header parameter (typ, alg). --- jwt/api_jws.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jwt/api_jws.py b/jwt/api_jws.py index 8b8f92ff..895423ac 100644 --- a/jwt/api_jws.py +++ b/jwt/api_jws.py @@ -89,9 +89,9 @@ def encode( pass # Header - header = {"alg": algorithm} - if typ: - header["typ"] = typ + header = {"typ": typ} if typ else {} + header["alg"] = algorithm + if headers: self._validate_headers(headers) header.update(headers) From 12878471b9a7635ddb2318631d61ea8803a92d55 Mon Sep 17 00:00:00 2001 From: "Ajitomi, Daisuke" Date: Fri, 16 Jul 2021 08:06:18 +0900 Subject: [PATCH 08/10] Make typ optional with headers argument. --- CHANGELOG.rst | 4 +++- docs/api.rst | 3 +-- jwt/api_jws.py | 10 +++++++--- jwt/api_jwt.py | 3 +-- tests/test_api_jws.py | 19 ++++++++----------- tests/test_api_jwt.py | 4 +++- 6 files changed, 23 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0dcf96ea..eba9b920 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -13,9 +13,12 @@ Changed Fixed ~~~~~ +- Make `typ` optional in JWT to be compliant with RFC7519. `#644 `__ + Added ~~~~~ + `v2.1.0 `__ -------------------------------------------------------------------- @@ -30,7 +33,6 @@ Fixed - Remove padding from JWK test data. `#628 `__ - Make `kty` mandatory in JWK to be compliant with RFC7517. `#624 `__ - Allow JWK without `alg` to be compliant with RFC7517. `#624 `__ -- Make `typ` optional in JWT to be compliant with RFC7519. `#644 `__ - Allow to verify with private key on ECAlgorithm, as well as on Ed25519Algorithm. `#645 `__ Added diff --git a/docs/api.rst b/docs/api.rst index f3041825..d23d4bdd 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -3,7 +3,7 @@ API Reference .. module:: jwt -.. function:: encode(payload, key, algorithm="HS256", headers=None, json_encoder=None, typ="JWT") +.. function:: encode(payload, key, algorithm="HS256", headers=None, json_encoder=None) Encode the ``payload`` as JSON Web Token. @@ -16,7 +16,6 @@ API Reference :param str algorithm: algorithm to sign the token with, e.g. ``"ES256"`` :param dict headers: additional JWT header fields, e.g. ``dict(kid="my-key-id")`` :param json.JSONEncoder json_encoder: custom JSON encoder for ``payload`` and ``headers`` - :param str typ: media type of the payload, e.g. ``"secevent+jwt"`` :rtype: str :returns: a JSON Web Token diff --git a/jwt/api_jws.py b/jwt/api_jws.py index 895423ac..5b679fd0 100644 --- a/jwt/api_jws.py +++ b/jwt/api_jws.py @@ -19,6 +19,8 @@ class PyJWS: + header_typ = "JWT" + def __init__(self, algorithms=None, options=None): self._algorithms = get_default_algorithms() self._valid_algs = ( @@ -78,7 +80,6 @@ def encode( algorithm: str = "HS256", headers: Optional[Dict] = None, json_encoder: Optional[Type[json.JSONEncoder]] = None, - typ: Optional[str] = "JWT", ) -> str: segments = [] @@ -89,12 +90,15 @@ def encode( pass # Header - header = {"typ": typ} if typ else {} - header["alg"] = algorithm + header = {"typ": self.header_typ, "alg": algorithm} if headers: self._validate_headers(headers) header.update(headers) + if not header["typ"]: + print(header) + del header["typ"] + print(header) json_header = json.dumps( header, separators=(",", ":"), cls=json_encoder diff --git a/jwt/api_jwt.py b/jwt/api_jwt.py index fcbc876d..70a5e537 100644 --- a/jwt/api_jwt.py +++ b/jwt/api_jwt.py @@ -41,7 +41,6 @@ def encode( algorithm: str = "HS256", headers: Optional[Dict] = None, json_encoder: Optional[Type[json.JSONEncoder]] = None, - typ: Optional[str] = "JWT", ) -> str: # Check that we get a mapping if not isinstance(payload, Mapping): @@ -61,7 +60,7 @@ def encode( payload, separators=(",", ":"), cls=json_encoder ).encode("utf-8") - return api_jws.encode(json_payload, key, algorithm, headers, json_encoder, typ) + return api_jws.encode(json_payload, key, algorithm, headers, json_encoder) def decode_complete( self, diff --git a/tests/test_api_jws.py b/tests/test_api_jws.py index 6e58a73c..5da42af8 100644 --- a/tests/test_api_jws.py +++ b/tests/test_api_jws.py @@ -643,7 +643,9 @@ def test_encode_with_typ(self, jws): } } """ - token = jws.encode(payload.encode("utf-8"), "secret", typ="secevent+jwt") + token = jws.encode( + payload.encode("utf-8"), "secret", headers={"typ": "secevent+jwt"} + ) header = token[0 : token.index(".")].encode() header = base64url_decode(header) @@ -653,7 +655,7 @@ def test_encode_with_typ(self, jws): assert header_obj["typ"] == "secevent+jwt" def test_encode_with_typ_empty_string(self, jws, payload): - token = jws.encode(payload, "secret", typ="") + token = jws.encode(payload, "secret", headers={"typ": ""}) header = token[0 : token.index(".")].encode() header = base64url_decode(header) @@ -661,21 +663,18 @@ def test_encode_with_typ_empty_string(self, jws, payload): assert "typ" not in header_obj - def test_encode_with_typ_and_headers_include_typ(self, jws, payload): - headers = {"typ": "a"} - token = jws.encode(payload, "secret", typ="b", headers=headers) + def test_encode_with_typ_none(self, jws, payload): + token = jws.encode(payload, "secret", headers={"typ": None}) header = token[0 : token.index(".")].encode() header = base64url_decode(header) header_obj = json.loads(header) - assert "typ" in header_obj - # typ in headers overwrites typ parameter. - assert header_obj["typ"] == "a" + assert "typ" not in header_obj def test_encode_with_typ_without_keywords(self, jws, payload): headers = {"foo": "bar"} - token = jws.encode(payload, "secret", "HS256", headers, None, "baz") + token = jws.encode(payload, "secret", "HS256", headers, None) header = token[0 : token.index(".")].encode() header = base64url_decode(header) @@ -683,8 +682,6 @@ def test_encode_with_typ_without_keywords(self, jws, payload): assert "foo" in header_obj assert header_obj["foo"] == "bar" - assert "typ" in header_obj - assert header_obj["typ"] == "baz" def test_encode_fails_on_invalid_kid_types(self, jws, payload): with pytest.raises(InvalidTokenError) as exc: diff --git a/tests/test_api_jwt.py b/tests/test_api_jwt.py index 2a9f991d..7a842bfc 100644 --- a/tests/test_api_jwt.py +++ b/tests/test_api_jwt.py @@ -184,7 +184,9 @@ def test_encode_with_typ(self, jwt): } }, } - token = jwt.encode(payload, "secret", algorithm="HS256", typ="secevent+jwt") + token = jwt.encode( + payload, "secret", algorithm="HS256", headers={"typ": "secevent+jwt"} + ) header = token[0 : token.index(".")].encode() header = base64url_decode(header) header_obj = json.loads(header) From 2e21797ae055092aad2e59b95bec544f3cd1b1c7 Mon Sep 17 00:00:00 2001 From: "Ajitomi, Daisuke" Date: Fri, 16 Jul 2021 08:10:27 +0900 Subject: [PATCH 09/10] Make typ optional with headers argument. --- CHANGELOG.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index eba9b920..b5df5d50 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,7 +18,6 @@ Fixed Added ~~~~~ - `v2.1.0 `__ -------------------------------------------------------------------- From 36e518d93cb80d50366c9ca9630ce1c38769e44a Mon Sep 17 00:00:00 2001 From: "Ajitomi, Daisuke" Date: Fri, 16 Jul 2021 08:11:51 +0900 Subject: [PATCH 10/10] Remove unused log. --- jwt/api_jws.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/jwt/api_jws.py b/jwt/api_jws.py index 5b679fd0..4e13b4ee 100644 --- a/jwt/api_jws.py +++ b/jwt/api_jws.py @@ -96,9 +96,7 @@ def encode( self._validate_headers(headers) header.update(headers) if not header["typ"]: - print(header) del header["typ"] - print(header) json_header = json.dumps( header, separators=(",", ":"), cls=json_encoder