Skip to content

50_セキュリティ

本章では、filix-shadowwork-api におけるセキュリティ前提と、認証・署名・Secrets/PII の取り扱いを定義する。
ここで扱うのは「どのように安全性を確保するか」という設計上の前提であり、個別の実装詳細(ライブラリ呼び出し等)は対象外とする。


セキュリティ方針(前提)

  • 最小権限(Least Privilege)
    API の利用者・運用者・外部サービスに付与する権限は最小化する。

  • 入力はすべて不正である前提
    すべてのリクエスト入力を検証し、想定外のデータを拒否する。

  • 外部サービスとの境界は署名/トークンで保護する
    Webhook、管理 API、LLM 連携は必ず認証情報で境界を守る。

  • 監査可能性(Auditability)
    不具合や不正の切り分けのため、重要操作はログに残る設計にする(PIIを含めない)。


認証(Authentication)

目的

  • 「このリクエストが誰のものか」を確実に特定する(なりすまし防止)
  • user_id の真正性を担保する

前提

  • フロントエンドは外部の会員基盤(例: Memberstack)によりログイン状態を得る。
  • バックエンドは、フロントエンドから提示される認証情報を検証して user_id を確定する。

採用方式

  • JWT(JSON Web Token)
  • Memberstack のログイン トークンをバックエンドで検証し、JWT を発行して返す。
  • フロントエンドは以後、すべての API リクエストで JWT をCookieで送信する。
  • バックエンドは JWT の署名を検証し、sub クレーム(= memberId)からユーザーを特定する。

詳細仕様

JWT発行エンドポイント:POST /api/auth/exchange
  • 入力:Memberstack ログイン トークン(リクエストボディに token フィールド)
  • 処理
  • Memberstack API で トークンを検証し memberId を取得
  • JWT を以下の仕様で発行
  • HttpOnly / Secure / SameSite Cookie で返却
  • 出力
    {
      "ok": true,
      "member_id": "mem_xxx",
      "token_type": "Bearer",
      "expires_in": 900
    }
    
JWT仕様
  • 署名方式:HS256(HMAC-SHA256)
  • 署名鍵JWT_SIGNING_SECRET(環境変数/Secret)
  • 有効期限:900秒(15分)
  • クレーム
  • sub:memberId(ユーザー識別子)
  • issfilix-shadowwork-api(発行者)
  • audshadowwork-navigator-web(対象者)
  • exp:発行時刻 + 900秒
  • iat:発行時刻
  • 送信方法:Cookie(以下参照)

Set-Cookie: access_token=<JWT>; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=900
- HttpOnly:JavaScript からのアクセス禁止(XSS対策) - Secure:HTTPS のみで送信 - SameSite=Strict:CSRF対策(クロスサイトリクエストで送信しない) - Path=/:全パスで有効 - Max-Age=900:900秒後に自動削除

Memberstack トークン検証
  • 検証方法:Memberstack API に照会(公開鍵による署名検証よりも確実)
  • 検証エンドポイントMEMBERSTACK_VERIFY_URL(環境変数)
  • 検証失敗時:401 エラーを返す

ユーザー特定の流れ

  1. フロント:Shadowwork Navigator でログイン後、Memberstack トークンを取得
  2. フロント:POST /api/auth/exchange に Memberstack トークンを送信
  3. API:Memberstack API で署名検証 → memberId を取得
  4. API:JWT を生成し、Cookie で返却
  5. フロント:以後の API リクエストで自動的に Cookie が送信される(credentials: include)
  6. API:受け取った JWT を検証 → sub から memberId を確定
  7. API:user_id はすべて JWT の sub から取得(クライアント入力は受け付けない)

必須要件(実装前の前提確認)

  • Memberstack API での トークン検証方法が確定していること
  • MEMBERSTACK_VERIFY_URLJWT_SIGNING_SECRET などの環境変数が設定可能であること
  • ブラウザの Cookie 送受信が確実に機能すること(CORS credentials)

CORS(Cross-Origin Resource Sharing)

方針

  • フロント(Shadowwork Navigator)は別ドメイン(https://shadowwork-navigator.com)で運用される。
  • JWT は Cookie で送信するため、CORS で Access-Control-Allow-Credentials: true が必須。
  • 認証情報を含むため、許可 Origin は *(ワイルドカード)禁止、allowlist で明示的に制限する。

許可Origin(Allowlist)

本番

  • https://shadowwork-navigator.com(完全一致のみ)
  • Origin ヘッダがあるリクエストで、allowlist 不一致の場合は 403 FORBIDDENorigin not allowed)を返す。
  • ALLOWED_ORIGINS が空の場合、Origin ヘッダ付きリクエストは許可しない(fail close)。

開発・Preview

  • 原則として preview(*.pages.dev) は許可しない。
  • セキュリティと Cookie 運用との相性が悪いため。
  • どうしても必要な期間だけ、例外として 1件だけ allowlist に追加(例:https://shadowwork-navigator-9lt.pages.dev)。
  • 追加時も、ワイルドカード(*.pages.dev)は禁止。

独自ドメイン導入時

  • 本番 allowlist に追加し、環境変数で管理する。

CORS レスポンスヘッダ

Access-Control-Allow-Origin: https://shadowwork-navigator.com(許可Originのみ)
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Vary: Origin
- Vary: Origin:キャッシュの重複を防止(重要) - Origin ヘッダが無いリクエスト(サーバー間通信やCLI等)は CORS 判定対象外として扱う。

クライアント実装

fetch('https://api.shadowwork-navigator.com/api/thread/start', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  credentials: 'include',  // Cookie の送受信を有効化
  body: JSON.stringify({ /* ... */ })
});

  • 有料状態(paid)は user_flags.paid を参照する。
  • paid の更新経路は 40_課金と利用権限 に従う。
  • 認証により確定した user_id に対してのみ paid を参照する(JWT の sub から確定される)。

Stripe Webhook の署名検証

目的

  • Webhook の送信元が Stripe であることを保証し、偽装を防ぐ。

識別子の取り扱い(Checkout ↔ Webhook)

  • Checkout Session 作成時に、Memberstack の memberId を client_reference_id に設定する。
  • Webhook 側は client_reference_id のみを信頼し、user_id として扱う(フォールバックは行わない)。

必須要件

  • Stripe が付与する署名ヘッダ(例: Stripe-Signature)を検証する。
  • 検証に使用する秘密鍵(webhook secret)は 環境変数(Secrets) として安全に管理する。
  • 検証失敗時は 4xx を返し、DB更新等の副作用を起こさない。

追加要件

  • タイムスタンプ検証により、リプレイ攻撃の難易度を上げる。
  • Webhook の冪等性(重複排除)は 40_課金と利用権限 に従う。

管理用API(/api/admin/*)の保護

/api/admin/set_paid のような管理操作は、通常のユーザー操作と同等には扱えない。
誤用・悪用時の影響が大きいため、強い制限を必須とする。

必須要件

  • 管理者のみ実行可能
  • 管理者トークン、または環境変数で持つ共有秘密を用いて認証する。
  • 本番環境では最小限
  • 可能なら本番で露出させず、運用ツールや一時的な手段に閉じる。

/api/admin/set_paid の認証方式(確定仕様)

  • JWT(Cookie)で管理者(memberId)を特定し、ADMIN_MEMBER_IDS(カンマ区切りallowlist)に含まれること
  • 追加の管理トークン(共有秘密) が一致すること
  • 環境変数: PAID_ADMIN_TOKEN
  • リクエストヘッダ: X-PAID-ADMIN-TOKEN: <token>
  • いずれかが満たされない場合は 403 を返す

追加要件

  • IP 制限(Cloudflare Access / Zero Trust など)
  • 監査ログ(誰がいつ何を変更したか)
  • レート制限(ブルートフォース対策)

Secrets の管理

対象

  • Stripe webhook secret
  • LLM API key
  • 管理者トークン(admin secret)
  • その他、外部サービスの API key / secret

方針

  • Secrets は リポジトリにコミットしない
  • Cloudflare Workers の Secrets / Vars に格納する
  • ログに出さない(例外メッセージに含めない)

PII(個人情報)の取り扱い

取り扱い対象

  • user_id(会員基盤の識別子)
  • ユーザー入力(シャドウワークの内容は機微性が高い)

方針

  • 最小限の保存: 必要な範囲に限定して保存する
  • 復号鍵を保持しない: バックエンドはメッセージ暗号文を復号できない(運用者/管理チームが内容を閲覧できない前提)
  • ログには出さない: 平文(/api/thread/chat 入力、復号後本文)をログに出さない
  • アクセス制御: 認証された本人のデータのみ取得可能とする
  • データ削除: 将来、ユーザー要求に応じた削除・退会対応を検討する(運用ポリシー)

補足: - メッセージ永続化は暗号文保存(/api/thread/message)を前提とし、DBに平文を保存しない。


入力検証(Validation)

  • すべての API は入力を検証し、想定外の型・範囲・サイズは拒否する。
  • LLM利用料金の抑制を目的とした文字数制限は、message/api/thread/chat の平文入力)に適用する。
  • /api/thread/message の保存入力は、平文ではなく暗号文を受けるため、コスト制御ではなく保存API保護(型妥当性・異常入力対策)として検証する。
  • client_message_id などの識別子は型・サイズ制約を設ける。
  • id(run/thread/message 等)は形式を統一し、受け入れ可能な文字種を限定する。

CORS とブラウザ公開

  • ブラウザから API を呼ぶ場合、CORS を適切に設定する。
  • Origin を必要以上に許可しない(固定のフロントURLを許可する)。
  • 認証情報(Authorizationヘッダ等)を使う場合は、CORS で明示的に許可する。

参照

  • 20_API仕様: 認証前提、エラー形式
  • 40_課金と利用権限: Webhook と paid 更新の冪等性
  • 60_利用制限と運用ポリシー: 悪用対応、運用上の制限