JSON Web Token の中身
JSON Web Token (JWT) は、ピリオドでつながれた 3 つのテキストの塊です。各塊は Base64url でエンコードされており、最初の 2 つは単なる JSON です。トークンを持っている人なら誰でも中身を読めます。通常の JWT に暗号化された部分はありません。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 <- header
eyJzdWIiOiIxMjM0NSIsIm5hbWUiOiJBbGV4In0 <- payload
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFW <- signature
これらをピリオドでつないだものが、リクエストに貼り付けるトークンです。最初の 2 つの部分をデコードすると、普通の JSON が得られます。
{ "alg": "HS256", "typ": "JWT" }
{ "sub": "12345", "name": "Alex" }
実際のペイロードには、たいていタイムスタンプと対象者も入っています。
{
"iss": "https://auth.example.com",
"sub": "12345",
"aud": "api.example.com",
"iat": 1756000000,
"exp": 1756003600,
"roles": ["editor"]
}
ヘッダー
ヘッダーは、そのトークンがどう署名されているかを記述します。特に重要なフィールドは 2 つです。
algは署名アルゴリズムの名前です。たとえばHS256(共有の秘密値を使った SHA-256 の HMAC) やRS256(SHA-256 を用いた RSA 署名。秘密鍵で署名し、公開鍵で検証する) です。kid(キー ID) は省略可能で、複数ある鍵のどれが使われたかを受け取り側に伝えます。これにより、既存のトークンを壊さずに鍵をローテーションできます。
typ はたいてい JWT であるだけで、セキュリティ上の意味は持ちません。
ペイロードとクレーム
ペイロードは クレーム から成る JSON オブジェクトです。クレームとは、トークンの主体についての言明を指す、仕様上の呼び名にすぎません。一部のクレーム名は標準に登録されており、いずれも短いものです。
| クレーム | 名称 | 内容 |
|---|---|---|
iss | 発行者 | トークンを作成した主体。通常は URL かサービス名 |
sub | 主体 | トークンが誰についてのものか。通常は変わらないユーザー ID |
aud | 対象者 | トークンが誰に向けられているか。別の相手向けのトークンをサービスが拒否できるようにする |
exp | 有効期限 | これ以降はトークンを拒否しなければならない瞬間 |
nbf | 開始時刻 | これより前ではトークンを拒否しなければならない瞬間 |
iat | 発行時刻 | トークンが作成された時刻 |
jti | JWT ID | 一意な識別子。個々のトークンの追跡や遮断に使える |
exp、nbf、iat はいずれも NumericDate の値であり、1970 年 1 月 1 日 UTC からの秒数を意味します。ミリ秒ではなく秒です。タイムスタンプをデコードして 57,000 年という日付が出てきたなら、それはミリ秒を読んでいるので 1000 で割る必要があります。
ペイロードのそれ以外の部分は、発行者が入れたものすべてです。ロール、権限、メールアドレス、テナント ID などです。独自のクレーム名がいつか登録済みの名前と衝突する可能性があるため、発行者は URL に似た接頭辞で自前の名前空間を付けることがよくあります。
署名
署名は、最初の 2 つの部分をドットでつないだテキストそのものに対して、ヘッダーが示すアルゴリズムと鍵を使って計算されます。ヘッダーやペイロードを 1 文字でも変えれば、署名は一致しなくなります。
HS256 では同じ秘密値で署名と検証を行うので、トークンを検証できる者はトークンを作り出すこともできます。RS256 や ES256 では秘密鍵で署名し、公開鍵で検証します。だからこそ ID プロバイダーは、秘密の値を一切持たない多数の独立したサービスが検証できるトークンを配布できるのです。
Base64url は暗号化ではない
Base64url は Base64 と同じ考え方で、違いは 2 点です。+ と / の代わりに - と _ を使うため結果が URL の中でも安全であること、そして末尾の = によるパディングが通常は取り除かれることです。これはエンコードであり、バイト列をテキストとして書き表す可逆な方法にすぎません。機密性はまったく提供しません。
したがって、機微な情報を JWT のペイロードに入れてはいけません。パスワード、カード番号、そのユーザーに関する内部的なメモなどです。トークンを持っている人、そしてログファイルやブラウザーのストレージからそれを読み取った人は、すべてのフィールドを見られると考えてください。
ペイロードを実際に暗号化する JWE という別のフォーマットがあり、こちらは 3 つではなく 5 つの部分から成ります。手元のトークンが 3 つの部分から成るなら、それは署名されているだけで、読み取り可能です。
デコードは検証ではない
ここが肝心な点です。デコーダーが見せてくれるのは、そのトークンが何を主張しているかです。そのトークンが本物かどうかは教えてくれません。検証は別の工程であり、鍵が必要です。
トークンを実際に信頼するために、サーバーは次のことを行わなければなりません。
- 正しい鍵に対して署名を検証する。
- トークン内の
algフィールドを信用するのではなく、自分が期待するアルゴリズムを強制する。これを省くことから 2 つの古典的な攻撃が生まれます。algをnoneにして署名を取り除く手口と、そのサービスの RSA 公開鍵を HMAC の秘密値として使い、RS256の検証器をだましてHS256を実行させる手口です。 expと、存在すればnbfを確認する。時計の小さなずれは許容する。issとaudが、このサービスの期待する値と一致することを確認する。
そこまで済んで初めて、クレームは意味を持ちます。それまでのペイロードは、ネットワーク越しに届いた未検証の文字列にすぎません。
実務上の注意
- トークンは通常、HTTP ヘッダーで
Authorization: Bearer <token>の形で送られるので、ペイロードは小さく保ってください。追加したクレームは、すべてのリクエストで送られます。 - 署名されたトークンは、有効期限が切れるまで有効なままです。これを失効させる仕組みは組み込まれていません。アクセストークンの有効期間が短く、しばしば数分程度であり、新しいものを取得するために別途リフレッシュトークンが使われるのは、このためです。
- トークンの形がおかしいと感じたら、まずドットの数を数えてください。ドットが 2 つ、部分が 3 つなら、通常の署名付き JWT です。ターミナルから貼り付けた際に途中で切れたトークンは、「invalid signature」の非常によくある原因です。
- デバッグ中にトークンをデコードして有効期限、主体、ロールを読むことは完全に安全であり、秘密の値も必要ありません。ペイロードは、まさにそのためにあります。