きり丸の技術日記

技術検証したり、資格等をここに残していきます。

同じJSONやDictでも生成されるJWTは変わることがある(Pythonで例示)

同一の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を出してみてください。