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 の検証なども行えます。 これについては以下の記事として記載しました。