ログイン機能を作って認証情報を受け取ったあと、多くの人が最初に迷うのが「これをどこに保存するのか」です。よく候補に挙がるのがブラウザのlocalStorageと、Cookie。選び方によってセキュリティが変わります。

なお、ひとくちに「認証トークン」といっても、サーバー上のログイン状態を指すセッションID、APIにアクセスするための短命なアクセストークン、それを再発行するためのリフレッシュトークンなど、いくつか種類があり、最適な保存方法は種類や構成で変わります。この記事では、ブラウザからAPIを使う一般的なWebアプリを前提に、保存場所の違い、XSSやCSRFとの関係、迷ったときの現実的な選び方を整理します。トークンそのものの仕組みはトークン認証とはで解説しています。

ムチオ
ムチオ
とりあえずlocalStorageに入れとけばいいんじゃないの?
ルミナ
ルミナ
手軽なんですが、落とし穴があります。localStorageは、ページ上で動くJavaScriptから読めてしまう。ここがセキュリティの分かれ目になります。

保存先としてよく使われるのが、この2つです。

  • localStorage:ブラウザにデータを保存する仕組み。JavaScriptから自由に読み書きできる。
  • Cookie:サーバーとやり取りする小さなデータ。HttpOnly を付けるとJavaScriptからは読めなくなり、条件に合うリクエストでブラウザが自動的に送る。

違いは、大きく「JavaScriptから読めるか」と「リクエストに自動で付くか」の2点です。ここが、後で出てくるXSSとCSRFのリスクに直結します。なお、どちらが使えるかは、APIがCookie認証に対応しているか、それとも Authorization ヘッダーにトークンを要求するかによっても変わります。

localStorage:手軽だが、XSSでトークンを盗まれうる

localStorageは扱いが簡単で、トークンを入れて必要なときに取り出す実装がすぐ書けます。ただし、JavaScriptから読めるという性質が、そのまま弱点になります。

もしサイトに、悪意のあるスクリプトを実行されてしまう脆弱性(XSS)があると、そのスクリプトはlocalStorageの中身も読めます。保存しておいたトークンを丸ごと外部へ送られ、盗まれる可能性があります。盗まれれば、攻撃者はそれを使ってあなたのユーザーになりすませます。

HttpOnly Cookie:JavaScriptから直接盗まれにくい。ただしCSRF対策が要る

一方、HttpOnly を付けたCookieは、JavaScriptから読めません。そのため、XSSがあっても、トークンそのものは直接は盗まれにくくなります。

ただし、Cookieは、Domain・Path・SameSiteなどの条件に一致するリクエストで、ブラウザが自動的に付けて送ります。この「自動で付く」性質を悪用されるのがCSRFです。攻撃者が用意した別のサイトから、あなたのサイトへリクエストを飛ばすと、ブラウザがログイン中のCookieを勝手に付けてしまい、ユーザーのブラウザを利用して、意図しない操作を実行させられることがあります。Cookieそのものの仕組みはCookieとはで整理しています。

だからCookieを使うなら、CSRF対策がセットになります。ここで、Cookieの属性は役割がそれぞれ違うので、混同しないことが大事です。

  • HttpOnly:JavaScriptからCookieを直接読めなくする。
  • Secure:HTTPS通信でのみCookieを送る(通信経路の保護)。CSRFを防ぐ属性ではありません。
  • SameSite:クロスサイトでのCookie送信を制限する(CSRFリスクを下げる重要な防御の一つ)。Strictはクロスサイトリクエストでは送らず、Laxは一部の遷移では送り、Noneはクロスサイトでも送る(その場合Secureが必要)。
  • CSRFトークンやOriginの検証:状態を変えるリクエストに対する、追加のCSRF対策。

注意したいのは、SameSiteの「site」は「オリジン」と同じではない点です。たとえば同じ example.com 配下の別サブドメインは、別オリジンでも同一サイト扱いになることがあり、SameSiteだけではすべてのCSRFを防げるとは限りません。だからSameSiteを適切に設定したうえで、状態を変える操作にはCSRFトークンなども組み合わせる、という多層で考えます。

ムチオ
ムチオ
属性がいっぱいで混乱してきた…。結局どうすればいいの?
ルミナ
ルミナ
1つ考え方を変えると楽になります。「トークンをブラウザのどこに置くか」より、「そもそもブラウザに実トークンを持たせない」方向です。次で説明しますね。

迷うなら、「ブラウザに実トークンを持たせない」構成が有力

localStorageとCookieの比較で整理すると、盗まれにくさではCookieのほうが有利です。ただ、セキュリティを優先するなら、もう一歩踏み込んだ構成が有力です。

自社のWebアプリで迷うなら、アクセストークンなどの実トークンをブラウザに直接持たせるのではなく、サーバー側でセッションやトークンを管理し、ブラウザにはHttpOnly・Secure・適切なSameSiteを設定したセッションCookieだけを持たせる構成が有力です。ブラウザには、サーバー側のセッションを指し示すランダムなセッション識別子だけをCookieとして持たせ、アクセストークンやリフレッシュトークンはサーバー側で管理します。OAuthを利用し、バックエンドがトークン管理とAPIへの代理アクセスまで担う構成は、BFF(Backend for Frontend)と呼ばれます。

一方、SPAが Authorization ヘッダーにトークンを載せてAPIを直接叩く構成では、HttpOnly CookieのトークンはJavaScriptから読めないため、そのままヘッダーに載せられません。この場合は、APIをCookie認証に対応させる、サーバーを1枚挟む(BFF)、アクセストークンはブラウザのメモリで扱う、といったアーキテクチャの選択が必要になります。つまり、最適解はAPIや認証基盤の作りによって変わります。「Cookieに入れれば安心」と一律に決めず、構成から考えるのが正確です。

メモリ+リフレッシュトークンという形もある

もう一つ、アクセストークンはブラウザのメモリ(変数)に置き、リフレッシュトークンだけをCookieに保存する構成もあります。リフレッシュトークンをHttpOnly Cookieに置く場合は、バックエンド側の再発行エンドポイントがCookieを受け取り、アクセストークンの再発行処理を行います。

メモリに置いたトークンは、ページ更新やタブ終了で消え、永続ストレージにも残らないため、localStorageより露出する期間を短くできます。ただし、ページ上でXSSが実行されている間は、保管方法によってはメモリ内のトークンを読み取られる可能性があります。直接読み取れないよう分離していても、正規アプリと同じ方法でAPIを呼ばれたり、新しいトークンを取得されたりする可能性は残ります。メモリ保存は「露出を今の実行中に限定する」もので、XSSへの完全な防御ではありません。

この形を採るなら、リフレッシュトークンのローテーション、有効期間の上限や未使用時の失効、ログアウト時の失効処理、再発行エンドポイントのCSRF対策まで含めて設計します。

いちばん大事なのは、保存場所より「XSSを作らないこと」

ここまで保存場所の話をしてきましたが、もっと根っこにある前提があります。XSSを通さない作りにすることです。

XSSがあると、localStorageのトークンは読まれます。Cookieでトークン自体を守れても、XSSで実行されたスクリプトは、その場でログイン中のユーザーとして操作を実行できてしまいます。サーバー側にトークンを置く構成でも、ブラウザが乗っ取られること自体は残ります。つまり、保存場所をどれだけ工夫しても、XSSが残っていれば安全とは言えません。

XSS対策は、エスケープだけではありません。フレームワークの自動エスケープを無効にしない、出力先(HTML・属性・URL・JavaScript)に合ったエンコードをする、HTMLを許可する場合はサニタイズする、innerHTMLやdangerouslySetInnerHTMLのような危険な出力を避ける、外部スクリプトや依存パッケージを管理する、そして多層防御としてCSP(コンテンツセキュリティポリシー)を設定する、といった積み重ねになります。公開時に確認したいサーバ側の全体像はAIで作ったアプリのサーバは安全かも参考になります。

今日やれる、トークン保存まわりの点検

  • 実トークンをlocalStorageに置いていないか。ブラウザに直接持たせない構成を検討したか。
  • Cookieを使うなら、HttpOnly・Secure・適切なSameSiteが付いているか。
  • 状態を変えるリクエストに、必要なCSRF対策(SameSite+必要ならCSRFトークン/Origin検証)があるか。
  • 出力先に応じたエスケープ・サニタイズで、XSSを防げているか。危険な出力方法を使っていないか。
  • AIに書かせた認証コードを、そのまま信用せず、保存場所・XSS・CSRFの観点で確認したか。

AIで開発すると、認証まわりも短時間で形になります。ただ、保存場所やXSS・CSRFの考え方は、AIに丸投げでは詰めきれません。動くかどうかだけでなく、安全な作りとテストで、脆弱なサイトにしないようにしましょう。

まとめ

  • 「認証トークン」にはセッションID・アクセストークン・リフレッシュトークンなどがあり、最適な保存方法は種類と構成で変わる。
  • localStorageはXSSで盗まれやすい。HttpOnly Cookieは直接読まれにくいが、属性の役割は別物(HttpOnly=JavaScriptからの直接読み取り防止、Secure=通信経路、SameSite=クロスサイトでのCookie送信を制限)。SecureはCSRF対策ではない。
  • SameSiteだけで全CSRFは防げない。状態を変える操作にはCSRFトークンなども組み合わせる。
  • 迷うなら、ブラウザに実トークンを持たせず、サーバー側で管理してHttpOnlyセッションCookieだけを持たせる構成が有力。最適解はAPI構成による。
  • 最も大事なのは保存場所より、XSSを作らないこと。XSSが残れば、どこに置いても安全とは言えない。