こんにちは織田です
今回はRAG-STDの開発中にPyJWTを用いてトークン検証を実施しましたので、そこで得た知見についてまとめようと思います。
JWTおよびPyJWTとは?
JWT (JSON Web Token) とは、インターネット上で情報を安全にやり取りするための規格の一つで、特に認証や認可の分野で利用されています。
ヘッダー、ペイロード、署名(シグネチャ)の3つの要素からなり、それぞれが”.”で区切られています。
そして、PyJWTとはJWT トークンをエンコード、デコード、検証してくれる Python ライブラリです。
JWTの各要素の詳細は以下の通りです。
ヘッダー (Header)
JWTのタイプ(通常は”JWT”)と、署名の生成に使用されるアルゴリズム(例: HMAC SHA256やRSA)がJSON形式で記述されます。これはBase64Urlエンコードされます。
例:
{
"alg": "HS256",
"typ": "JWT"
}
ペイロード (Payload)
ここに、JWTに含める情報(クレームと呼びます)が記述されます。クレームには以下の3種類があります。
- 登録済みクレーム (Registered Claims): JWTの仕様で定義されているクレームで、iss(発行者)、exp(有効期限)、sub(主題)などがあります。これらは任意ですが、利用することで相互運用性が高まります。
- 公開クレーム (Public Claims): 衝突を避けるためにIANAのJWTクレームレジストリに登録されているか、URIとして定義されているクレームです。
- プライベートクレーム (Private Claims): 送信者と受信者の間で合意されたカスタムクレームです。
例:
{
"sub": "1234567890",
""name": "John Doe",
"admin": true
}
このペイロードもBase64Urlエンコードされます。
署名 (Signature)
エンコードされたヘッダー、エンコードされたペイロード、そして秘密鍵を使用して、ヘッダーで指定されたアルゴリズムによって生成されます。この署名があることで、トークンが改ざんされていないこと、および発行者が正しいことを検証できます。
これら3つの要素が「.」で連結されることで、最終的なJWTが構成されます。
例:`eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ`
JWTを用いたトークン検証のプロセス
RAG-STD開発で実施したJWTを用いたトークン検証は、主に以下のステップで構成されます。
- クライアントからのJWT受け取り:
フロントエンドから、ヘッダー情報を介してJWTを受け取ります。 - JWTの解析:
受け取ったJWTをヘッダー、ペイロード、署名の3つの部分に分割します。 - 署名の検証:
- 受け取ったJWTのヘッダーとペイロードを再度エンコードします。
- サーバーが保持している秘密鍵(または公開鍵)と、エンコードされたヘッダー・ペイロードを用いて、新たな署名を生成します。
- 生成された署名と、受け取ったJWTに付与されていた署名を比較します。両者が一致すれば、トークンは改ざんされておらず、有効であると判断できます。一致しない場合は、トークンが無効であると判断し、リクエストを拒否します。
- ペイロードの検証:
署名の検証に成功したら、ペイロードに含まれるクレームを検証します。- `exp` (有効期限) クレームを確認し、トークンが期限切れでないかを確認します。期限切れであれば、トークンは無効です。
- `iss` (発行者) クレームを確認し、信頼できる発行元からのトークンであるかを確認します。
- 必要に応じて、`sub` (主題) やその他のカスタムクレーム(例: ユーザーID、ロールなど)を検証し、リクエスト元のユーザーが適切な権限を持っているかを確認します。
これらの検証ステップを全てクリアした場合、トークンは有効とみなされ、ユーザーは認証・認可された状態となります。
トークン検証時のポイント
実際のコードでは、フロントエンドから発行されたIDトークンの検証において、以下のような流れで処理を進めました。
- 公開鍵の一覧を取得: IDトークンを発行した認証プロバイダ(今回はMicrosoft)が公開している公開鍵の一覧(JWKS: JSON Web Key Set)を取得します。これは通常、特定のURLからJSON形式で提供されます。
- IDトークンの署名を検証する公開鍵を一覧の中から抽出: 取得した公開鍵の一覧から、検証対象のIDトークンのヘッダーにある`kid` (Key ID) に対応する公開鍵を特定し、抽出します。これにより、正しい公開鍵を使って署名を検証できます。
- 署名検証ありでデコード: 抽出した公開鍵とIDトークンを用いて、署名の検証を行いながらトークンをデコードします。このステップで、トークンが改ざんされていないか、正しい発行者によって署名されたものかを確認します。PyJWTのようなライブラリを使用すると、この署名検証はデコード処理の一部として自動的に行われます。
- ユーザー情報を抽出: 署名検証に成功したら、デコードされたペイロードからユーザ名やグループIDなどの必要なユーザ情報を抽出します。この情報をもとに、アプリケーション内でユーザーの識別や認可を行います。
その他のポイントとしては、以下のものがあげられます。
- エラーハンドリング: 署名検証失敗、期限切れ、不正なペイロードなど、あらゆるケースで適切なエラーハンドリングを行うことが重要です。自分が実装しようとした当初はエラーハンドリングを適切に設定できておらず、エラーが発生しても原因が何か分からず悪戦苦闘しました…
- ライブラリの活用: PyJWTを活用することで、ルーティングごとに検証処理を記述する手間を省き、コードの可読性と保守性を高めました。当初は「検証のコードを自前で用意しなければならないのか…」と不安でしたが、PyJWTを用いることで僅かな行数で実装できました。
- `aud` (Audience) クレームの理解: トークンをデコードする際に、`aud` (Audience) クレームの検証に苦労しました。これは、トークンが「誰(どのサービス)のために発行されたものか」を示す、いわば手紙の「宛名」のような概念です。JWTを受け取る側(つまり、検証を行う私たちのアプリケーション)が、そのトークンの`aud`クレームに自身が含まれているかを確認することで、意図しない宛先に送られたトークンの利用を防ぐことができます。
まとめ
RAG-STDの開発を通して、JWTの仕組みと検証プロセス、そして実装上の注意点について深く理解することができました。検証用のコードを0から書くのは大変だろうと身構えていたのですが、PyJWTを使うことで、簡単に実装できました。また、コードを段階的に実装していくことで、検証に必要なプロセスについて詳しく理解することができました。この知見が、今後JWTを用いた認証・認可システムを構築される方々の一助となれば幸いです。