メインコンテンツまでスキップ

トークン保存のセキュリティ

このドキュメントの目的

OAuth/OIDCで発行されるトークン(Access Token、Refresh Token、ID Token)をどこに保存すべきか、各方式のセキュリティリスクと対策を学びます。


保存場所は XSS への防御になりません

RFC 10017(OAuth 2.0 for Browser-Based Applications、BCP 212) は、悪意ある JavaScript は正規のコードと同じ権限を持つことを出発点に置いています。正規のコードがトークンを読めるなら、攻撃者も読めます。

保存場所の選択は、XSS が起きた場合の被害を減らしません。減らせるのは「トークンをブラウザに置かない」ことだけです。

このドキュメントは保存場所ごとの性質を整理したものです。アーキテクチャの選択が先で、その結論は上記の RFC を参照してください。

なぜトークン保存が重要なのか

OAuth/OIDCでは、認証後にトークンが発行される。
このトークンは「入場パス」のようなもの。

トークンを持っている = その人として扱われる

つまり、トークンが盗まれると:
- ユーザーのデータを閲覧される
- ユーザーとして操作される
- 長期間なりすまされる(Refresh Tokenの場合)

だから「どこに保存するか」が重要。

ブラウザでの保存場所と攻撃の関係

┌─────────────────────────────────────────────────────────────┐
│ ブラウザ │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ localStorage │ │sessionStorage│ │ Cookie │ │
│ │ │ │ │ │ │ │
│ │ XSSで │ │ XSSで │ │ HttpOnlyなら │ │
│ │ 盗める ✗ │ │ 盗める ✗ │ │ 盗めない ◎ │ │
│ └─────────────┘ └─────────────┘ └──────┬──────┘ │
│ │ │
│ ┌─────────────┐ │ │
│ │ メモリ │ │ │
│ │ (変数) │ ←── 永続化はされないが │ │
│ │ │ XSS では読める ✗ │ │
│ └─────────────┘ │ │
│ │ │
└─────────────────────────────────────────────┼───────────────┘

CSRFで勝手に送信されるリスク △

トークンの種類と機密性

トークン用途有効期限機密性
Access TokenAPIアクセス短期(1時間)
Refresh TokenAccess Token更新長期(30日)非常に高
ID Tokenユーザー認証証明短期(5分)

保存場所の選択肢

比較表

保存場所XSS で読めるか永続性CSRF耐性使いやすさ
メモリ(変数)読めるリロードで消える
HttpOnly Cookie読めない属性次第(Max-Age / Expires 無しならブラウザを閉じると消える)
localStorage読める永続
sessionStorage読めるタブ内で永続
「XSS で読めない」は「安全」ではありません

HttpOnly Cookie は JavaScript から読めませんが、攻撃者はブラウザにリクエストを送らせるだけで Cookie を使えます(RFC 10017 §5.1.4)。読めないことと悪用できないことは別です。

メモリは永続化されないという性質が価値であって、XSS への防御ではありません。


1. メモリ保存

概要

// トークンをJavaScript変数として保持
let accessToken = null;

async function login() {
const response = await fetch('/oauth/token', { ... });
const data = await response.json();
accessToken = data.access_token; // メモリに保存
}

async function callApi() {
const response = await fetch('/api/resource', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
}

メリット

✅ 永続化されない
- ブラウザを閉じればトークンは消える
- ディスクに残らないため、後から取り出される経路が無い

✅ CSRF攻撃に対して安全
- Cookieを使用しないため、自動送信されない
XSS には効きません

「クロージャに入れれば外部スクリプトからアクセスできない」という説明を見かけますが、誤りです

攻撃者のコードは正規のコードと同じコンテキストで動きます。正規のコードがトークンを使ってリクエストを送れる以上、攻撃者は同じ関数を呼ぶか、fetch を差し替えるか、リクエストを傍受すれば到達できます。クロージャは別オリジンからの保護であって、同一コンテキストで動くコードへの障壁ではありません。

メモリ保存の価値は「XSS で読めないこと」ではなく「永続化されないこと」です。

デメリットと対策

❌ ページリロードでトークン消失
対策: サイレントリフレッシュ(iframe + Refresh Token Cookie)

❌ 新しいタブでログイン状態が共有されない
対策: BroadcastChannel API、SharedWorker

❌ Refresh Tokenの保存場所が別途必要
対策: HttpOnly Cookieで保存

推奨パターン

Access Token: メモリ
Refresh Token: HttpOnly Cookie(BFF経由)

概要

Set-Cookie: refresh_token=xyz789;
HttpOnly;
Secure;
SameSite=Strict;
Path=/oauth;
Max-Age=2592000

メリット

✅ XSS攻撃に対して安全
- JavaScriptからアクセス不可(HttpOnly属性)

✅ 自動的にリクエストに含まれる
- API呼び出し時に明示的な処理不要

デメリットと対策

❌ CSRF攻撃のリスク
対策: SameSite属性(Strict/Lax)、CSRFトークン

❌ クロスオリジンリクエストで制限
対策: 同一オリジンのBFF(Backend For Frontend)パターン

3. localStorage

概要

// 保存
localStorage.setItem('access_token', accessToken);

// 取得
const token = localStorage.getItem('access_token');

// 削除
localStorage.removeItem('access_token');

メリット

✅ 永続的な保存(ブラウザを閉じても維持)
✅ 使いやすいAPI
✅ CSRF攻撃に安全(Cookieではないため)

デメリット(重大)

❌ XSS攻撃に対して脆弱
- JavaScriptから直接アクセス可能
- 悪意のあるスクリプトが実行されるとトークンが盗まれる

XSS攻撃でトークンが盗まれる具体的な流れは 入力バリデーション - XSS攻撃シミュレーションを参照。

使用すべき場合

- 機密性の低いデータのみ
- XSS対策が十分に実装されている場合(CSP等)
- 短期間のAccess Tokenのみ(Refresh Tokenは不可)

結局どうすればいいのか?

ここまで見てきた保存場所には、それぞれ弱点がある。

┌─────────────────┬────────────────────────────────────┐
│ 保存場所 │ 弱点 │
├─────────────────┼────────────────────────────────────┤
│ メモリ │ ページリロードで消える │
│ │ タブ間で共有できない │
├─────────────────┼────────────────────────────────────┤
│ HttpOnly Cookie │ SPAからAPIを直接呼ぶ時に不便 │
│ │ CSRFリスクがある │
├─────────────────┼────────────────────────────────────┤
│ localStorage │ XSSで盗まれる │
│ │ Refresh Tokenは保存禁止 │
└─────────────────┴────────────────────────────────────┘

→ 単独では完璧な方法がない
→ そもそもブラウザにトークンを置かない(BFF)が第一選択
保存場所の比較より前に決めること

上の表は「ブラウザにトークンを置く」ことを前提にした比較です。RFC 10017 は、その前提そのものを見直すことを勧めています。

  1. BFF — ブラウザには Cookie のみ。トークンはバックエンドが持つ
  2. Token-Mediating Backend — アクセストークンだけ渡し、リフレッシュトークンは渡さない
  3. ブラウザのみのクライアント — すべての攻撃シナリオに晒される

アーキテクチャの選択については RFC 10017 を参照してください。


BFFパターン

BFF は、保存場所の弱点を補う手段ではなく、ブラウザにトークンを渡さないアーキテクチャです。RFC 10017 では3つのアーキテクチャの筆頭に置かれています。

RFC が定義する BFF の3つの責務:

  1. confidential クライアントとして認可サーバーとやり取りする
  2. トークンを Cookie ベースのセッションの文脈で管理し、ブラウザに露出させない
  3. リソースサーバーへのすべてのリクエストを中継し、アクセストークンを付与する
API Gateway ではありません

BFF はサーバーサイドで動きますが、フロントエンドアプリケーションの構成要素です。RFC は「BFF がフロントエンドアプリケーションの OAuth クライアントになる」と明記しています。リバースプロキシや API Gateway と混同しないよう注意が必要です。

BFF でも防げないものがあります。攻撃者はブラウザから BFF にリクエストを送れるため、RFC 10017 §5.1.4(ブラウザ経由のプロキシ)は残ります。HttpOnly Cookie を使うことで、セッション状態への直接アクセスを防ぎ、client hijacking から session hijacking への昇格は止められます。

ただし、BFFにもトレードオフがある。

BFFパターンのトレードオフ:

メリット デメリット
─────────────────────────────────────────────────
✅ トークンをSPAに持たせない ❌ 実装が複雑になる
✅ XSSでトークンを盗まれない ❌ レイテンシが増加
✅ 新規トークンの取得も防げる ❌ サーバー運用コスト増
(confidentialクライアント) ❌ BFF自体が攻撃対象になる
△ ブラウザ経由のプロキシは残る

→ セキュリティ要件とコストのバランスで判断

SPAとBFFをセッションで繋ぐことの注意点

BFFパターンでは、SPAとBFFの間をセッションCookieで繋ぐことが多い。

┌───────┐  セッションCookie   ┌───────┐
│ SPA │ ←────────────────→ │ BFF │
└───────┘ (HttpOnly) └───────┘

これはOAuthトークン管理の複雑さを、セッション管理の複雑さに置き換えているとも言える。

セッション管理の詳細(攻撃対策、Cookie属性、分散環境での課題)については セッションセキュリティを参照。


パターンA: BFFがAPI代理呼び出し(より安全)

┌───────┐                ┌───────┐                ┌───────┐
│ SPA │ │ BFF │ │ API │
└───┬───┘ └───┬───┘ └───┬───┘
│ │ │
│ 1. リクエスト │ │
│ (セッションCookie) │ │
│───────────────────────>│ │
│ │ │
│ │ 2. API呼び出し │
│ │ (Access Token) │
│ │───────────────────────>│
│ │ │
│ │ 3. レスポンス │
│ │<───────────────────────│
│ │ │
│ 4. データ返却 │ │
│<───────────────────────│ │

BFFが保持:
- Access Token(メモリ/セッション)
- Refresh Token(HttpOnly Cookie)

SPAが保持:
- セッションCookie のみ(トークンなし)

パターンB: BFFがトークン管理のみ

┌───────┐                ┌───────┐                ┌───────┐
│ SPA │ │ BFF │ │ API │
└───┬───┘ └───┬───┘ └───┬───┘
│ │ │
│ 1. トークン取得/更新 │ │
│ (セッションCookie) │ │
│───────────────────────>│ │
│ │ │
│ 2. Access Token │ │
│<───────────────────────│ │
│ │ │
│ 3. API呼び出し(直接) │
│ (Access Token) │
│────────────────────────────────────────────────>│
│ │ │
│ 4. レスポンス │
│<────────────────────────────────────────────────│

BFFが保持:
- Refresh Token(HttpOnly Cookie)

SPAが保持:
- Access Token(メモリ)

どちらを選ぶか

観点パターンAパターンB
セキュリティ◎ SPAにトークンなし○ Access Tokenのみ
実装複雑度△ BFFでAPI代理が必要○ トークン管理のみ
レイテンシ△ BFF経由で増加◎ 直接通信
推奨金融系など高セキュリティ一般的なWebアプリ

XSS が起きたとき、保存場所で何が変わるか

保存場所によって変わるのは「攻撃者がトークンをどう取るか」であって、「取れるかどうか」ではありません。

保存場所攻撃者の取り方
localStorage / sessionStoragelocalStorage.getItem() で直接読む
メモリ(変数)正規コードの関数を呼ぶ/fetch を差し替えて送信時に傍受する
HttpOnly Cookie読めないが、リクエストを送らせれば Cookie は自動で付く

いずれも、攻撃者はトークンを使えます。

さらに RFC 10017 は、保存場所に一切依存しない攻撃を2つ挙げています。

シナリオ内容
§5.1.3 新しいトークンの取得隠し iframe でサイレントに認可フローを回し、アプリが持つものとは別のトークンを発行させる
§5.1.4 ブラウザ経由のプロキシトークンを盗まず、正規のアプリと同じようにリソースサーバーを叩く

保存場所を変えても、この2つは塞がりません。

→ XSS 対策(CSP・出力エンコード・SRI 等)は必須です。保存場所の選択はその代わりにはなりません。詳しくは入力バリデーションRFC 10017 を参照してください。


チェックリスト

Web SPA

まずアーキテクチャを決めます。

  • BFF を採用(ブラウザにトークンを渡さない)
  • BFF が難しい場合、最低限 Refresh Token はブラウザに渡さない(Token-Mediating Backend)

ブラウザにアクセストークンを置く場合:

  • Access Tokenはメモリに保存(XSS 対策ではなく、永続化を避けるため
  • アクセストークンの有効期間を短くする
  • スコープを必要最小限にする
  • DPoP 等の sender-constrained token を検討する(ブラウザのみのクライアントの制約は解決しないが、盗用の難度は上がる)
  • CSRF対策を実装
  • CSP(Content Security Policy)を設定

共通

  • Access Tokenは短い有効期限(1時間以下)
  • Refresh Tokenは適切な有効期限
  • トークン失効機能を実装

発展: モバイルアプリでのトークン保存

この内容は発展的なトピックです。Webアプリの基礎を理解してから学習することを推奨します。

iOS

Keychainに保存(推奨)
- kSecAttrAccessibleWhenUnlockedThisDeviceOnly
- デバイス固有、バックアップ対象外

Android

EncryptedSharedPreferencesを使用(推奨)
- AES256-GCM暗号化
- Android Keystore連携

モバイルアプリのチェックリスト

  • iOS: Keychainを使用
  • Android: EncryptedSharedPreferencesを使用
  • トークンをログに出力しない
  • デバイス紛失時の失効機能

参考資料


更新履歴

日付変更
2026-08-28RFC 10017(BCP 212)の発行に伴い、保存場所の評価を全面的に見直し
2025-12-25初版

2026-08-28 の変更点

RFC 10017 が「悪意ある JavaScript は正規のコードと同じ権限を持つ」ことを出発点に置いたため、保存場所を XSS への防御として評価していた記述を訂正しました。

訂正前訂正後
メモリ保存は「XSS に対して最も安全 ◎」メモリの価値は永続化されないこと。XSS では読める
「クロージャなら外部スクリプトからアクセス困難」攻撃者は同一コンテキストで動くため到達できる
「万が一に備えて保存場所も考慮する(多層防御)」保存場所は多層防御にならない。§5.1.3 / §5.1.4 は保存場所に依存しない
BFF は「保存場所の弱点を補う手段の1つ」BFF はブラウザにトークンを渡さないアーキテクチャ。RFC の第一選択
比較表の観点は「XSS耐性」「XSS で読めるか」と「永続性」に分離(読めないことと悪用できないことは別)

最終更新: 2026-08-28 対象: フロントエンド開発者、セキュリティエンジニア