Amazon CognitoのIDトークンのJWTをPyJWTを使って検証する

Cognitoの認証により発行されるA JSON Web Token (JWT) に対して、Pythonで検証処理を自前実装する方法を記載します。

JWT検証に利用するライブラリ

タイトル通りPyJWTを利用します。

github.com

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の検証方法

公式ドキュメントに記載があります。

docs.aws.amazon.com

ざっくり箇条書きにすると以下の検証が必要です。

  • 署名を検証
  • 有効期限( 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"

f:id:yomon8:20200723181610p:plain

cognito_user_pool_id = "COGNITO_USER_POOL_ID"

f:id:yomon8:20200723181723p:plain

補足 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
)

audienceissuer は **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
        }