JWT Tokens Explained: Structure, Security & Decoding

Understand JSON Web Tokens from the inside out. Learn the header, payload, and signature structure, common claims, signing algorithms, and security best practices.

JSONTech チームFebruary 1, 202510 min read

JWTとは何か

JSON Web Token(JWT、発音は "jot"〔ジョット〕)は、当事者間のクレームを表すコンパクトでURLセーフな文字列です。現代のWebアプリケーションにおける認証と認可を扱う際、最も一般的な方式のひとつです。

Webアプリにログインしたとき、サーバーがサーバーサイドのセッションを作らずにトークンを返す場合、そのトークンがJWTであることが多いです。ブラウザは以降のリクエストごとにそれを送り、サーバーはデータベースに問い合わせずに署名を検証できます。

JWTが使われる場所

  • 認証。 ユーザーがログインすると、サーバーがJWTを発行します。クライアントはそれを保持し、リクエストごとに Authorization ヘッダーで送ります。
  • シングルサインオン(SSO)。 JWTにより、ユーザーは一度認証すれば複数サービスにアクセスできます。Auth0、Okta、Keycloakなどのアイデンティティプロバイダーが発行するJWTは、下流のサービスがそれぞれ独立して検証できます。
  • APIの認可。 マイクロサービス同士がJWTを受け渡し、認証済みユーザーに代わって、特定の権限を伴うリクエストであったことを示します。
  • 情報の受け渡し。 JWTは署名されているため、追加の検証手順なしに当事者間で信頼できるデータを運べます。

JWTの3つの構成要素

JWTは、ドットで区切られた3つのBase64URLエンコード部分からなります。

header.payload.signature

次の例は実際のJWTです(読みやすさのため省略しています)。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

各部分をデコードして確認します。

第1部: ヘッダー

ヘッダーはトークンの種類と署名アルゴリズムを表します。Base64URLをデコードすると次のとおりです。

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

alg は検証時に使うアルゴリズムを示し、typ はこれがJWTであること(JWEなど別種のトークンではないこと)を示します。

第2部: ペイロード(クレーム)

ペイロードにはクレーム、すなわちトークンが運ぶ実データが入ります。デコードすると次のとおりです。

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

クレームはキーと値のペアです。標準化されたもの(登録クレーム)もあれば、必要に応じてカスタムクレームも追加できます。

ペイロードはBase64URLエンコードされているだけで、暗号化されていません。トークンを入手した者は誰でもデコードして内容を読めます。秘密情報、パスワード、機微な個人データをJWTのペイロードに入れないでください。

第3部: 署名

署名は、エンコード済みヘッダー、ドット、エンコード済みペイロードをつなげたものを秘密鍵で署名して計算します。

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

署名により、トークンが改ざんされていないことが保証されます。ヘッダーやペイロードの1文字でも変えると署名が一致せず、検証側はトークンを拒否します。

試してみる: 任意のJWTを JWT Decoder に貼り付けると、3つの部分をすぐに確認できます。

よく使われるJWTクレーム

JWTの仕様(RFC 7519)では、登録クレームの集合が定義されています。いずれも必須ではありませんが、広く使われています。

クレーム名称説明
subSubjectトークンの主体(多くはユーザーID)
issIssuer発行者(例: 認証サーバーのURL)
audAudience想定される受信者(例: APIのURL)
expExpiration TimeこのUnix時刻を過ぎるとトークンは無効
iatIssued Atトークンが発行されたUnix時刻
nbfNot BeforeこのUnix時刻より前はトークンは無効
jtiJWT IDトークンの再利用を防ぐ一意の識別子

カスタムクレームは、これ以外に自分で追加する任意の項目です。rolepermissionsemailorg_id などがよく使われます。カスタムクレームは必要最小限にしてください。バイトが増えるほど、リクエストごとの転送量も増えます。

署名アルゴリズムの比較

アルゴリズムの選び方で、署名の生成と検証の方法が決まります。代表的な3つは次のとおりです。

アルゴリズム種類向いている用途
HS256対称(HMAC)共有秘密鍵1つ発行者と検証者が同一サービスであるシンプルな構成
RS256非対称(RSA)秘密鍵で署名、公開鍵で検証分散システムやSSO。検証側は秘密鍵を不要
ES256非対称(ECDSA)秘密鍵で署名、公開鍵で検証RS256と同様だが鍵が小さく、処理も速い傾向

対称方式(HS256) は設定が簡単で、発行者と検証者が同じ秘密を共有します。一方、検証に関わるすべてのサービスがその秘密にアクセスする必要があり、攻撃面が広がります。

非対称方式(RS256、ES256) は分散構成に適しています。認証サーバーが秘密鍵で署名し、各サービスは対応する公開鍵で検証できます。公開鍵は配布しても問題ありません。ES256は、RS256と同等の強度をより小さなトークンと高速な検証で実現できる、現代的な選択肢です。

セキュリティのベストプラクティス

JWTは強力ですが、誤用も起きやすい仕組みです。次の点を守ってください。

  • ペイロードに秘密を入れない。 ペイロードはエンコードされているだけで暗号化されていないため、誰でもデコードできます。暗号化が必要なら、代わりにJWE(JSON Web Encryption)を検討してください。
  • 有効期限を短くする。 アクセストークンは5〜15分程度で失効させ、長期セッションはリフレッシュトークンで支えます。短命にすれば、漏えい時の被害を抑えられます。
  • 必ずHTTPSを使う。 HTTP上のJWTは転送路で盗聴されるおそれがあります。HTTPSは譲れない前提です。
  • クレームをすべて検証する。 expissaud は必ず確認してください。署名だけを検証して、期限切れや宛先違いのトークンを通さないでください。
  • alg: none を受け入れない。 一部のライブラリは、ヘッダーが "alg": "none" のとき署名なしトークンを受け入れます。検証設定でこのアルゴリズムを明示的に拒否してください。
  • アルゴリズムを許可リストで制限する。 想定しているアルゴリズム(例: RS256のみ)だけを受け入れるよう検証器を設定し、アルゴリズム混同攻撃を防ぎます。
  • トークンの保管を安全にする。 HttpOnly クッキーはJavaScriptから読めないため、localStorage よりXSS対策の観点で有利です。localStorage を使う場合は、そのトレードオフを理解したうえで選んでください。

よくあるJWTの誤り

セキュリティ監査で繰り返し見られるパターンです。

  • JWTをURLに載せる。 クエリ文字列はサーバーログ、ブラウザ履歴、Refererヘッダーなどに残ります。Authorization ヘッダーやクッキーを使ってください。
  • 署名検証を省略する。 Base64でデコードするだけで検証を飛ばすと、攻撃者は任意のトークンを偽造できます。
  • HS256に弱い秘密を使う。 "secret""password123" のような共有秘密は、短時間で総当たりされる可能性があります。少なくとも256ビットの暗号論的乱数から作った文字列を使ってください。
  • 失効しないトークン。 exp がなければトークンは事実上ずっと有効です。漏えいしたあと、署名鍵を回転させない限り失効できません(鍵を変えると全トークンが無効になります)。
  • データを詰め込みすぎる。 JWTはHTTPリクエストごとに送られます。4KBのトークンはAPI呼び出しのたびにオーバーヘッドになります。ペイロードは軽く保ってください。

トークンリフレッシュの流れ

短命のアクセストークンはすぐ期限切れになるため、ユーザーに再ログインさせずに新しいトークンを得る仕組みが必要です。それがリフレッシュトークンフローです。

  1. ユーザーがログインすると、サーバーはアクセストークン(短命、例: 15分)とリフレッシュトークン(長命、例: 7日間)を返します。
  2. クライアントはAPIリクエストごとに Authorization: Bearer ヘッダーでアクセストークンを送ります。
  3. アクセストークンが期限切れになると、APIは 401 Unauthorized を返します。
  4. クライアントはリフレッシュトークンを専用の /refresh エンドポイントに送ります。
  5. サーバーはリフレッシュトークンを検証し、新しいアクセストークン(必要なら新しいリフレッシュトークンも — リフレッシュトークンのローテーション)を発行して返します。
  6. クライアントは新しいアクセストークンで元のリクエストをやり直します。

リフレッシュトークンのローテーションは推奨される対策です。各リフレッシュトークンは一度しか使えません。攻撃者が盗んだトークンと、正当ユーザーの利用が競合すると、サーバーは再利用を検知してセッション全体を無効化できます。

// Simplified refresh flow (client-side)
const handleApiRequest = async (url, options) => {
  let response = await fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      Authorization: `Bearer ${getAccessToken()}`,
    },
  });

  if (response.status === 401) {
    const refreshResponse = await fetch("/api/refresh", {
      method: "POST",
      body: JSON.stringify({ refresh_token: getRefreshToken() }),
    });

    if (refreshResponse.ok) {
      const { access_token, refresh_token } = await refreshResponse.json();
      saveTokens(access_token, refresh_token);

      response = await fetch(url, {
        ...options,
        headers: {
          ...options.headers,
          Authorization: `Bearer ${access_token}`,
        },
      });
    } else {
      redirectToLogin();
    }
  }

  return response;
};

JWTとセッション: どちらを使うか

Web開発でもっとも議論の多い論点のひとつです。どちらにも妥当な用途があります。

観点JWTサーバーサイドセッション
状態の置き場所クライアント(サーバーはステートレス)サーバー(セッションストア/データベース)
スケーラビリティ容易 — サーバー間で共有状態が不要共有セッションストア(Redis、DBなど)が必要
失効難しい — ブロックリストか短い有効期限が必要容易 — ストアからセッションを削除すればよい
ペイロードサイズ大きい(リクエストごとにクレームを運ぶ)小さい(セッションIDのクッキーのみ)
クロスドメインBearerトークンとして送るため相性がよいCORSとクッキー設定が必要
マイクロサービス向いている — 各サービスが独立して検証できる各サービスがセッションストアを参照する必要がある
実装の単純さ署名やリフレッシュなど要素が多い正しく実装するには比較的単純

JWT向きなのは、分散サービス、モバイルアプリ、サードパーティのAPI利用者など、サーバー側をステートレスに保ちたい認証の場面です。

セッション向きなのは、失効を簡単にしたい場合(例: 「すべての端末からログアウト」)、単一のモノリシックアプリである場合、従来型Webアプリで実装を単純に保ちたい場合です。

試してみる: JWT Decoder なら、任意のJWTをすぐにデコードして内容を確認できます。データはブラウザの外に送られません。

関連ツール