50_セキュリティ
本章では、filix-shadowwork-api におけるセキュリティ前提と、認証・署名・Secrets/PII の取り扱いを定義する。
ここで扱うのは「どのように安全性を確保するか」という設計上の前提であり、個別の実装詳細(ライブラリ呼び出し等)は対象外とする。
セキュリティ方針(前提)
-
最小権限(Least Privilege)
API の利用者・運用者・外部サービスに付与する権限は最小化する。 -
入力はすべて不正である前提
すべてのリクエスト入力を検証し、想定外のデータを拒否する。 -
外部サービスとの境界は署名/トークンで保護する
Webhook、管理 API、LLM 連携は必ず認証情報で境界を守る。 -
監査可能性(Auditability)
不具合や不正の切り分けのため、重要操作はログに残る設計にする(PIIを含めない)。
認証(Authentication)
目的
- 「このリクエストが誰のものか」を確実に特定する(なりすまし防止)
user_idの真正性を担保する
前提
- フロントエンドは外部の会員基盤(例: Memberstack)によりログイン状態を得る。
- バックエンドは、フロントエンドから提示される認証情報を検証して user_id を確定する。
認証方式(JWT + Cookie)
採用方式
- 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(ユーザー識別子)iss:filix-shadowwork-api(発行者)aud:shadowwork-navigator-web(対象者)exp:発行時刻 + 900秒iat:発行時刻- 送信方法:Cookie(以下参照)
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 エラーを返す
ユーザー特定の流れ
- フロント:Shadowwork Navigator でログイン後、Memberstack トークンを取得
- フロント:
POST /api/auth/exchangeに Memberstack トークンを送信 - API:Memberstack API で署名検証 → memberId を取得
- API:JWT を生成し、Cookie で返却
- フロント:以後の API リクエストで自動的に Cookie が送信される(credentials: include)
- API:受け取った JWT を検証 →
subから memberId を確定 - API:
user_idはすべて JWT のsubから取得(クライアント入力は受け付けない)
必須要件(実装前の前提確認)
- Memberstack API での トークン検証方法が確定していること
MEMBERSTACK_VERIFY_URL、JWT_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 FORBIDDEN(origin 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 等)
- 有料状態(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_利用制限と運用ポリシー: 悪用対応、運用上の制限