JWTはHttpOnly、CSRFはダブルサブミット:SPA×APIの実践的セキュリティ設計ガイド
Tags: WebSecurity, JWT, CSRF, CORS, Cookie, SPA, Vue, React, SpringBoot
難易度: ★★★☆☆(中級)
対象読者: SPA×REST構成で「JWTをCookie運用」「CSRFはダブルサブミット」にしたい人/既存構成を安全に固めたい人
TL;DR
-
結論:JWTを HttpOnly Cookie に格納し、状態変更系で ダブルサブミット Cookie(
XSRF-TOKEN Cookie と X-XSRF-TOKEN ヘッダの一致検証)を行う構成は、実用的で堅牢。
-
成功の鍵:XSS対策最優先、Cookie属性の最小化、CORSのOrigin厳格化、Refresh Tokenのローテーション+再利用検知。
1. 全体像(アーキテクチャ)
アーキテクチャ概要
-
認証:ログイン成功時に
access_token(短命)と refresh_token(長め)を HttpOnly; Secure; SameSite=Lax の Cookie で発行
-
API呼び出し:ブラウザが Cookie を自動送出 → サーバ側で JWT 検証
-
CSRF対策:
XSRF-TOKEN(非HttpOnly Cookie)を配布し、状態変更系で 同一値 を X-XSRF-TOKEN ヘッダに載せて一致検証(Double Submit)
-
トークン更新:
/auth/refresh で Refresh ローテーション、再利用検知で 全セッション失効
-
ログアウト:
Max-Age=0 で Cookie 無効化+(必要に応じて)サーバ側無効化テーブル
システムアーキテクチャ概要

エラーケース:Refresh Token再利用検知

2. 構成要素と役割
(A) アクセストークン(短命, 5–15分)
-
用途:API の即時認可
-
格納:
HttpOnly; Secure; SameSite=Lax; Path=/
-
注意:個人情報や権限一覧を入れすぎない(JWTは Base64で可視。署名で改竄検知はできるが秘匿ではない)
(B) リフレッシュトークン(長め, 数日〜数週)
-
用途:アクセストークン更新
-
格納:HttpOnly Cookie(
Path=/auth/ 等でスコープ最小化)
-
必須:ローテーション+再利用検知(使い回し発見で全セッション失効)
(C) CSRFトークン(XSRF-TOKEN)
Set-Cookie: XSRF-TOKEN=abc123...; Secure; SameSite=Lax; Path=/
-
検証:状態変更API(POST/PUT/PATCH/DELETE)で Cookie 値とヘッダ値の一致
-
強化:トークンに ユーザID+期限+HMAC署名 を含めると盗用耐性UP
(D) CORS
-
withCredentials=true を使う場合:
-
Access-Control-Allow-Origin: 特定Originのみ(* は不可)
-
Access-Control-Allow-Credentials: true
-
Vary: Origin を忘れない(キャッシュ分離)
(E) Cookie属性(重要)
- 基本は
HttpOnly; Secure; SameSite=Lax
- クロスサイト必須時のみ、そのCookieに限って
SameSite=None; Secure(最小化 が原則)
3. 代表的フロー
正常フロー
-
CSRF取得:SPA初期表示で
GET /csrf → XSRF-TOKEN を受領
-
ログイン:
POST /auth/login(X-XSRF-TOKEN 必須)→ access_token / refresh_token を HttpOnly で発行
-
通常API:ブラウザが Cookie を自動送出。状態変更系のみ CSRF 照合
-
期限切れ:API が 401 →
POST /auth/refresh(X-XSRF-TOKEN 必須)→ 新しい JWT を再発行
-
ログアウト:
Set-Cookie で失効(Max-Age=0)+必要に応じてサーバ側の無効化
エラー時の処理(推奨)
-
401 Unauthorized:Access 期限切れ → Refresh 実行
-
403 Forbidden:認可不足 → UI で案内
-
CSRF不一致:
/csrf で再取得 → 1回だけ リトライ
-
注意:419 は一部FW(例:Laravel)の慣習で、標準HTTPではない。APIは 401/403/400+アプリコードで表現すると移植性◎
4. メリット・デメリット
メリット
| 項目 |
説明 |
| XSS耐性 |
JWTは HttpOnly でJSから読み取れない |
| CSRF対策 |
ダブルサブミットでステートレス寄りに実装可 |
| 実装簡潔性 |
Cookie自動送出でクライアント実装がシンプル |
| 長期セッション |
Refresh ローテーション+再利用検知 で安全に維持 |
| 実績 |
SPA×REST で豊富な運用事例 |
デメリットと対策
| 課題 |
対策 |
XSSで XSRF-TOKEN が盗まれるリスク |
CSP/サニタイズ/依存更新/Trusted Types |
| CORS/Cookie 設定が複雑 |
Origin限定、SameSite最小化、プリフライト設計
|
| Refresh再利用検知の実装コスト |
DB/キャッシュでトークン系譜管理 |
| JWTの情報露出 |
最小クレーム、識別子ベース+サーバ照合
|
5. よくある落とし穴(NG集)
- ❌
Access-Control-Allow-Origin: * と Allow-Credentials: true の併用
- ❌
SameSite=None の乱用(必要なCookieだけに限定)
- ❌ Refresh を長寿命・ローテなし(乗っ取り時の被害拡大)
- ❌ JWTに個人情報・権限一覧を詰め込む(Base64で誰でも閲覧可)
- ❌ JWT を
localStorage / sessionStorage に保存(XSS即漏洩)
- ❌ クリックジャッキング対策漏れ(
frame-ancestors 'self' or X-Frame-Options)
6. セキュリティ強化チェックリスト
Cookie / 認証
CSRF
CORS
セキュリティヘッダ
運用
7. 代替案との比較
| 方式 |
概要 |
長所 |
短所 |
| 本方式(JWT in HttpOnly + Double Submit) |
本記事の構成 |
実装コスト低〜中/ステートレス寄り/運用実績多 |
CORS/Cookie設計が要注意、XSS対策は必須 |
| Synchronizer Token Pattern |
サーバにCSRFトークン保持 |
CSRF耐性がさらに強い |
サーバ状態が増える(完全ステートレスではない) |
| Bearer in Storage |
JWTをstorage保持 |
実装が簡単 |
XSSに弱いので非推奨 |
| OIDC + PKCE |
認可サーバ分離・標準準拠 |
SSO/拡張性/コンプラ |
初期構築が重い/運用コスト増 |
8. 設計判断のポイント(実務の落としどころ)
-
1st-party運用(同一ドメイン/サブドメイン)なら SameSite=Lax が基本で十分
-
別ドメイン横断が要件なら、対象Cookieのみ SameSite=None; Secure に
- JWTの中身は 最小化:権限は ID/スコープ に留め、最終判定はサーバ側 RBAC
- Refresh は 短すぎず長すぎず:UXとリスクの折衷+ローテーション必須
-
XSSは常に最優先:CSP、依存ライブラリアップデート、エスケープ、Trusted Types
9. モバイルアプリの考慮
-
ネイティブ:Cookie 非依存 →
Authorization: Bearer ヘッダ
-
WebView:Cookie 共有の可否に注意(プラットフォーム差異)
-
RN / Capacitor:Secure Storage(Keychain / Keystore)を活用
10. まとめ
-
HttpOnly Cookie の JWT × ダブルサブミット CSRF は、SPA×API における 現実的かつ堅牢 な構成。
- 成功の鍵は XSS 最優先, Cookie/CORS の最小化, Refresh トークンのローテーション+再利用検知。
- 迷ったら本記事の チェックリスト と 比較表 で要件照合。要件によっては OIDC + PKCE も検討。
参考資料
-
OWASP Cheat Sheet Series
- CSRF Prevention Cheat Sheet
- JWT (JSON Web Token) Cheat Sheet
-
MDN Web Docs
- SameSite cookies / Set-Cookie / CORS
-
RFC / 標準
- RFC 6750: Bearer Token Usage
- RFC 6265: HTTP State Management Mechanism
- OAuth 2.0 / OpenID Connect(OIDC)