OpenID Connect のIDトークンを PyJWT で検証する
目次
はじめに
昨日、PyJWT で JWT (JSON Web Token) の生成や検証を行う方法を書きました。
今回はさらに発展させ、OpenID Connect の認証フローで得られるIDトークンを PyJWT で検証してみます。
今回用いた環境とトークン
本記事では OpenID Provider として OSS の Keycloak を利用しました。
また、認証フローについては本記事の対象外であり、今回は Implicit Flow により次のペイロードのIDトークンを得ました。 (IDトークンのペイロード部分を Base64 デコードして見やすくしました。)
1{
2 "exp": 1644112575,
3 "iat": 1644111675,
4 "auth_time": 1644111675,
5 "jti": "94a9cb24-910c-4322-9368-de1bdd3f9749",
6 "iss": "http://localhost:8080/auth/realms/example-realm",
7 "aud": "example-client",
8 "sub": "4eca8d6d-956c-4589-9e29-dea54bd2db37",
9 "typ": "ID",
10 "azp": "example-client",
11 "nonce": "n-0S6_WzA2Mj",
12 "session_state": "beaf5c1a-5433-4bff-9af6-4bd64cc05661",
13 "at_hash": "o7mfWVDAgHCEhZwYlVAo5w",
14 "acr": "1",
15 "s_hash": "bOhtX8F73IMjSPeVAqxyTQ",
16 "sid": "beaf5c1a-5433-4bff-9af6-4bd64cc05661",
17 "email_verified": true,
18 "name": "firstname1 lastname1",
19 "preferred_username": "user1",
20 "given_name": "firstname1",
21 "family_name": "lastname1",
22 "email": "[email protected]"
23}
IDトークンに含まれるクレームの意味については、OpenID Connect Core 仕様の Section 2.や5.辺り、および RFC 7519 に記載されています。
https://openid.net/specs/openid-connect-core-1_0.html
https://datatracker.ietf.org/doc/html/rfc7519
IDトークンの検証
IDトークンをどのように検証するかは OpenID Connect Core 仕様の 3.1.3.7. に記載されています。
これらの検証は、PyJWT を用いて次のようなプログラムで行えます。
1import jwt
2from typing import Any
3
4public_key_body = "Keycloakの公開鍵"
5public_key = "-----BEGIN PUBLIC KEY-----\n" \
6 + public_key_body \
7 + "\n-----END PUBLIC KEY-----"
8
9token = "IDトークン"
10
11payload: dict[str, Any] = jwt.decode( # type: ignore
12 jwt=token,
13 key=public_key,
14 algorithms=["RS256"],
15 audience="example-client",
16 issuer="http://localhost:8080/auth/realms/example-realm"
17)
18
19print(payload)
Keycloak の署名の公開鍵は、管理画面の Realm 設定画面 (※下の画像の赤丸の部分) または JWK (JSON Web Key) のエンドポイントから取得できます。 取得できる公開鍵の文字列にはヘッダやフッタが付いていないため、プログラム上で付けています。
IDトークンの検証は PyJWT の jwt
モジュールの decode
関数で行い、公開鍵による署名の検証が行われることについては前回の記事の通りです。
https://pyjwt.readthedocs.io/en/stable/api.html#jwt.decode
ペイロード部分の検証は PyJWT の jwt/api_jwt.py#L122-L215 辺りで実装されています。
jwt.decode
関数の引数 issuer
に期待する Issuer の値を指定すると、ペイロードの iss
と一致するかどうかチェックされます。
引数 issuer
を指定しなければチェックは行われません。
また、引数 audience
に期待するクライアントIDの値を指定すると、ペイロードの aud
と一致するかどうかチェックされます。
OpenID Connect では aud
の検証は必須であり、引数 audience
を指定せずペイロードに aud
が含まれていると InvalidAudienceError
が発生します。
ただし、クライアントIDの検証を行わない場合は、次のように options
を指定すれば検証を無効化できます。
1options = {
2 "verify_aud": False
3}
4
5payload: dict[str, Any] = jwt.decode( # type: ignore
6 jwt=token,
7 key=public_key,
8 algorithms=["RS256"],
9 options=options,
10 issuer="http://localhost:8080/auth/realms/example-realm"
11)
IDトークンが有効期限内かどうかのチェックについては、ペイロードの exp
の値が現在時刻よりも前であった場合に ExpiredSignatureError
が発生します。
OpenID Connect の仕様にはありませんが、ペイロードに nbf
(Not Before) が含まれている場合、その値が現在時刻よりも後であった場合には ImmatureSignatureError
が発生します。
jwt.decode
関数でエラーが発生することなく戻り値のペイロードを取得できたら、ペイロードに含まれるユーザ情報などのクレームを信用して利用できます。