JWT認証におけるCookieとlocalStorageの比較
認証フローを構築し、JWTが正常に動作するようになると、すべてのフロントエンド開発者がいずれ直面する問いに行き着きます。「このトークンを実際にどこに保存すべきか?」この答えは、多くのチュートリアルが示す以上に重要であり、「HttpOnly Cookieを使えばいい」という一般的なアドバイスは、理解しておくべき本質的なトレードオフを見落としています。
ここでは、両方の選択肢を明確に整理し、それぞれが実際に何を防ぐのか、そして現代のアプリケーションが実際にどのように対処しているかを解説します。
重要なポイント
localStorageはページ上で実行されるあらゆるJavaScriptからアクセス可能なため、XSSによるトークン窃取に対して脆弱です。HttpOnlyCookieはJavaScriptによるアクセスを完全にブロックしますが、CSRFリスクをもたらします。これはSameSiteおよびSecure属性によって軽減できます。- 現代的なパターンでは、有効期限の短いアクセストークンをメモリに保存し、リフレッシュトークンを
HttpOnly、Secure、SameSiteCookieに保存します。 - OWASPおよびOAuthのブラウザベースアプリに関するガイダンスは、有効期限の長いトークンを
localStorageに保存することを推奨していません。 - 適切な選択は、脅威モデル、バックエンドの制御範囲、およびAPIが
Authorizationヘッダーを必要とするかどうかによって異なります。
JWTの保存場所が持つ本質的なリスク
JWTをどこに保存するかによって、アプリケーションに適用される攻撃ベクターが決まります。主な脅威は次の2つです。
- XSS(クロスサイトスクリプティング): アプリのコンテキストで悪意のあるJavaScriptが実行されること。
- CSRF(クロスサイトリクエストフォージェリ): ユーザーのブラウザを騙して、意図しない認証済みリクエストを送信させること。
どちらの保存方法も、両方のリスクを同時に排除することはできません。重要なのは、どのリスクを受け入れるかを理解し、その軽減策を講じることです。
localStorage:利便性は高いが、JavaScriptからアクセス可能
JWTをlocalStorageに保存する方法はシンプルです。書き込み、読み取り、そしてAuthorization: Bearerヘッダーに手動で付加するだけです。このヘッダー形式を期待するAPIとも問題なく連携できます。
問題は、localStorageがページ上で実行されるあらゆるJavaScriptから完全にアクセスできる点です。依存パッケージの脆弱性、侵害されたCDN、または自身のコードのXSS欠陥を通じて攻撃者がスクリプトを注入することに成功した場合、トークンを直接読み取って外部に持ち出すことができます。OWASPはこの理由から、セッション識別子をlocalStorageに保存することを明示的に推奨していません。
これは机上の話ではありません。現代のWebアプリケーションは数十ものサードパーティスクリプトを読み込んでおり、それぞれが潜在的な攻撃対象領域となります。
HttpOnly Cookie:XSS耐性は向上するが、新たな考慮事項が生じる
HttpOnly CookieはJavaScriptから一切読み取ることができません。攻撃者がページ上でコードを実行したとしても、トークンの値を抽出することはできません。これは実質的な改善です。
ただし、CookieはCSRFの露出をもたらします。ブラウザは一致するリクエストに対して自動的にCookieを付加しますが、これには悪意のあるサードパーティサイトによってトリガーされたリクエストも含まれます。
このギャップを埋めるために、3つのCookie属性が連携して機能します。
HttpOnly— JavaScriptによるアクセスを完全にブロックします。Secure— CookieをHTTPS経由でのみ送信します。SameSite— クロスサイトでのCookie送信を制御します。
SameSiteについては、属性が未設定の場合、現代のブラウザはデフォルトでLaxを適用します。これは、クロスサイトのサブリクエスト(別オリジンからのPOSTなど)ではCookieをブロックしますが、トップレベルのナビゲーションでは許可します。Strictはより保守的で、トップレベルのナビゲーションを含むあらゆるクロスサイトリクエストでCookieの送信を防ぎます。ブラウザのデフォルトに依存せず、常に明示的に設定してください。SameSiteのブラウザサポートは現代のブラウザ全体で優れており、Can I Useで確認できます。
SameSite=StrictまたはLaxが適切に設定されていれば、ほとんどの同一サイト認証構成においてCSRFリスクは大幅に低減されます。機密性の高い状態変更エンドポイントに対しては、多層防御としてアンチCSRFトークンと組み合わせることを推奨します。
Discover how at OpenReplay.com.
現代的なアプリケーションが採用するパターン
多くの本番アプリケーションでは、問題を分割して対処します。
- 有効期限の短いアクセストークンをJavaScriptのメモリ(モジュールレベルの変数またはReactのstate)に保存する。
- リフレッシュトークンを
HttpOnly、Secure、SameSiteCookieに保存する。
アクセストークンはタブを閉じるかページをリフレッシュすると消えますが、/refreshエンドポイントへのサイレントな呼び出しにより、Cookieを使用して新しいトークンを取得できます。アクセストークンは永続ストレージに触れることなく、リフレッシュトークンはJavaScriptから読み取られることもありません。
このアプローチは、OAuth 2.0 with PKCE(PKCEを使用した認可コードフロー)を使用するブラウザベースアプリの現在のガイダンスと一致しており、OAuth 2.0 for Browser-Based Appsが推奨する方式です。**OpenID Connect(OIDC)**を使用している場合も同様のパターンが適用されます。IDトークンとリフレッシュトークンはlocalStorageに保存しないようにしてください。
セキュリティ監査チェックリスト
リリース前に以下を確認してください。
- トークンを保持するCookieに
HttpOnlyフラグが設定されていること。 Secureフラグが有効であること(HTTPSが強制されていること)。SameSiteがStrictまたはLaxに明示的に設定されていること。- アクセストークンの有効期限が短く、通常は時間単位ではなく分単位であること。
- Content Security Policyヘッダーが設定されていること。
- 有効期限の長いJWTが
localStorageに保存されていないこと。
アプリケーションに適したアプローチの選択
万能な答えはありません。バックエンドを制御しており、同一ドメインからアプリを提供している場合は、適切なSameSite設定を施したHttpOnly Cookieがより堅牢なデフォルトです。サーバー側でCookieを設定できず、Authorizationヘッダーを必要とするサードパーティAPIと統合している場合は、有効期限の短いインメモリストレージが合理的な代替手段です。ただし、有効期限の長いトークンをlocalStorageに永続化することは避けてください。
まとめ
有効期限の長いJWTをlocalStorageに保存することは、現在のセキュリティガイダンスが一貫して推奨しないことです。SecureおよびSameSite属性を持つHttpOnly Cookieは、ほとんどの同一ドメイン構成において最も堅牢なデフォルトを提供します。一方、インメモリストレージとリフレッシュトークンCookieの組み合わせは、より複雑なケースに対応します。脅威モデル(一方にXSS、他方にCSRF)を理解すれば、アプリケーションに適した選択が推測ではなく、明確に根拠のあるトレードオフとして判断できるようになります。
よくある質問
sessionStorageはlocalStorageと同じ弱点を持っています。ページ上で実行されるJavaScriptから読み取ることができます。唯一の違いは、sessionStorageがタブを閉じると消去される点です。これにより露出時間は短縮されますが、XSSからは保護されません。トークンの保存においては、sessionStorageをlocalStorageと同様の注意を持って扱い、有効期限の長いトークンの保存は避けてください。
SameSite=StrictはクロスサイトリクエストでのCookie送信を防ぎ、ほとんどのCSRF攻撃パターンをブロックします。ただし、重要度の高い状態変更エンドポイントに対しては、アンチCSRFトークンを追加することで多層防御が実現できます。SameSiteはブラウザによって強制されるため、古いクライアントや特殊なエッジケースでは遵守されない場合があります。ダブルサブミットトークンパターンは、依然として合理的な安全策です。
一般的な範囲は5〜15分です。盗まれたトークンの価値を限定するほど短く、かつリフレッシュエンドポイントへの過剰なリクエストを避けるほど長い設定です。これを、HttpOnly Cookieに保存した有効期限の長いリフレッシュトークン(数時間〜数日)と組み合わせてください。アプリが決済などの機密性の高い操作を扱う場合は、有効期限をより短くし、重要なアクションには再認証を要求することを推奨します。
アクセストークンはlocalStorageではなく、JavaScriptのメモリ(モジュール変数、Reactのstate、またはクロージャ)に保存してください。有効期限を短く保ち、可能な限り自身のバックエンドエンドポイントを通じてリフレッシュしてください。リロード後も何かを永続化する必要がある場合は、有効期限の長い認証情報をサーバー側で保持する自身のバックエンドを通じてリフレッシュフローを実行し、有効期限の長いトークンをクライアントのストレージに公開しないようにしてください。
Gain control over your UX
See how users are using your site as if you were sitting next to them, learn and iterate faster with OpenReplay. — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.