0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JWTはHttpOnly、CSRFはダブルサブミット:SPA×APIの実践的セキュリティ設計ガイド

Posted at

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 に格納し、状態変更系で ダブルサブミット CookieXSRF-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/refreshRefresh ローテーション、再利用検知で 全セッション失効
  • ログアウトMax-Age=0 で Cookie 無効化+(必要に応じて)サーバ側無効化テーブル

システムアーキテクチャ概要

image.png

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

image.png

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=/
  • 送信時
X-XSRF-TOKEN: abc123...
  • 検証:状態変更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 /csrfXSRF-TOKEN を受領
  • ログインPOST /auth/loginX-XSRF-TOKEN 必須)→ access_token / refresh_tokenHttpOnly で発行
  • 通常API:ブラウザが Cookie を自動送出。状態変更系のみ CSRF 照合
  • 期限切れ:API が 401POST /auth/refreshX-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 / 認証

  • access_token: HttpOnly; Secure; SameSite=Lax; Path=/
  • refresh_token: ローテ+再利用検知Path=/auth/ 等で限定
  • JWTクレームは 最小sub, scopeId など)

CSRF

  • XSRF-TOKEN非HttpOnly で配布
  • 状態変更APIヘッダとCookieの一致 を必須
  • ログイン/リフレッシュ 時も CSRF 検証

CORS

  • AllowedOrigins厳格列挙
  • Access-Control-Allow-Credentials: trueVary: Origin
  • プリフライトで X-XSRF-TOKEN を許可

セキュリティヘッダ

  • CSPdefault-src 'self'script-srcnonce/hash)
  • X-Content-Type-Options: nosniff
  • Referrer-Policy: strict-origin-when-cross-origin
  • Permissions-Policy(不要権限は空 or 不許可)
  • frame-ancestors 'self'(or X-Frame-Options: DENY/SAMEORIGIN

運用

  • レート制限/ログイン試行制限/BOT検知
  • 監査ログ(ログイン成功/失敗、Refresh再利用検知、権限変更)
  • 端末バインディング(Refresh と端末指紋の紐づけ)
  • 重要画面は MFA

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 / CapacitorSecure 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)
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?