JWTの中身と、デコードと検証の違い
JSON Web Token(JWT)は、ドットで区切られた3つの文字列、つまりヘッダー、ペイロード、署名からなります。前の2つは符号化されているだけで暗号化されてはいないため、トークンを持っている人なら誰でも中身を読めます。署名によって、サーバーは信頼できる発行元がそのトークンを発行したこと、そして内容が改ざんされていないことを確認できます。トークンをデコードすると、そこに何が書かれているかがわかります。信用してよいかどうかを判断できるのは、検証だけです。
JWTデコーダー期限切れ?それとも有効?無料で使う3つの部分
トークンはheader.payload.signatureの形をしています。jwt.ioの有名なサンプルトークンの最初の部分は次のとおりです。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
最初の2つの部分はそれぞれ、JSONをbase64urlに変換したものです。base64urlはURLに安全に使える文字列の形式です。最初の部分をデコードすると次のようになります。
{"alg":"HS256","typ":"JWT"}
同じサンプルのペイロードをデコードすると次のとおりです。
{"sub":"1234567890","name":"John Doe","iat":1516239022}
ペイロードには、ユーザーやセッションに関する主張であるクレームが入ります。署名は最初の2つの部分と鍵から計算されるため、1文字でも変わると一致しなくなります。base64urlは鍵がなくても元に戻せるので、パスワード、秘密鍵、個人情報をペイロードに入れてはいけません。
登録済みクレーム
JWTの標準であるRFC 7519は、短いクレーム名をいくつか予約しています。
iss(発行者):トークンを作成した主体です。sub(主体):トークンが誰についてのものか示します。通常はユーザーIDです。aud(対象):トークンの想定される宛先となるサービスです。exp(有効期限):この時刻を過ぎると、トークンは無効になります。nbf(有効開始時刻):トークンが有効になる最も早い時刻です。iat(発行時刻):トークンが作成された時刻です。
時刻は、UTCの1970年1月1日からの経過秒数で表し、UNIX時間と呼ばれます。サンプルにはexpがないため、標準の定義では期限切れになりません。実際のトークンには有効期限を設定すべきです。roleやemailなどのその他の名前は、アプリケーションが独自に定義するものです。
デコードは検証ではない
デコードでは、トークンをドットで分割し、base64urlを元に戻して、JSONを読み取ります。これによってトークンが何を主張しているかはわかりますが、偽造されたトークンでも同じように読めます。誰でも任意の署名を付けて、任意のクレームを書けるからです。
検証では、正しい鍵で署名を計算し直して一致するかを確かめます。さらに、expとnbfが現在時刻を挟んでいるか、issとaudがサービスの想定する値かも確認します。検証せずにroleクレームを読んで信頼するアプリは、トークンを書いた人を信じていることになります。これはよくある間違いです。
HS256、RS256、ES256
algの値は署名方式を示します。よく使われるものは3つあります。
- HS256は、SHA-256によるHMACです。1つの共有秘密鍵で署名と検証の両方を行うため、その秘密鍵はトークンを発行するサーバーの外に出してはいけません。
- RS256は、SHA-256によるRSA署名です。発行者は秘密鍵で署名し、公開鍵を持つ人は誰でも検証できます。他のサービスは、トークンを作成できなくても検証できます。
- ES256は、P-256曲線とSHA-256によるECDSA署名です。RS256と同様の仕組みで、鍵と署名のサイズが小さくなります。
1つのサービスが自身のトークンを発行と検証の両方に使う場合はHS256を使います。発行していないトークンを他のサービスが検証する必要がある場合はRS256かES256を使います。
ヘッダーを信用することで生じる問題
ヘッダーはトークンを送る側が書くものなので、検証する側はヘッダーにルールを決めさせてはいけません。
alg: none。 署名のないトークンに"alg":"none"と書けます。かつては、このようなトークンを受け入れて検証を省略してしまうライブラリがありました。許可するアルゴリズムは自分のコードで設定し、トークンから取得してはいけません。- アルゴリズムの混同。 サーバーはRS256を想定し、公開鍵を保持しています。攻撃者は、その公開鍵をHMACの秘密鍵として使って署名したHS256のトークンを送ります。ヘッダーで指定されたアルゴリズムをそのまま受け入れる検証処理は、このトークンを受け入れてしまいます。各鍵は必ず1つのアルゴリズムだけに結び付けてください。
- 弱い秘密鍵。 攻撃者はヘッダー、ペイロード、署名を入手できるため、オフラインで候補の秘密鍵を試せます。短い秘密鍵や一般的な単語は、そのようなリストで簡単に突破されます。RFC 7518では、HS256の鍵は少なくとも256ビットであり、かつランダムであるべきとされています。
本番環境のトークンをWebサイトに貼り付ける
有効で期限内のトークンは、期限切れになるまで、それを持つ人なら誰でも使えます。パスワードと同じように扱ってください。サイトに貼り付ける前に、そのサイトがトークンをサーバーに送信するかどうかを確認してください。ブラウザのタブだけで完結するツールであれば、送信する必要はありません。ブラウザの開発者ツールでネットワークリクエストを見れば、どのページでも確認できます。
サーバーが一覧を保持していない限り、トークンは期限切れの前に取り消せないのが一般的です。有効なトークンが漏えいした場合は、サービスにセッションの取り消しや鍵のローテーションを依頼してください。可能であれば、デバッグにはテスト環境のトークンを使ってください。JWTデコーダーは、3つの部分をブラウザのタブ内で読み取ります。貼り付けた内容がどこかに送信されることはありません。
Webhook:共有秘密鍵を使った同じ考え方
多くのWebhookプロバイダーは、リクエスト本文のHMACを、あなたと共有した秘密鍵で計算し、ヘッダーに入れて送ります。受信側も同じHMACを計算して、2つの値を比較します。よくある失敗は細かいものです。ハッシュ化の前にJSONが整形し直されてバイト列が変わってしまう。テスト用の秘密鍵を本番のトラフィックに使ってしまう。あるいは、署名を通常の文字列比較で照合してしまい、処理時間からタイミング情報が漏れるおそれがある。定数時間比較を使ってください。
Webhookの署名検証ツールでは、貼り付けた秘密鍵と本文から署名を計算します。GitHub、Stripe、Slack、Shopifyや、その他任意のHMAC方式に対応しているので、2つの値が一致するかを確認できます。
よくある質問
JWTは暗号化されていますか?
いいえ。標準的なJWTは署名されているだけで、誰でもヘッダーとペイロードを読めます。改ざんを防いでいるのは署名だけです。
ペイロードを編集するとどうなりますか?
署名が一致しなくなるため、検証を行うサーバーはそのトークンを拒否します。クレームを読んだだけでは署名は確認されないので、デコーダーでは編集後の内容がそのまま表示されます。
英語原文をもとに、AIの支援を受けて翻訳しています。
コメント
まだコメントはありません。最初のコメントを書いてみましょう。


