JSON Web Token の中身

JWT の内部にあるヘッダー、ペイロード、署名を解説し、標準のクレーム、そしてトークンを Base64url でデコードすることが検証とは違う理由を扱います。

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発行時刻トークンが作成された時刻
jtiJWT ID一意な識別子。個々のトークンの追跡や遮断に使える

expnbfiat はいずれも NumericDate の値であり、1970 年 1 月 1 日 UTC からの秒数を意味します。ミリ秒ではなく秒です。タイムスタンプをデコードして 57,000 年という日付が出てきたなら、それはミリ秒を読んでいるので 1000 で割る必要があります。

ペイロードのそれ以外の部分は、発行者が入れたものすべてです。ロール、権限、メールアドレス、テナント ID などです。独自のクレーム名がいつか登録済みの名前と衝突する可能性があるため、発行者は URL に似た接頭辞で自前の名前空間を付けることがよくあります。

署名

署名は、最初の 2 つの部分をドットでつないだテキストそのものに対して、ヘッダーが示すアルゴリズムと鍵を使って計算されます。ヘッダーやペイロードを 1 文字でも変えれば、署名は一致しなくなります。

HS256 では同じ秘密値で署名と検証を行うので、トークンを検証できる者はトークンを作り出すこともできます。RS256ES256 では秘密鍵で署名し、公開鍵で検証します。だからこそ ID プロバイダーは、秘密の値を一切持たない多数の独立したサービスが検証できるトークンを配布できるのです。

Base64url は暗号化ではない

Base64url は Base64 と同じ考え方で、違いは 2 点です。+/ の代わりに -_ を使うため結果が URL の中でも安全であること、そして末尾の = によるパディングが通常は取り除かれることです。これはエンコードであり、バイト列をテキストとして書き表す可逆な方法にすぎません。機密性はまったく提供しません。

したがって、機微な情報を JWT のペイロードに入れてはいけません。パスワード、カード番号、そのユーザーに関する内部的なメモなどです。トークンを持っている人、そしてログファイルやブラウザーのストレージからそれを読み取った人は、すべてのフィールドを見られると考えてください。

ペイロードを実際に暗号化する JWE という別のフォーマットがあり、こちらは 3 つではなく 5 つの部分から成ります。手元のトークンが 3 つの部分から成るなら、それは署名されているだけで、読み取り可能です。

デコードは検証ではない

ここが肝心な点です。デコーダーが見せてくれるのは、そのトークンが何を主張しているかです。そのトークンが本物かどうかは教えてくれません。検証は別の工程であり、鍵が必要です。

トークンを実際に信頼するために、サーバーは次のことを行わなければなりません。

  1. 正しい鍵に対して署名を検証する。
  2. トークン内の alg フィールドを信用するのではなく、自分が期待するアルゴリズムを強制する。これを省くことから 2 つの古典的な攻撃が生まれます。algnone にして署名を取り除く手口と、そのサービスの RSA 公開鍵を HMAC の秘密値として使い、RS256 の検証器をだまして HS256 を実行させる手口です。
  3. exp と、存在すれば nbf を確認する。時計の小さなずれは許容する。
  4. issaud が、このサービスの期待する値と一致することを確認する。

そこまで済んで初めて、クレームは意味を持ちます。それまでのペイロードは、ネットワーク越しに届いた未検証の文字列にすぎません。

実務上の注意

  • トークンは通常、HTTP ヘッダーで Authorization: Bearer <token> の形で送られるので、ペイロードは小さく保ってください。追加したクレームは、すべてのリクエストで送られます。
  • 署名されたトークンは、有効期限が切れるまで有効なままです。これを失効させる仕組みは組み込まれていません。アクセストークンの有効期間が短く、しばしば数分程度であり、新しいものを取得するために別途リフレッシュトークンが使われるのは、このためです。
  • トークンの形がおかしいと感じたら、まずドットの数を数えてください。ドットが 2 つ、部分が 3 つなら、通常の署名付き JWT です。ターミナルから貼り付けた際に途中で切れたトークンは、「invalid signature」の非常によくある原因です。
  • デバッグ中にトークンをデコードして有効期限、主体、ロールを読むことは完全に安全であり、秘密の値も必要ありません。ペイロードは、まさにそのためにあります。