PyJWT で JWT の生成と検証を行う

目次

はじめに

OpenID Connect の認証フローで得らえるIDトークンやアクセストークンは JWT (JSON Web Token) 形式であり、トークン自体に署名が付いています。 期待する Issuer によってトークンが署名されていることを検証できれば、トークンに含まれるユーザ情報などを信用できることになります。

JWT を Python で検証するには、PyJWT を利用するのが最も簡単でした。
https://pyjwt.readthedocs.io/en/stable/

本記事では備忘録も兼ねて、PyJWT を利用して JWT を検証 (デコード) する方法と、ついでに JWT を生成 (エンコード) する基本的な方法をまとめます。

なお、JWT の検証には公開鍵、生成には秘密鍵が必要になりますが、ここでは openssl コマンドで次のように RSA のキーペアを作成して用いました。

1## 秘密鍵の作成
2$ openssl genrsa -out private_key_file 2048
3
4## 秘密鍵から公開鍵の作成
5$ openssl rsa -pubout -in private_key_file -out public_key_file

JWT を生成 (エンコード) する

PyJWT を利用するには、PyJWT ライブラリと暗号化用の cryptography ライブラリをインストールする必要があります。

1$ pip install PyJWT cryptography

JWT の生成は jwt モジュールの encode 関数で行えます。
https://pyjwt.readthedocs.io/en/stable/api.html#jwt.encode

例えば次のようなプログラムで生成できます。

 1import jwt
 2
 3with open("private_key_file", "r") as f:
 4    private_key = f.read()
 5
 6payload = {
 7    "key1": "value1",
 8    "key2": "value2"
 9}
10
11token: str = jwt.encode(  # type: ignore
12    payload=payload,
13    key=private_key,
14    algorithm="RS256"
15)
16
17print(token)

引数の payload には任意の dict を指定できます。 ここでは例示用の単純な dict を指定していますが、OpenID Connect の OP として JWT を生成する場合は OpenID Connect Core 仕様 に従ってクレームを含める必要がありますね。

引数の key には、秘密鍵の内容の文字列を指定します。

引数の algorithm には署名のために利用する暗号化アルゴリズムを指定します。 指定可能なアルゴリズムは以下に記載されており、指定しない場合はデフォルト値 HS256 が利用されます。
https://pyjwt.readthedocs.io/en/stable/algorithms.html

上記のプログラムを実行すると、次のようなトークンが出力されます。

1eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJrZXkxIjoidmFsdWUxIiwia2V5MiI6InZhbHVlMiJ9.gMQmf_eiaxCcwZkRlOvwfjMyawEdbSUeKEcMkj5hnJubQsFcgVRW82vRJVXQJ02oMrMtHRARXf29PR0zAY55sQfp35qo3osEGdtZBEXR1XGF9zCafWZgr7yO2awYoqxTfiOc60yvlI_sXmWMvRC-nU9Yuct6ozc5DLCiq9vXn2ke_BL0ed_rEVc8D9Ka9R-3NvbMrs5Zw09LdgLPjiOmRTSBfWEUFYUggpIuP9nAz6xALYib6jbNeL-szYKu-1-tU45uJU0azG3_ffXgcknhUVQTTzviZ6r1q9nFq8hAiZF3RHuBODJR-FeKu8o7aVdN609ctIPqsu2nxrIQG3xN8A

トークンのヘッダ (ピリオド区切りで1番目の部分) を Base64 デコードすると、指定した通りのアルゴリズムが格納されています。

1$ echo -n "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9" | base64 -d
2{"typ":"JWT","alg":"RS256"}

また、トークンのペイロード (ピリオド区切りで2番目の部分) を Base64 デコードすると、プログラムで指定したペイロードがそのまま期待通り格納されています。

1$ echo -n "eyJrZXkxIjoidmFsdWUxIiwia2V5MiI6InZhbHVlMiJ9" | base64 -d
2{"key1":"value1","key2":"value2"}

JWT を検証 (デコード) する

JWT の検証は jwt モジュールの decode 関数で行えます。
https://pyjwt.readthedocs.io/en/stable/api.html#jwt.decode

例えば次のようなプログラムで検証できます。

 1import jwt
 2from typing import Any
 3
 4with open("public_key_file", "r") as f:
 5    public_key = f.read()
 6
 7token = "トークン文字列"
 8
 9payload: dict[str, Any] = jwt.decode(  # type: ignore
10    jwt=token,
11    key=public_key,
12    algorithms=["RS256"]
13)
14
15print(payload)

引数の jwt には、検証したい JWT のトークン文字列を指定します。

引数の key には、署名の検証で利用する公開鍵の内容の文字列を指定します。

引数の algorithms には署名として許可する暗号化アルゴリズムを1つ以上指定する必要があり、指定したアルゴリズム以外による署名を持つトークンは検証エラーとなります。

decode 関数の戻り値として、トークンのペイロードを dict 型として受け取れます。

前項で生成したトークンを用いて上記のプログラムを実行すると、期待通りのペイロードが出力されました。

1{'key1': 'value1', 'key2': 'value2'}

トークンの署名部分を改変して実行すると、期待通りに署名の検証エラーが発生しました。

1Traceback (most recent call last):
2  (略)
3jwt.exceptions.InvalidSignatureError: Signature verification failed

PyJWT では OpenID Connect のIDトークンであることを想定した検証機能もあり、トークンの有効期限や Issuer の検証なども行えます。 これについては以下の記事として記載しました。

OpenID Connect のIDトークンを PyJWT で検証する