この記事の結論JWTは header.payload.signature という3要素で構成されます。ペイロードは暗号化されているわけではないため秘密情報を入れず、署名検証、exp、iss、aud などの確認をサーバー側で行います。HS256を使うなら、共有秘密鍵は最低でも256ビット、つまり32バイト以上の暗号学的にランダムな値にします。

JWTは3つの要素でできている

JWTはピリオドで区切られた3つの文字列です。前から順に、ヘッダー、ペイロード、署名を表します。

base64url(header).base64url(payload).base64url(signature)
要素役割
ヘッダー署名アルゴリズムなど、JWTの処理方法を示す。代表例は alg と typ
ペイロードユーザーID、有効期限、発行者などのクレームを入れる本文
署名ヘッダーとペイロードが改ざんされていないことを検証するための値

ヘッダーとペイロードはBase64URLエンコードされます。これは読みやすい文字へ変換しているだけで、暗号化ではありません。ブラウザの開発者ツールや手元のスクリプトで復元できるため、パスワード、APIキー、個人情報などの秘密情報は入れません。

ヘッダーにはアルゴリズムを書く

ヘッダーはJWTの処理に必要なメタデータです。認証でよく見るJWTでは、次のようなJSONが使われます。

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

alg は署名アルゴリズムです。HS256 はHMAC-SHA-256、RS256 はRSA署名とSHA-256を使います。typ はトークン種別を示す補助的な値で、JWTでは一般に JWT を入れます。

algはヘッダー任せにしない検証側は、受け取ったJWTの alg をそのまま信じて処理を切り替えるのではなく、APIや用途ごとに許可するアルゴリズムを固定します。想定外のアルゴリズムや none は拒否します。

ペイロードには代表的なクレームが入る

ペイロードの中身はクレームと呼ばれる名前付きの値です。RFC 7519では登録済みクレーム名が定義されています。すべて必須ではありませんが、認証・認可の実装では有効期限や発行者を検証できるように設計します。

クレーム意味と使いどころ
ississuer。JWTを発行した認証サーバーやサービスを表す
subsubject。ユーザーIDなど、JWTの対象を表す
audaudience。JWTを受け取ってよいAPIやサービスを表す
expexpiration time。この時刻以降は受け付けない
nbfnot before。この時刻より前は受け付けない
iatissued at。JWTが発行された時刻
jtiJWT ID。トークンを一意に識別したいときに使う
{
  "iss": "https://auth.example.com",
  "sub": "user_12345",
  "aud": "https://api.example.com",
  "exp": 1787043600,
  "iat": 1787040000,
  "scope": "articles:read"
}

scope や role のような独自クレームもよく使われます。ただし、権限は後から変わることがあります。長すぎる有効期限のJWTに強い権限を入れると、権限変更やアカウント停止をすぐ反映しにくくなります。

署名はどう作られるのか

署名は、ヘッダーとペイロードをそれぞれBase64URLエンコードし、ピリオドでつなげた文字列に対して計算します。署名対象は次の形です。

signing_input =
  BASE64URL(UTF8(header)) + "." + BASE64URL(UTF8(payload))

HS256の場合は、この signing_input に対して共有秘密鍵でHMAC-SHA-256を計算します。最後にその結果をBase64URLエンコードし、3つ目の要素として連結します。

signature = BASE64URL(HMAC-SHA-256(signing_input, shared_secret))
jwt = signing_input + "." + signature

検証側は同じ入力から署名を再計算し、JWTに付いている署名と一致するかを確認します。ペイロードの sub や role を1文字でも書き換えると、署名が一致しなくなるため改ざんを検出できます。

HS256は共有秘密鍵で署名する

HS256 はHMAC-SHA-256です。署名する側も検証する側も、同じ共有秘密鍵を持ちます。単一のバックエンド内でJWTを発行・検証する構成では扱いやすく、実装も比較的シンプルです。

観点HS256の注意点
鍵の共有検証できるサーバーは同じ鍵で署名もできる。検証先が増えるほど秘密鍵の管理範囲が広がる
鍵長RFC 7518では、HS256の鍵はSHA-256の出力サイズ以上、つまり256ビット以上が必要
生成方法人間が考えた文字列ではなく、CSPRNGで生成した32バイト以上のランダム値を使う

「32文字以上」ではなく「32バイト以上のランダムな鍵」と捉えるのが重要です。たとえば英単語をつなげた32文字の文字列は、見た目が長くても推測しやすい場合があります。生成例は次のようになります。

# 32 bytes = 256 bits
openssl rand -base64 32

Base64で表示すると44文字程度に見えますが、元のランダムデータは32バイトです。環境変数に保存する場合も、ログへ出さず、リポジトリへコミットせず、定期的なローテーション手順を用意します。

RS256は秘密鍵と公開鍵を分ける

RS256 はRSA署名とSHA-256を使います。発行側は秘密鍵で署名し、検証側は公開鍵で署名を検証します。公開鍵だけでは署名を作れないため、複数のAPIや外部サービスに検証だけを任せたい構成に向いています。

観点RS256の特徴
鍵の分離秘密鍵は発行者だけが保持し、検証側には公開鍵を配布できる
公開鍵配布JWKSエンドポイントなどで公開鍵を配り、kid で鍵を選べるようにする構成が多い
運用鍵ペアの生成、ローテーション、古い鍵の保持期間などを設計する必要がある

マイクロサービスや外部連携では、RS256の方が権限境界を作りやすいことがあります。一方、単一アプリ内で完結するならHS256でも十分な場合があります。大切なのは、アルゴリズムを混在させず、検証側で期待する方式と鍵を明示することです。

JWT認証で必ず検証すること

署名が正しいだけでは、認証として十分とは限りません。検証時には、少なくとも次の観点を確認します。

  1. 署名が正しいこと
  2. alg が許可したアルゴリズムであること
  3. exp を過ぎていないこと
  4. nbf より前ではないこと
  5. iss が期待する発行者であること
  6. aud がこのAPI向けであること
  7. 必要な権限やスコープがあること

JWTはステートレスに扱いやすい反面、一度発行したトークンを即座に無効化しづらい性質があります。アクセストークンは短命にし、必要に応じてリフレッシュトークン、失効リスト、セッション管理を組み合わせます。

まとめ

JWTは、ヘッダー、ペイロード、署名の3つからなるコンパクトなトークン形式です。署名はBase64URL化したヘッダーとペイロードをつなげた文字列に対して作られ、改ざん検出に使われます。HS256では同じ共有秘密鍵で署名と検証を行い、鍵は最低256ビット、32バイト以上のランダム値が必要です。RS256では秘密鍵で署名し、公開鍵で検証するため、検証者を増やす構成に向いています。

詳細は RFC 7519(JWT)、RFC 7515(JWS)、RFC 7518(JWA) を参照してください。