JWTとは何か

認証を実装したことがある人なら、必ず eyJhb... で始まる謎の文字列を目にしたことがあるはずです。これが JWT(JSON Web Token、「ジョット」と読みます) です。JWTは2者の間で「クレーム(主張)」の集合をやり取りするための、コンパクトでURLセーフな方式で、RFC 7519 で標準化されています。現在ではREST APIのステートレス認証における事実上の標準となっています。

JWT (JSON Web Token) の仕組みとデバッグ方法:構造・クレーム・安全な検証

ここで重要なキーワードが ステートレス です。サーバー側にセッション情報を保存する従来のセッション認証と違い、JWTは必要な情報をトークン自身の中に持ちます。サーバーは何も照会する必要がなく、署名を検証して中身を信頼するだけで済みます。

生のJWTは次のような見た目をしています。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTczNjkwMDgwMH0.Xe4kJl7YQoLPkZ3r9Vv2mHgGqNtWbF8sDcA1eRfuByk

ドット(.)で区切られた3つのBase64Urlエンコード済みセグメント。それぞれに役割があります。

3つの構造

1. ヘッダー(Header)

ヘッダーはトークン自身を説明するJSONオブジェクトで、主にどのアルゴリズムで署名されたかを示します。

{
  "alg": "HS256",
  "typ": "JWT"
}

alg は署名アルゴリズムを指定します。よく使われる値は以下のとおりです。

アルゴリズム種類説明
HS256共通鍵(HMAC-SHA256)署名と検証で同じシークレットを共有
RS256公開鍵(RSA-SHA256)秘密鍵で署名、公開鍵で検証
ES256公開鍵(ECDSA-SHA256)RSAより鍵が小さく、採用が増加中
none署名なし危険。本番では絶対に受け入れない

2. ペイロード(Payload)

ペイロードには実際のデータが入ります。各データは クレーム(claim) と呼ばれます。

{
  "sub": "user_123",
  "name": "山田太郎",
  "role": "admin",
  "iat": 1736814400,
  "exp": 1736900800
}

クレームには3種類あります。

登録済みクレーム(RFC 7519で標準化)

  • iss(Issuer)— トークンの発行者。例:"https://auth.example.com"
  • sub(Subject)— トークンの対象となる主体。通常はユーザーID
  • aud(Audience)— このトークンが向けられた対象
  • exp(Expiration Time)— これ以降は拒否すべきUNIXタイムスタンプ
  • iat(Issued At)— トークンが作成されたUNIXタイムスタンプ
  • nbf(Not Before)— これ以前は受け入れてはならないUNIXタイムスタンプ

公開クレーム — IANAのJWTクレームレジストリで定義されるか、衝突を避けるためURI名前空間を使って定義されます。

プライベートクレーム — 当事者間で合意したカスタムクレーム。上の例の role などがこれにあたります。

重要な注意: ペイロードは エンコード されているだけで、暗号化 はされていません。トークンを持つ人は誰でもBase64デコードすれば中身を読めます。パスワードやクレジットカード番号などの機密情報をペイロードに入れてはいけません。

3. 署名(Signature)

署名はHS256の場合、次のように計算されます。

HMAC-SHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secretKey
)

署名こそがJWTを「改ざん検知可能」にしている要素です。ヘッダーやペイロードを1文字でも変更すると署名が一致しなくなり、トークンは拒否されます。サーバーはトークンをどこにも保存する必要がなく、期待される署名を再計算して提供された署名と照合するだけです。

実際の認証フロー

典型的な流れは次のとおりです。

  1. ログイン — ユーザーが認証エンドポイントに認証情報(ユーザー名+パスワード)を送信する。
  2. トークン発行 — サーバーが認証情報を検証し、シークレットまたは秘密鍵で署名したJWTを作成してクライアントに返す。
  3. 以降のリクエスト — クライアントはJWTを Authorization ヘッダーに含める:Authorization: Bearer <token>
  4. 検証 — サーバーが署名を検証し、exp を確認し、クレームを読む。すべてDB照会なしで完結する。
// サーバー側の検証例(Node.js / jose ライブラリ)
import * as jose from 'jose';

async function verifyRequest(authHeader) {
  const token = authHeader?.split(' ')[1];
  if (!token) throw new Error('トークンがありません');

  const secret = new TextEncoder().encode(process.env.JWT_SECRET);
  const { payload } = await jose.jwtVerify(token, secret, {
    issuer: 'https://auth.example.com',
    audience: 'https://api.example.com',
  });

  return payload; // { sub, role, exp, ... }
}

JWT とセッショントークンの違い

どちらも有効な方式で、アーキテクチャによって適切な選択は変わります。

観点JWT(ステートレス)セッショントークン(ステートフル)
サーバー保存不要セッションストア(Redis、DB)が必要
失効難しい(期限切れを待つか失効リストが必要)簡単(ストアから削除するだけ)
水平スケール容易(どのサーバーでも検証可能)共有ストアが必要
トークンサイズ大きい(クレームを持つ)小さい(IDのみ)
向いている用途マイクロサービス、API、モバイル従来型のWebアプリ

よくあるセキュリティの落とし穴

alg: none 攻撃

初期のJWTライブラリの一部は alg: none(署名不要)を許容していました。攻撃者は署名を取り除き、任意のペイロードを偽造できてしまいます。サーバーが受け入れるアルゴリズムを明示的に指定し、none は必ず拒否してください。

アルゴリズム混同攻撃(RS256 → HS256)

サーバーがRS256(非対称)を期待しているのに、攻撃者がヘッダーをHS256(対称)に変更すると、一部のライブラリは 公開鍵 をHMACのシークレットとして使ってしまいます。公開鍵は公開されているため、攻撃者は有効なトークンを偽造できます。対策は、サーバー側で受け入れるアルゴリズムを固定することです。

localStorage への保存

localStorage に保存したトークンはページ上の任意のJavaScriptからアクセスでき、XSS攻撃に対して脆弱です。JavaScriptからアクセスできない HttpOnly Cookieを優先しましょう。

長すぎる有効期限

exp を1週間や1か月に設定したくなりますが、それは盗まれたトークンが長期間有効であり続けることを意味します。短命なアクセストークン(15〜60分)と、安全に保管した長命なリフレッシュトークンを組み合わせましょう。

JWTを手動でデコードする

ペイロードはBase64Urlエンコードされているだけなので、ライブラリなしでもデコードできます。

function decodeJWT(token) {
  const parts = token.split('.');
  if (parts.length !== 3) return null;

  // Base64Url → Base64 → デコード
  const decode = (str) =>
    JSON.parse(atob(str.replace(/-/g, '+').replace(/_/g, '/')));

  return {
    header:  decode(parts[0]),
    payload: decode(parts[1]),
    // 署名(parts[2])はシークレット/公開鍵なしには検証できない
  };
}

デバッグにはこれで十分です。ただし、デコードは検証とは別物です。クレームを信頼する前に、必ずサーバー側で署名を検証してください。

401エラーを調べるときの見方

APIが 401 Unauthorized を返すときは、まずPayloadの expiatissaud を確認します。exp が現在時刻より前なら単純な期限切れ、aud が別サービス向けならトークンの発行先違い、iss が想定と違うなら環境の混在を疑えます。exp はUNIX時間で入ることが多いため、UNIXタイム変換ツール と合わせて確認すると効率的です。

ブラウザ完結だから、安心してデバッグできる

本番環境のトークンをデバッグするときは、ジレンマに直面します。中身を見たいけれど、実際のユーザーのJWTを第三者のオンラインツールに貼り付けるのはセキュリティリスクです。トークンにはユーザーID・権限・その他のメタデータが含まれている可能性があります。

DevToolKitsの JWTデバッガー は、入力されたトークンを あなたのブラウザ内だけで 解析します。サーバーへのデータ送信は一切ありません。ブラウザのネットワークタブを開けば、トークンを貼り付けても外部リクエストがゼロであることを自分で確認できます。シークレットや公開鍵を渡せば、クライアントサイドで署名検証も可能です。

よくある質問

JWTの最大サイズはどれくらいですか?

仕様上の厳密な上限はありませんが、現実的な制約があります。HTTPヘッダーには制限があり(通常1ヘッダー8KB程度)、ブラウザのCookieサイズも約4KBに制限されています。ペイロードは必要なクレームだけに絞り、軽量に保ちましょう。

有効期限前にJWTを無効化できますか?

ネイティブにはできません。これがステートレストークンのトレードオフです。一般的な回避策は、(1) jti クレームに入れたトークンIDをサーバー側の失効リストで管理する、(2) 非常に短い有効期限を使う、(3) 署名鍵をローテーションして古い鍵の受け入れ期間を短くする、などです。

JWE(暗号化)とJWS(署名)の違いは?

通常のJWTは JWS(JSON Web Signature) で、ペイロードは読めますが改ざん検知が可能です。一方 JWE(JSON Web Encryption) はペイロードを暗号化し、復号鍵なしには中身を読めません。JWSの方が圧倒的に一般的で、JWEはペイロード自体を秘匿する必要がある場合にのみ使います。

シークレットは全サービスで同じにすべき?

いいえ。サービスごと、またはトークン種別ごと(アクセストークンとリフレッシュトークン)に別々の署名鍵を使いましょう。1つの鍵が漏れたときの被害範囲を限定でき、鍵のローテーションも容易になります。

トークンのリフレッシュはどう扱う?

短命なアクセストークン(例:15分)と、HttpOnly Cookieに保存した長命なリフレッシュトークン(例:7日)を発行します。アクセストークンが切れたら、クライアントはリフレッシュトークンを専用エンドポイントに送り、新しいアクセストークンを得ます。リフレッシュトークンは使うたびにローテーション(無効化して再発行)するのが安全です。