JWTの中身を確認する方法と注意点 — デコードと署名検証の違い
公開日:
ログインAPIのレスポンスやAuthorizationヘッダーで見かける「eyJ」から始まる長い文字列は、多くの場合JWT(JSON Web Token)です。認証まわりの不具合を調べるときは、まずトークンの中身を確認するのが近道ですが、「中身が読めること」と「そのトークンが正しいこと」は別の話です。この記事では、JWTの構造と中身の確認方法、実装やデバッグで注意すべきポイントを解説します。
JWTの構造: 3つのパートをピリオドでつなぐ
JWTは header.payload.signature の3つのパートをピリオドで連結した文字列です。headerには署名アルゴリズム(alg)やトークンの種類(typ)、payloadにはユーザーIDや有効期限などの情報(クレーム)がJSONで入っており、どちらもBase64URLでエンコードされています。signatureは、headerとpayloadに対して秘密鍵や共有鍵で計算した署名です。
多くのJWTが「eyJ」で始まるのは、headerのJSONが「{"」で始まり、それをBase64URLエンコードすると「eyJ」になるためです。例えば {"alg":"HS256","typ":"JWT"} をエンコードすると eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 になります。
なお、ピリオドで区切られたパートが5つあるトークンはJWE(暗号化されたトークン)です。payloadが暗号化されているため、鍵がなければ中身は読めません。
payloadに入っている主なクレーム
RFC 7519では、次のような登録済みクレームが定義されています。どれも必須ではなく、サービスごとにロールやメールアドレスなどの独自クレームを追加することもできます。
- iss(Issuer): トークンの発行者。認可サーバーのURLなど
- sub(Subject): トークンの主体。通常はユーザーID
- aud(Audience): トークンの受け手。文字列または配列で、APIの識別子やクライアントIDが入る
- exp(Expiration Time): 有効期限。この時刻以降は受け付けてはいけない
- nbf(Not Before): この時刻より前は受け付けてはいけない
- iat(Issued At): 発行時刻
- jti(JWT ID): トークンを一意に識別するID。再利用(リプレイ)の検出などに使う
exp・nbf・iat は、Unix時間の「秒」で表されます。JavaScriptの Date.now() はミリ秒を返すため、比較するときは Math.floor(Date.now() / 1000) のように秒に直す必要があります。ミリ秒のまま exp を設定すると、有効期限が数万年後のトークンになってしまうので注意しましょう。
中身を確認する方法
payloadはBase64URLでエンコードされているだけなので、鍵がなくても誰でもデコードできます。Base64URLは、通常のBase64の「+」「/」を「-」「_」に置き換え、末尾の「=」を省略した形式です。JavaScriptでデコードする場合は、文字を戻してからパディングを補います。
function decodeJwtPart(part) {
const base64 = part.replace(/-/g, "+").replace(/_/g, "/");
const padded = base64 + "=".repeat((4 - (base64.length % 4)) % 4);
const bytes = Uint8Array.from(atob(padded), (c) => c.charCodeAt(0));
return JSON.parse(new TextDecoder().decode(bytes));
}
const [header, payload] = token.split(".").slice(0, 2).map(decodeJwtPart);日本語などを含むpayloadは、atob() の結果をそのまま使うと文字化けするため、TextDecoderでUTF-8として解釈しています。コマンドラインの base64 -d でデコードする場合も、「-」「_」の置き換えやパディング不足でエラーになりやすい点に注意してください。
手早く確認したいときは、このサイトのJWTデコードツールに貼り付ければ、headerとpayloadを整形して表示し、expから有効期限の日時と残り時間も確認できます。iat などの数値を日時に直したいときは、Unixタイムスタンプ変換も便利です。
デコードは「検証」ではない
デコードして中身が読めても、そのトークンが正規に発行されたものかどうかはわかりません。payloadは誰でも書き換えられるため、サーバー側では必ず署名を検証し、改ざんされていないことを確かめてから中身を信用する必要があります。サーバー側で確認すべき主な項目は次のとおりです。
- 署名が正しいか(HS256などのHMACは共有鍵、RS256やES256などは公開鍵で検証する)
- alg が、自分のサービスで想定しているアルゴリズムか
- exp・nbf が期限内か(サーバー間の時刻のずれを考慮して、数十秒程度の猶予を設けることもある)
- iss・aud が想定どおりか(別のサービス向けに発行されたトークンを受け付けない)
これらを自前で実装すると漏れが生じやすいため、実績のあるライブラリ(JavaScriptなら jose など)を使い、許可するアルゴリズムや issuer・audience を明示的に指定して検証するのが基本です。
alg=none とアルゴリズム混同の落とし穴
JWTの仕様には、署名なしを表す「alg: none」が定義されています。過去には、headerの alg を none に書き換えて署名を空にしたトークンを受け付けてしまう脆弱性が、複数のライブラリで見つかりました。トークン自身が宣言する alg をそのまま信じず、サーバー側で許可するアルゴリズムを固定することが重要です。
同じ理由で「アルゴリズム混同(algorithm confusion)」と呼ばれる攻撃もあります。RS256(公開鍵で検証する方式)を使うサービスに対して、攻撃者が alg を HS256 に書き換え、公開されている公開鍵をHMACの共有鍵として使って署名すると、実装によっては正しい署名と判定されてしまうというものです。これも、検証時に許可するアルゴリズムと鍵の種類を明示することで防げます。
また、headerの kid(鍵ID)や jku(鍵セットのURL)を検証せずに使うと、攻撃者が用意した鍵で検証させられるおそれがあります。鍵の取得先はサーバー側の設定で決めておきましょう。
本番のトークンを扱うときの注意
有効期限内のアクセストークンは、それ自体がログイン状態を表す「鍵」です。本番環境のトークンを、どのような処理をしているかわからないWebサイトに貼り付けるのは避けてください。このサイトのJWTデコードツールはブラウザ内だけで処理し、トークンを外部に送信しませんが、業務で使う場合は社内のルールに従い、できれば期限切れのトークンや開発環境のトークンで確認すると安全です。
payloadは暗号化されていないので、パスワードや個人情報など、第三者に読まれて困る情報を入れてはいけません。ログやエラーレポートにトークン全体を出力しないことも大切です。
署名検証の動作確認には、JWTエンコード・署名ツールでテスト用のトークンを作り、payloadを書き換えたトークンや期限切れのトークンが正しく拒否されるかを試すと効果的です。