同一のJSONやDictを与えているはずなのに、生成されたJWTが変わってしまったというメモ。
なお、ライブラリの特性である可能性はあるので、すべてのJWTライブラリで発生するわけではありません。
環境
- Python
- 3.11
- python-jose
- 3.3.0
原因
JSONやDictとしては等価だったが、JSONを文字列化(シリアライズ)したタイミングでキーの順番が変わってしまったため。シリアライズした結果を元にencodeするので、生成されるJWTが等価にならない。
事象発生時のシリアライズ
{"id": 1, "exp": 1710152710, "iss": "kirimaru"}
{"exp": 1710152710, "id": 1, "iss": "kirimaru"}
対応
JWTに関しては検証を行わない。JWTからJSONの復元ができることのみを確認する。
動作確認時のコード
次の3つを確認していました。
- JSON(Dict)のキーが異なった場合でも等しいこと
- キーをソートしたら同一のJWTが生成されること
- 異なるJWTから同一のJSONを得られること
from jose import jwt async def test_expected_same_token(): KEY = "SECRET" ALGORITHM = "HS256" json_token = { "id": 1, "exp": 1710152710, "iss": "kirimaru" } pydantic_token = { "exp": 1710152710, "id": 1, "iss": "kirimaru" } # NOTE: JSON のキーの順番が違ってもTrue になる assert json_token == pydantic_token # NOTE: JWTの payload の順番が違ったら別のトークンになるのでソートしたら True になる json_encode = jwt.encode(dict(sorted(json_token.items())), KEY, algorithm=ALGORITHM) pydantic_encode = jwt.encode(dict(sorted(pydantic_token.items())), KEY, algorithm=ALGORITHM) assert json_encode == pydantic_encode # NOTE: ソートしなくても別のトークンから同じJSONに戻せることのチェック json_encode = jwt.encode(json_token, KEY, algorithm=ALGORITHM) pydantic_encode = jwt.encode(pydantic_token, KEY, algorithm=ALGORITHM) json_decode = jwt.decode(json_encode, KEY, algorithms=ALGORITHM) pydantic_decode = jwt.decode(pydantic_encode, KEY, algorithms=ALGORITHM) assert json_decode == pydantic_decode
ソースコード
事象発生時のpython-jose
ではなく、PyJwt
でGitHubでは公開しています。
終わりに
複数の言語を使用しているアプリケーションを運用しているので、別アプリケーションで生成したJWTを元に、他アプリケーションでも同一のJWTを生成していることを期待してハマりました。
パラメータとしてJSONを期待しているなら、キーのソートまでライブラリ側でやってほしいのですがね…。PullRequestを出そうかと思っていましたが、python-jose
の最終マージが2023/05/04だったので、取り込まれそうになかったのもありPullRequestは作ってません。
もし興味があれば該当箇所までは判別したので、PullRequestを出してみてください。