JWT(JSON Web Token)の仕組みと正しい使い方 - 署名・検証とセキュリティの落とし穴

JWT(JSON Web Token)の仕組みと正しい使い方 - 署名・検証とセキュリティの落とし穴

作成日:
読了:12
更新日:

ログイン認証やAPI認可で必ず出てくる JWT(JSON Web Token)。「使ってはいるけど仕組みはなんとなく」「localStorage に入れていいの?」「中身って暗号化されてるの?」——このあたりは誤解も多い領域です。

この記事では、JWT の構造・署名と検証・正しい使い方を、RFC(7519/7515/7518/8725)と OWASP を一次ソースに整理します。とくにセキュリティの落とし穴は、知らないと事故るポイントなので重点的に扱います。

JWT とは

JWT は単独の規格ではなく、JOSE(JSON Object Signing and Encryption)という規格群の上に成り立ちます。

RFC内容
RFC 7519JWT 本体(クレーム・構造)
RFC 7515JWS(署名/MAC)
RFC 7516JWE(暗号化)
RFC 7518JWA(使用する暗号アルゴリズム)
RFC 8725JWT のベストプラクティス(BCP 225)

世の中で「JWT」と呼ばれているものの大半は、署名された JWS 形式(後述の3部構造)です。ここがまず重要で、デフォルトの JWS は「改ざん検知(署名)」だけで、暗号化ではありません。中身は誰でも読めます(詳細は後述)。

構造: header.payload.signature

JWS 形式の JWT は、3つの部分をドット(.)で連結した文字列です。各部は Base64URL(URL セーフな Base64)でエンコードされます。

JWT の見た目
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
└──────── header ────────┘ └──── payload ────┘ └──────── signature ────────┘
  • Header(JOSE ヘッダ): アルゴリズムなどのメタ情報
  • Payload(クレームセット): 伝えたい情報(クレーム)
  • Signature: ヘッダとペイロードに対する署名/MAC
Header の例
{ "alg": "HS256", "typ": "JWT" }
Payload(クレーム)の例
{
  "iss": "https://auth.example.com",
  "sub": "1234567890",
  "aud": "https://api.example.com",
  "name": "Taro Yamada",
  "iat": 1300819380,
  "exp": 1300822980
}

代表的なクレーム(RFC 7519)

クレーム意味
iss発行者(issuer)
sub主題=対象の主体(subject)
aud想定受信者(audience)
exp有効期限。この時刻以降は受理してはならない
nbfこの時刻より前は受理してはならない
iat発行時刻(issued at)
jtiJWT の一意な ID

NOTE

exp/iat/nbfUNIX エポック秒(ミリ秒ではない)です。また登録済みクレームの使用はすべて任意で、必須ではありません。

署名アルゴリズム: HS256 と RS256/ES256

RFC 7518 が定めるアルゴリズムのうち、よく使うのは次の2系統です。

種別使いどころ
HMAC(対称鍵)HS2561つの共有秘密鍵で署名も検証も発行者と検証者が同一/信頼関係にある単一システム
公開鍵(非対称)RS256 / ES256秘密鍵で署名、公開鍵で検証発行者と検証者が分離(認可サーバ+複数 API)

ポイントは、HS256 は鍵が1つなので検証側にもその鍵を渡す必要があり、渡した相手はトークンを偽造できてしまうこと。発行者と検証者が別主体なら、公開鍵方式(RS256/ES256)が原則です。検証側には公開鍵だけ配ればよく、OpenID Connect などで標準的に使われます。

検証の流れ

受信側がやるべきことは、署名検証だけではありません

  1. ヘッダの alg自分が許可したアルゴリズムかを確認(許可リスト方式)
  2. 対応する鍵で署名を検証(成功するまでトークンを信用しない)
  3. exp / nbf時刻を検証
  4. iss / audアプリの期待値と一致するか検証(RFC 8725 では検証必須)
Node.js での検証(概念コード)
import jwt from "jsonwebtoken";
 
try {
  const payload = jwt.verify(token, secretOrPublicKey, {
    algorithms: ["RS256"],                 // alg を許可リストで固定(必須)
    issuer: "https://auth.example.com",    // iss 検証
    audience: "https://api.example.com",   // aud 検証
    // exp / nbf はライブラリが自動検証
  });
  // ここで初めて payload を信用してよい
} catch (err) {
  // 署名不一致・期限切れ・iss/aud 不一致など
}

NOTE

「ライブラリに渡せば全部チェックしてくれる」は誤解です。aud/iss の期待値や許可アルゴリズムは呼び出し側が明示しないと検証されない実装が多いので、必ず指定してください(API はライブラリのバージョンで変わるため公式ドキュメントで確認を)。

セキュリティの落とし穴

ここが本題です。JWT の事故はだいたいこのどれかです。

alg:none 攻撃

攻撃者がヘッダの alg"none"(署名なし)に書き換え、署名を空にして送る手口。署名検証をスキップする実装はこれを通してしまいます。RFC 7518 は「none をデフォルトで受理してはならない」と定めています。対策は検証時に許可アルゴリズムを明示すること。

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

RS256(公開鍵)で運用しているシステムに、攻撃者が algHS256 に書き換えて送ると、脆弱な実装は公開鍵を HMAC の共有鍵として検証してしまいます。公開鍵は誰でも入手できるので、任意のトークンを偽造可能になります。対策はやはり許可アルゴリズムをリストで固定すること(RFC 8725)。

「JWT は暗号化」という誤解

最重要の誤解です。JWS 形式の JWT は署名(改ざん検知)であって暗号化ではありません。ペイロードは鍵なしで Base64URL デコードするだけで誰でも読めます

ペイロードは鍵なしで読める
// payload 部分を Base64URL デコードするだけで中身が見える
atob("eyJzdWIiOiIxMjM0NTY3ODkwIn0")  // => {"sub":"1234567890"}

したがってパスワードや個人情報などの秘密をペイロードに入れてはいけません。秘匿が必要なら JWE(RFC 7516)で暗号化します。

失効(revocation)が難しい

JWT はステートレスなので、発行後は exp を迎えるまで基本的に無効化できません。「今すぐログアウトさせる」「漏洩トークンを即無効化」が難しいのが弱点です。対策は、失効リスト(デナイリスト)を持つか、後述の短命トークン+リフレッシュトークン構成です。

保存先リスク備考
localStorageXSS で盗まれる(JS から読める)手軽だが危険
httpOnly CookieXSS には強いがCSRF 対策が別途必要Secure / SameSite を併用

どちらも一長一短で、「これだけやれば安全」はありません。XSS そのものを塞ぐ前提で、用途に応じて選びます。

セッション(サーバ側)との比較

サーバ側セッションJWT
状態サーバが保持(ステートフル)トークンが持つ(ステートレス)
失効即時・容易困難(exp まで有効)
スケール共有ストアが要る横スケールしやすい
向く用途即時失効・長期ログイン・高リスクAPI/マイクロサービス間の認可、発行者と検証者の分離

「ステートレスだからスケールに強い」は本当ですが、失効を実現しようとするとサーバ側状態を足すことになり、その利点は薄れます。即時失効が必須なら、素直にサーバ側セッションが堅実なこともあります。認証方式そのものは パスキー/WebAuthn も参照してください。

実務での推奨

  • 短命なアクセストークン+リフレッシュトークン: アクセストークンは短い exp(例として15〜30分など)にし、更新はサーバ側で管理・失効できるリフレッシュトークンで行う
  • alg は許可リストで固定。発行者と検証者が分離するなら RS256/ES256
  • iss / aud を必ず検証する
  • 検証はライブラリに任せる。自前の署名検証実装は避ける
  • HMAC 秘密鍵は十分に強く。HS256 は最低256ビット、人間が覚えるパスワードを鍵に直接使わない(RFC 8725)

まとめ

  • JWT は header.payload.signature の3部構造(Base64URL)。中身の大半は署名付きの JWS
  • クレームは iss/sub/aud/exp/iat/nbf/jti。時刻はUNIX 秒
  • 署名は HS256(対称)と RS256/ES256(非対称)。発行者と検証者が分離するなら非対称
  • 検証は署名だけでなく alg 許可リスト・exp/nbf・iss/aud まで
  • 落とし穴: alg:none / アルゴリズム混同 / 「暗号化ではない」/ 失効困難 / 保存場所
  • 推奨: 短命トークン+リフレッシュ、alg 固定、iss/aud 検証、ライブラリ任せ、強い鍵

JWT は便利ですが「署名であって暗号化ではない」「失効が苦手」という性質を押さえないと事故ります。仕組みを理解して、向く場面で正しく使ってください。

参考リンク

Cookie・セッション・SameSite の基礎 - Secure / HttpOnly と CSRF・CORS の関係

Cookie・セッション・SameSite の基礎 - Secure / HttpOnly と CSRF・CORS の関係

11

Web認証の土台となる Cookie・セッション・SameSite を実務目線で整理します。Set-Cookie の属性(Expires/Max-Age・Domain/Path)、Secure と HttpOnly が防ぐ脅威、SameSite の Strict/Lax/None の違いと None に Secure が要る理由、サーバー側セッションとログイン後の ID 再生成、SameSite だけで CSRF を防ぎきれない理由、クロスオリジンで Cookie を送る CORS の条件、__Host-/__Secure- プレフィックスまで、MDN・RFC 6265bis・OWASP を一次ソースにまとめます。

REST API設計の基礎と冪等性 - HTTPメソッドの意味と Idempotency-Key

REST API設計の基礎と冪等性 - HTTPメソッドの意味と Idempotency-Key

10

REST API設計の基礎を、HTTPメソッドの意味(safe / idempotent)から整理します。リソース指向のURL設計、GET/POST/PUT/PATCH/DELETE の使い分けと冪等性の早見表、なぜ冪等性がリトライや二重課金防止に重要か、POST を安全にリトライする Idempotency-Key の仕組み、ステータスコードの使い分け、RFC 9457 のエラー形式まで、RFC 9110・MDN・Stripe を一次ソースにまとめます。

CORS の仕組みとハマりどころ - プリフライト・credentials・Allow-Origin を理解する

CORS の仕組みとハマりどころ - プリフライト・credentials・Allow-Origin を理解する

10

CORS(オリジン間リソース共有)を実務目線で整理します。同一オリジンポリシーとの関係、単純リクエストとプリフライト(OPTIONS)の条件、Access-Control-Allow-Origin などの各ヘッダ、credentials 付きで * が使えない理由、Vary: Origin とキャッシュ、そして「No Access-Control-Allow-Origin header」エラーの意味と対処まで、MDN と WHATWG Fetch Standard を一次ソースにまとめます。CORS はブラウザの仕組みであって認可ではない、という勘所も。