JWT トークンをデコードして検査する方法
JSON Web Tokens(JWT)は、モダンなWebアプリケーションで認証を扱う最も一般的な方法です。認証で何かがうまくいかないとき(ユーザーが予期せずログアウトされた、権限が間違っている、APIが401を返す)、JWTのデコードは通常、最初のデバッグステップです。JWTの3つの部分、標準的なクレーム、署名に使えるアルゴリズム、よくある落とし穴を理解すると、認証のデバッグは魔法のようなものから日常的な確認作業になります。
JWTの簡単な歴史
JWTは2015年5月にRFC 7519として標準化されました。それまでIETFで数年にわたるドラフトの反復がありました。フォーマットは初期のコンパクトトークン設計(SAMLアサーション、シンプルな不透明クッキー)から借りていますが、それらに欠けていた2つを加えました。あらゆる言語で読める厳格なJSON形状と、URLパラメーター、HTTPヘッダー、フォームフィールドを再エスケープなしで通過できるbase64url安全エンコーディングです。仲間の仕様、署名のためのJWS(RFC 7515)、暗号化のためのJWE(RFC 7516)、アルゴリズム名のためのJWA(RFC 7518)が、合わせてJOSE(JavaScript Object Signing and Encryption)ファミリーを形成します。
OAuth 2.0とOpenID Connectは間もなくJWTをデフォルトのトークンフォーマットとして採用しました。これが、ほぼすべてのモダンな認証プロバイダー(Auth0、Okta、Cognito、Keycloak、Firebase、Supabase、Clerk)が今日JWTを発行している理由です。自己完結型トークンとステートレスバックエンドの組み合わせは、マイクロサービスとAPIゲートウェイに非常に自然な形でフィットすることが分かりました。難点はJWTが悪用しやすいことで悪名高く、過去10年間、アルゴリズムを慎重に検証しなかったライブラリで安定したCVEの流れが生まれてきました。
JWTの中身
JWTはドットで区切られた3つの部分を持ちます:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U
ヘッダー: アルゴリズム(HS256、RS256など)とトークンタイプを含みます。
{"alg": "HS256", "typ": "JWT"}
ペイロード: ユーザーとトークンに関するクレーム(データの主張)を含みます。
{"sub": "1234567890", "name": "Alice", "exp": 1700000000}
署名: トークンが改ざんされていないことを検証する暗号学的ハッシュです。署名鍵なしではこれを読めません。
各セクションはbase64urlエンコードされています。これは+と/の代わりに-と_を使い、末尾の=パディングを省略します。Base64urlは暗号化ではありません。真ん中のセグメントを任意のデコーダーに貼り付けるだけでペイロードが見えます。それは設計上の意図です。中間セグメントは経路上のサービスが読めるように設計されており、署名だけが信頼性を証明する部分です。
よくあるJWTクレーム
標準クレームはIANAに登録され、RFC 7519で定義されています。ほとんどは任意ですが、以下のものはほぼ常に存在します。
| クレーム | 正式名 | 含まれる内容 |
|---|---|---|
sub | Subject | ユーザーIDまたは識別子 |
exp | Expiration | トークンの有効期限のUnixタイムスタンプ |
iat | Issued At | トークンが作成されたUnixタイムスタンプ |
iss | Issuer | トークンを作成した者(認証サーバー) |
aud | Audience | トークンが対象とする者 |
nbf | Not Before | この時刻より前はトークンは有効ではない |
jti | JWT ID | トークンの一意な識別子 |
azp | Authorized Party | トークンが発行された相手(OIDC) |
scope / scp | OAuthスコープ | 付与された権限、スペース区切りが多い |
email | 標準的なOIDCユーザー識別子 | |
name | Name | 表示名(OIDC) |
nonce | Nonce | OIDCのリプレイ保護値 |
kid(ヘッダー) | Key ID | どの署名鍵が使われたか(JWKSルックアップ用) |
標準セットを超えて、アプリケーションは独自のカスタムクレーム(roles、tenant_id、feature_flags、permissions)を追加します。カスタムクレーム名はデフォルトでは名前空間化されていないため、2つの異なるサービスが同じ名前を異なる意味で使うことがあります。URIで接頭する(https://myapp.com/roles)というOIDCの慣習が衝突を回避します。
JWTをデコードする手順
- トークンを貼り付ける: 完全なJWT(header.payload.signature形式)をデコーダーに入力します。ブラウザベースのデコーダーはローカルで処理するため、トークンはページを離れません。
- デコードされたセクションを表示: ツールはヘッダー(アルゴリズム)、ペイロード(クレーム)、署名を整形されたJSONとして表示し、タイムスタンプはUnix整数と人間が読める日付の両方として示します。
- クレームを確認する: 有効期限、発行者、サブジェクト、オーディエンス、認可ロジックを駆動するカスタムクレームを調べます。
- 期待値と比較する: 発行者を設定した認証プロバイダーと、オーディエンスをトークンが送られるAPIと、ロール/スコープクレームをユーザーが持つべき権限と相互参照します。
- 時刻のテスト:
iat、nbf、expにマウスを乗せて、トークンが現在有効か、まもなく期限切れか、クロックスキューの許容範囲を超えるほど前に発行されたかを確認します。
署名アルゴリズム
すべてのJWTが同じ暗号を使うわけではありません。algヘッダーが署名がどのファミリーに属するかを示し、それぞれが大きく異なるセキュリティ特性を持ちます。
| アルゴリズム | ファミリー | 鍵の種類 | 選ぶべきとき |
|---|---|---|---|
HS256 | HMAC | 共有秘密 | 単一サービスのアプリ。チームをまたいで秘密を共有しない |
HS384 / HS512 | HMAC | 共有秘密 | HS256と同じだがダイジェストが長い |
RS256 | RSA | 公開鍵/秘密鍵ペア | OIDCで最も一般的。検証側は公開鍵だけが必要 |
RS384 / RS512 | RSA | 鍵ペア | RS256と同じだが鍵が大きい |
PS256 / PS384 / PS512 | RSA-PSS | 鍵ペア | モダンなRSA、新規導入ではRSより推奨 |
ES256 / ES384 / ES512 | ECDSA | 楕円曲線鍵ペア | RSAより鍵が小さく、検証が高速 |
EdDSA | Ed25519 | Edwards曲線鍵ペア | 最新、最小、最速。まだ普遍的ではない |
none | なし | なし | 本番では禁止。一部の古いライブラリはまだ受け付ける |
非対称アルゴリズム(RS*、PS*、ES*、EdDSA)は、どのサービスも公開鍵だけでトークンを検証できるようにします。これがOIDCで主流である理由です。対称(HS*)は単一アプリケーション内では問題ありませんが、複数のコンシューマーにまたがるローテーションや配布は悪夢になります。
JWTでのデバッグ
トークン期限切れ? expクレームを確認します。Unixタイムスタンプを人間が読める日付に変換します。過去なら、トークンは期限切れでリフレッシュが必要です。ほとんどのJWTライブラリは期限切れトークンをデフォルトで拒否します。アプリがそれを受け付けるなら、それはセキュリティのバグです。
権限が違う? ペイロード内のロールやスコープのクレームを探します。これらは実装によって異なりますが、しばしば"role": "admin"や"scope": "read write profile"のように見えます。
ユーザー識別の問題? subクレームがユーザーを識別します。期待するユーザーIDと一致するか検証します。一部のプロバイダーは不透明なGUIDを使い、他はメールアドレスを使うことに注意してください。デコーダーが実際に何があるかを見せます。
トークンが受け付けられない? aud(オーディエンス)クレームを確認します。APIが特定のオーディエンス値を期待していて、トークンが別のものを持っていれば拒否されます。オーディエンスの不一致はトークンを誤ったサービスにルーティングした症状としてよくあります。
デプロイ後の401エラー? iss(発行者)クレームを確認します。新しい認証プロバイダーのテナントや切り替わった署名鍵は発行者URLを変えます。検証側がまだ古いものを信頼していると、すべてのトークンが無効に見えます。
クロックスキューの問題? iatがわずかに未来、またはexpがわずかに過去なら、サーバーの時計がずれている可能性があります。ほとんどのJWTライブラリは数秒の余裕を許可します。そうでない場合、NTPで同期した時計が問題を解決します。
よくある落とし穴
- 許可リストなしで
algヘッダーを信頼する: 古典的なJWT脆弱性は、サーバーがトークンが使うと言ったどんなアルゴリズムも受け付けることでした。alg: none(署名なし)やalg: HS256(あなたの公開鍵を秘密として署名)のトークンは、どんなペイロードも偽造できます。検証側を期待される正確なアルゴリズムに固定してください。 - ペイロードに秘密情報を入れる: ペイロードはbase64urlエンコードされており、暗号化されていません。トークンを持つ誰もが読めます。パスワード、APIキー、クエリ文字列に入れたくないものは絶対に入れないでください。
- 取り消しなしの長寿命トークン: 30日のJWTは、トークンブラックリストやセッションストアなしには取り消せません。アクセストークンを短く(5から60分)保ち、長いセッションにはリフレッシュトークンフローを使ってください。
- クロックスキューを忘れる: 異なるタイムゾーンや時計のずれたサーバーは、有効なはずのトークンを拒否します。
expとnbfに30から60秒の余裕を許してください。 - 検証なしで発行者を信頼する:
issクレームはペイロードの一部で、誰もが書けます。その値が設定された発行者と一致することの検証は必須です。許可リスト化することで、攻撃者があなたのプロバイダーから来たと主張するトークンを偽造することを防ぎます。 - 環境間でHS256秘密を再利用する: 開発と本番の同じ秘密は、漏洩した開発トークンが本番で動作することを意味します。環境ごとの鍵、理想的にはシークレットマネージャーから取得した鍵を使ってください。
- localStorageにトークンを保存する: localStorageはあらゆるJavaScriptから読めるため、1つのXSSバグですべてのユーザーのトークンが漏れます。HttpOnlyクッキーとSameSite=Lax(またはStrict)がより安全なデフォルトです。
- 完全なトークンをログに記録する: 完全なJWTをキャプチャするアプリケーションログは、ログアクセスを持つ誰にでもトークンを漏らします。最初の10文字に切り詰めるか、
jtiだけをログに記録してください。 kidのローテーションを無視する: プロバイダーが署名鍵をローテーションすると、新しいトークンは新しいkidヘッダーを持ちます。JWKSを永遠にキャッシュする検証側は、有効なトークンを拒否し始めます。鍵IDのミス時にJWKSを再取得してください。- JWTとセッションを一貫性なく混ぜる: 一部のエンドポイントはJWTの背後、他はクッキーセッションの背後、これはログイン中のユーザーがあるルートで認証されていないように見えるバグにつながります。サービスごとに1つのモデルを選んでください。
JWTの代替手段
JWTは主流ですが唯一の選択肢ではありません。各代替案は異なる特性とのトレードオフです。
| メカニズム | 強み | 弱み |
|---|---|---|
| JWT(JWS) | 自己完結型、サービス間で扱いやすい | 追加のステートなしには取り消せない |
| 不透明トークン + イントロスペクション | 取り消しが容易、クレームを隠せる | 毎リクエストが認証サーバーに当たる |
| サーバーサイドセッション | 最もシンプルなモデル、即座の取り消し | サービス間でスケールしにくい |
| PASETO | より安全なJWTの置き換え(algの混乱がない) | エコシステムが小さい |
| Macaroons | 組み込みの減衰(委譲権) | 限定的なライブラリサポート |
| OAuth 2.0 + JWTアクセストークン | APIの業界標準 | 仕様が大きく、誤実装しやすい |
| OIDC IDトークン | 標準的なユーザー識別 + JWT | アクセストークンとよく混同される |
| mTLSクライアント証明書 | トランスポート層で最強の認証 | 証明書管理のオーバーヘッド |
ほとんどのチームにとって選択肢はJWTか不透明トークンです。検証が安価でオフラインである必要があるときはJWTが勝ち、取り消しが即座である必要があるときは不透明トークンが勝ちます。
プライバシーとデコーダー
JWTデコーダーは完全にブラウザ内で動作します。貼り付けたトークンは分割され、base64urlでデコードされ、JSONはネットワークリクエストなしでパースされ、整形されます。デコードされたトークンのログも、含まれるクレームの分析も、誰のためにデバッグしていたかを再構築する方法もありません。JWTはしばしばユーザー識別子、メールアドレス、内部のロール名、テナントIDを含みます。これらはまさに第三者のサーバーに送りたくない種類のメタデータです。クライアントサイドでデコードすることは、その情報をマシン上に保ち、認証に触れるどんなデバッグタスクにとっても正しいデフォルトです。
よくある質問
デコーダーで JWT の署名を検証できますか?
いいえ。署名検証にはサーバーに保管されている署名シークレットまたは公開鍵が必要です。デコーダーはトークンの中身を表示しますが、暗号学的検証はバックエンドで行わなければなりません。本番環境では検証されていない JWT を信頼してはいけません。
JWT をオンラインツールに貼り付けても安全ですか?
ツールがブラウザ内で動作する場合は安全です。ブラウザベースのデコーダーはトークンをローカルで処理し、サーバーには何も送信しません。トークンをネットワーク経由で送信するツールは避けてください。
exp クレームとは何ですか?
exp(expiration)クレームはトークンが期限切れになる時刻を示す Unix タイムスタンプです。この時刻を過ぎたトークンは拒否すべきです。認証問題のデバッグでは必ずこのクレームを確認してください。
JWT を暗号化することはできますか?
標準的な JWT(JWS)は署名されますが暗号化されません, 誰でもペイロードをデコードできます。JWE(JSON Web Encryption)トークンは暗号化されていますが、あまり一般的ではありません。標準の JWT ペイロードに機密データ(パスワードやシークレット)を入れることは絶対に避けてください。
What is the alg none vulnerability?
Early JWT libraries accepted tokens with an alg header set to "none", meaning the signature could be omitted entirely. An attacker who set this header could forge any payload. Modern libraries reject "none" by default, but legacy systems may still be exposed; always allow-list the expected algorithm rather than trusting the header.
How should I store a JWT on the client?
HttpOnly secure cookies with SameSite=Lax (or Strict) are the safest default; they cannot be read by JavaScript, which mitigates XSS token theft. localStorage is convenient but vulnerable to any XSS bug. Never store long-lived JWTs alongside untrusted scripts.