
JWT(JSON Web Token)の仕組みと正しい使い方 - 署名・検証とセキュリティの落とし穴
ログイン認証やAPI認可で必ず出てくる JWT(JSON Web Token)。「使ってはいるけど仕組みはなんとなく」「localStorage に入れていいの?」「中身って暗号化されてるの?」——このあたりは誤解も多い領域です。
この記事では、JWT の構造・署名と検証・正しい使い方を、RFC(7519/7515/7518/8725)と OWASP を一次ソースに整理します。とくにセキュリティの落とし穴は、知らないと事故るポイントなので重点的に扱います。
JWT とは
JWT は単独の規格ではなく、JOSE(JSON Object Signing and Encryption)という規格群の上に成り立ちます。
| RFC | 内容 |
|---|---|
| RFC 7519 | JWT 本体(クレーム・構造) |
| RFC 7515 | JWS(署名/MAC) |
| RFC 7516 | JWE(暗号化) |
| RFC 7518 | JWA(使用する暗号アルゴリズム) |
| RFC 8725 | JWT のベストプラクティス(BCP 225) |
世の中で「JWT」と呼ばれているものの大半は、署名された JWS 形式(後述の3部構造)です。ここがまず重要で、デフォルトの JWS は「改ざん検知(署名)」だけで、暗号化ではありません。中身は誰でも読めます(詳細は後述)。
構造: header.payload.signature
JWS 形式の JWT は、3つの部分をドット(.)で連結した文字列です。各部は Base64URL(URL セーフな Base64)でエンコードされます。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
└──────── header ────────┘ └──── payload ────┘ └──────── signature ────────┘- Header(JOSE ヘッダ): アルゴリズムなどのメタ情報
- Payload(クレームセット): 伝えたい情報(クレーム)
- Signature: ヘッダとペイロードに対する署名/MAC
{ "alg": "HS256", "typ": "JWT" }{
"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) |
jti | JWT の一意な ID |
NOTE
exp/iat/nbf は UNIX エポック秒(ミリ秒ではない)です。また登録済みクレームの使用はすべて任意で、必須ではありません。
署名アルゴリズム: HS256 と RS256/ES256
RFC 7518 が定めるアルゴリズムのうち、よく使うのは次の2系統です。
| 種別 | 例 | 鍵 | 使いどころ |
|---|---|---|---|
| HMAC(対称鍵) | HS256 | 1つの共有秘密鍵で署名も検証も | 発行者と検証者が同一/信頼関係にある単一システム |
| 公開鍵(非対称) | RS256 / ES256 | 秘密鍵で署名、公開鍵で検証 | 発行者と検証者が分離(認可サーバ+複数 API) |
ポイントは、HS256 は鍵が1つなので検証側にもその鍵を渡す必要があり、渡した相手はトークンを偽造できてしまうこと。発行者と検証者が別主体なら、公開鍵方式(RS256/ES256)が原則です。検証側には公開鍵だけ配ればよく、OpenID Connect などで標準的に使われます。
検証の流れ
受信側がやるべきことは、署名検証だけではありません。
- ヘッダの
algが自分が許可したアルゴリズムかを確認(許可リスト方式) - 対応する鍵で署名を検証(成功するまでトークンを信用しない)
exp/nbfで時刻を検証iss/audがアプリの期待値と一致するか検証(RFC 8725 では検証必須)
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(公開鍵)で運用しているシステムに、攻撃者が alg を HS256 に書き換えて送ると、脆弱な実装は公開鍵を HMAC の共有鍵として検証してしまいます。公開鍵は誰でも入手できるので、任意のトークンを偽造可能になります。対策はやはり許可アルゴリズムをリストで固定すること(RFC 8725)。
「JWT は暗号化」という誤解
最重要の誤解です。JWS 形式の JWT は署名(改ざん検知)であって暗号化ではありません。ペイロードは鍵なしで Base64URL デコードするだけで誰でも読めます。
// payload 部分を Base64URL デコードするだけで中身が見える
atob("eyJzdWIiOiIxMjM0NTY3ODkwIn0") // => {"sub":"1234567890"}したがってパスワードや個人情報などの秘密をペイロードに入れてはいけません。秘匿が必要なら JWE(RFC 7516)で暗号化します。
失効(revocation)が難しい
JWT はステートレスなので、発行後は exp を迎えるまで基本的に無効化できません。「今すぐログアウトさせる」「漏洩トークンを即無効化」が難しいのが弱点です。対策は、失効リスト(デナイリスト)を持つか、後述の短命トークン+リフレッシュトークン構成です。
保存場所: localStorage か Cookie か
| 保存先 | リスク | 備考 |
|---|---|---|
localStorage | XSS で盗まれる(JS から読める) | 手軽だが危険 |
| httpOnly Cookie | XSS には強いが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 は便利ですが「署名であって暗号化ではない」「失効が苦手」という性質を押さえないと事故ります。仕組みを理解して、向く場面で正しく使ってください。


