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 をそのまま信じて処理を切り替えるのではなく、APIや用途ごとに許可するアルゴリズムを固定します。想定外のアルゴリズムや none は拒否します。ペイロードには代表的なクレームが入る
ペイロードの中身はクレームと呼ばれる名前付きの値です。RFC 7519では登録済みクレーム名が定義されています。すべて必須ではありませんが、認証・認可の実装では有効期限や発行者を検証できるように設計します。
| クレーム | 意味と使いどころ |
|---|---|
iss | issuer。JWTを発行した認証サーバーやサービスを表す |
sub | subject。ユーザーIDなど、JWTの対象を表す |
aud | audience。JWTを受け取ってよいAPIやサービスを表す |
exp | expiration time。この時刻以降は受け付けない |
nbf | not before。この時刻より前は受け付けない |
iat | issued at。JWTが発行された時刻 |
jti | JWT 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認証で必ず検証すること
署名が正しいだけでは、認証として十分とは限りません。検証時には、少なくとも次の観点を確認します。
- 署名が正しいこと
algが許可したアルゴリズムであることexpを過ぎていないことnbfより前ではないことissが期待する発行者であることaudがこのAPI向けであること- 必要な権限やスコープがあること
JWTはステートレスに扱いやすい反面、一度発行したトークンを即座に無効化しづらい性質があります。アクセストークンは短命にし、必要に応じてリフレッシュトークン、失効リスト、セッション管理を組み合わせます。
まとめ
JWTは、ヘッダー、ペイロード、署名の3つからなるコンパクトなトークン形式です。署名はBase64URL化したヘッダーとペイロードをつなげた文字列に対して作られ、改ざん検出に使われます。HS256では同じ共有秘密鍵で署名と検証を行い、鍵は最低256ビット、32バイト以上のランダム値が必要です。RS256では秘密鍵で署名し、公開鍵で検証するため、検証者を増やす構成に向いています。
詳細は RFC 7519(JWT)、RFC 7515(JWS)、RFC 7518(JWA) を参照してください。