Cognitoの認証により発行されるA JSON Web Token (JWT) に対して、Pythonで検証処理を自前実装する方法を記載します。
JWT検証に利用するライブラリ
タイトル通りPyJWTを利用します。
PyJWTはAuth0がスポンサーになっていて、Auth0のSDKでも使われているライブラリです。
https://github.com/auth0/auth0-python/blob/3.12.0/requirements.txt#L4
今回は以下のバージョンを利用しています。
pip install pyjwt[crypto]==1.7.1
Amazon CognitoのJWTの検証方法
公式ドキュメントに記載があります。
ざっくり箇条書きにすると以下の検証が必要です。
- 署名を検証
- 有効期限(
exp
), 発行時刻 (iat
)のクレームを検証 - 対象者 (
aud
) のクレームの検証 - 発行者 (
iss
) のクレームの検証 token_use
のクレームを検証(今回はIDトークンであることを確認)
サンプルコード
説明はコメントに書いてあります。
import requests import json import jwt from jwt.algorithms import RSAAlgorithm id_token = "eyxxxxx...." # IDトークン cognito_region = "ap-northeast-1" cognito_client_id = "COGNITO_CLIENT_ID" cognito_user_pool_id = "COGNITO_USER_POOL_ID" cognito_url = ( f"https://cognito-idp.{cognito_region}.amazonaws.com/{cognito_user_pool_id}" ) cognito_jwk_url = f"{cognito_url}/.well-known/jwks.json" # TokenのヘッダからKey IDと署名アルゴリズムを取得 jwt_header = jwt.get_unverified_header( id_token ) # -> {'alg': 'RS256', 'kid': 'xxxxxxxxxxxxx'} key_id = jwt_header["kid"] jwt_algorithms = jwt_header["alg"] # ヘッダから取得したKey IDを使い、署名検証用の公開鍵をCognitoから取得 # 鍵は複数存在するので、ヘッダから取得したKey IDと合致するものを取得 res_cognito = requests.get(cognito_jwk_url) jwk = None for key in json.loads(res_cognito.text)["keys"]: if key["kid"] == key_id: jwk = key if not jwk: raise Exception("JWK Not Found") public_key = RSAAlgorithm.from_jwk(json.dumps(jwk)) # PyJWTではdecode時に様々な検証を行うことが可能 # ここでは以下の検証を一気に行えます # - 署名を検証 # - 有効期限( exp ), 発行時刻 ( iat )のクレームを検証 # - 対象者 ( aud ) のクレームの検証 # - 発行者 ( iss ) のクレームの検証 json_payload = jwt.decode( id_token, public_key, algorithms=[jwt_algorithms], verify=True, options={"require_exp": True}, audience=cognito_client_id, issuer=cognito_url, ) # token_use クレームを検証(今回はIDトークンであることを確認) if not "id" in json_payload["token_use"]: raise Exception("Not ID Token") print(json.dumps(json_payload, indent=2))
補足 Cognitoの設定
それぞれAWSのマネジメントコンソールから取得できます。
cognito_client_id = "COGNITO_CLIENT_ID"
cognito_user_pool_id = "COGNITO_USER_POOL_ID"
補足 jwt.decode()について
コード見てわかる通り、様々な検証が jwt.decode()
という関数により行われています。
# PyJWTではdecode時に様々な検証を行うことが可能 # ここでは以下の検証を一気に行えます # - 署名を検証 # - 有効期限( exp ), 発行時刻 ( iat )のクレームを検証 # - 対象者 ( aud ) のクレームの検証 # - 発行者 ( iss ) のクレームの検証 json_payload = jwt.decode( id_token, public_key, algorithms=[jwt_algorithms], verify=True, options={"require_exp": True}, audience=cognito_client_id, issuer=cognito_url, )
この辺りの細かい動きは以下のファイルを見るとわかります。
https://github.com/jpadilla/pyjwt/blob/1.7.1/jwt/api_jwt.py
検証に失敗すると、以下のようなExceptionが発生します。エラーハンドリングに使います。
from .exceptions import ( DecodeError, ExpiredSignatureError, ImmatureSignatureError, InvalidAudienceError, InvalidIssuedAtError, InvalidIssuerError, MissingRequiredClaimError )
audience
や issuer
は **kwarg で渡す仕様でした。
また、検証される項目などをオプションで指定することが可能です。
def _get_default_options(): # type: () -> Dict[str, bool] return { 'verify_signature': True, 'verify_exp': True, 'verify_nbf': True, 'verify_iat': True, 'verify_aud': True, 'verify_iss': True, 'require_exp': False, 'require_iat': False, 'require_nbf': False }