bff-entry-points について
ブラウザーおよび非ブラウザーから呼び出される BFF の入口を保護する開発者向けスキルです。明示的なアクセス分類と準備済みレジストラーを軸に、検証チェーンを強制し、実行時の挙動と API ドキュメントを一致させます。
主な機能
- 01入口ごとの明示的なアクセス分類
- 02レジストラーによる検証チェーンの適用
- 03アクセス宣言に基づくハンドラー型
- 04導出カタログと OpenAPI の照合
- 05リアルタイム接続とブラウザーセッションの保護指針
活用シーン
- 01ルートにデフォルト拒否のアクセス制御を追加
- 02ブラウザーの変更リクエストを CSRF から保護
- 03SSE と WebSocket の入口を保護
- 04自動ゲートでルートと API ドキュメントを照合
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ citypaul/ .dotfiles --skill bff-entry-pointsインストール後、Claude Code で/bff-entry-pointsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
agents/openai.yamlreferences/browser-session-coordination.mdendpoint-protection.mdenforcement-and-testing.mdhexagonal-auth-boundaries.mdhono-example.mdrealtime-entry-points.md詳細
BFF エントリーポイント
BFF は処理を駆動するホストです。ブラウザー向け HTTP 境界を所有し、単一のユーザー体験に代わってアプリケーション操作を呼び出します。このスキルは、その境界の動作上のセキュリティモデル、つまり各エントリーポイントを呼び出せる主体、その制御を構造によってどのように強制するか、呼び出し元がブラウザーではない場合にアプリケーションをどのように安全に保つかを扱います。ブラウザーベースのアプリ向けの IETF の BCP トラックのガイダンスに準拠しています。トークンはサーバー側に置き、ブラウザーにはセッション Cookie のみを保持します(draft-ietf-oauth-browser-based-apps §6.1 — 執筆時点で RFC Editor のキューにある BCP トラックの文書。RFC 9700 / BCP 240 は公開済みの OAuth セキュリティベースラインです)。
このスキルが前提としているパターンレベルの決定事項(そもそも BFF を導入するか、いくつ用意するか、集約と部分的な失敗の設計、上流サービスへユーザー ID を仲介する方法)については、bff-design を読み込んでください。物理的な endpoints/ ツリー、ルートカタログファイル、開発専用のルーティングについては、structure-codebase を読み込んでください。このスキルでは、各エンドポイントのリーフが宣言すべき内容と、登録による強制方法を定義します。ポートとアダプターについては、hexagonal-architecture を読み込んでください。このスキルでは、認証はアダプターの役割であり、認可はアプリケーションポリシーであるというルールをさらに掘り下げます。REST の規約、エラー本文の形式(RFC 9457)、レート制限、バージョニングについては、api-design を読み込んでください。ログインの背後にある OAuth/OIDC フローについては、secure-oauth-oidc を読み込んでください。このスキルでは、その結果として得られるアプリケーションセッションを扱います。レジストラーの契約を設計する際は、意図的に詳細なモジュールである codebase-design を読み込んでください。
実装前に、関連するリファレンスをお読みください。
- エンドポイント契約の形、セッション Cookie のプロファイル、Origin/Fetch Metadata/CSRF/コンテンツタイプのポリシー、およびそこから導かれるエントリーカタログについては、
references/endpoint-protection.mdをお読みください。 AuthenticatedPrincipal、アプリケーション内の認可、アクターのマッピング、封じ込めとしての行レベルセキュリティについては、references/hexagonal-auth-boundaries.mdをお読みください。- 保護された SSE ストリームと、保護された WebSocket アップグレード用レジストラーについては、
references/realtime-entry-points.mdをお読みください。 - ブラウザー側の単一の認証コーディネーターについては、
references/browser-session-coordination.mdをお読みください。 - 自動化されたゲート、ポジティブコントロール、悪意のある入力を想定したテストマトリクスについては、
references/enforcement-and-testing.mdをお読みください。 - 具体的な Hono レジストラーについては、
references/hono-example.mdをお読みください。このモデルはフレームワークに依存せず、Fastify、Express、Fetch ベースのルーターにも適用できます。
コアモデル:明示的なアクセス分類
すべての本番エントリーポイントで、アクセス分類を宣言します。暗黙のデフォルトはありません。分類されていないルートはビルド失敗となり、「たまたま最初に実行されるミドルウェアで保護される」ことはありません(OWASP API5:2023 はデフォルト拒否と明示的な許可を義務付けています。CWE-306 が脆弱性として示しているのは、チェックの不備ではなくチェックの欠落です)。
ブラウザー向け HTTP エンドポイントに必要な最低限の分類:
| 分類 | 意味 | レジストラーが組み込むもの |
|---|---|---|
public | アプリケーションのセッションなしで意図的に呼び出せる | 検証、不正利用対策、明示的に未認証としたドキュメント |
protected-read | 認証済みユーザー向けの安全なメソッド | セッション解決 → 401 → アプリケーション内での認可 |
protected-browser-mutation | ブラウザーから開始される状態変更 | Origin/Fetch Metadata/コンテンツタイプのポリシー → セッション → CSRF → 認可 |
// Canonical definition — references restate it only by pointer.
type EndpointAccess =
| { readonly kind: 'public'; readonly justification: string }
| { readonly kind: 'protected-read' }
| { readonly kind: 'protected-browser-mutation' }
| { readonly kind: 'protected-upgrade' }; // raw WebSocket upgrades — realtime reference
プロジェクトでは、判別共用体を拡張して、サービス資格情報、署名付き Webhook、管理操作などを追加できます。ただし、すべてのエントリーポイントで、明示的かつ網羅的な宣言を 1 つ行う必要があります。また、追加する各種別について、レジストラー内で独自の検証チェーンを定義します。ブラウザー以外の呼び出し元を持つアダプター(CLI、ジョブランナー、キューコンシューマー)も例外ではありません。それぞれの方式で認証し、独自の呼び出し元プリンシパルを構築します(ヘキサゴナルリファレンスのシステム呼び出し元のセクションを参照してください)。
Public は、信頼ではなく呼び出し元についての宣言です。 public エンドポイントでも、検証、レート制限、監視を行い、シークレットを公開しないようにします。ネットワーク内部専用ではなく、不正利用対策も免除されません。ヘルスプローブは典型例です。public ですが、返すのは判定のみです(依存関係名、バージョン、スタックトレースは含めません)。public にする範囲はアプリケーションで決定します。対象は小さく保ち、レビュー済みの許可リストテストでスナップショットを取り、差分はすべてセキュリティレビューの対象として扱ってください。
信頼できる情報源と派生カタログ
エンドポイントの契約は機能内に配置します。ルート、検証スキーマ、アクセス宣言、ハンドラー、ドキュメントをエンドポイントの末端にまとめることで、1 回のレビューで全体を確認できます。同じ場所への配置は、1 つのファイルにまとめることを意味しません。大規模なレスポンスマッピングとワークフローの配置は、structure-codebase が担います。
HTTP 境界では、アプリケーションの結果を安全なレスポンスに変換します。BFF ワークフローでは、アプリケーション呼び出しをホストが所有する公開ポリシーやライフタイムポリシーと連携できます。一方、アプリケーションの認可、事実の解決、ドメイン上の判断、条件付き永続化は、引き続き現在の所有者が担います。公開が非同期の場合、HTTP の成功は後続処理への配信を保証しません。他の呼び出し元が勝ち取った状態を公開することなく、既存の競合時および存在秘匿のセマンティクスを維持してください。
Effect ベースのハンドラーについては、ランタイム構成のガイダンスを参照してください。
手作業で保守する中央のルートセキュリティマップを別途作成しないでください。実行時の登録内容と乖離し、その乖離はインシデントが起きるまで見えません。代わりに、すでに存在する次の 2 つの情報から、中央エントリーカタログを生成してください。
- 機能内に配置されたエンドポイント契約。
- 明示的な本番構成。実際にマウントされた契約であり、生の WebSocket アップグレードも含みます。
レジストラーはすべてのマウントを記録します。カタログはその出力であり、入力ではありません。適用テストでは、カタログとフレームワークのルートテーブル、および生成された OpenAPI ドキュメントを照合します。これにより、中央での可視性を手作業で維持するのではなく、自動化できます。
準備済みレジストラー(深いモジュール)
コンポジションでは、レジストラーを一度だけ準備し、エンドポイントが決して自ら選択してはならないすべての要素を渡します。セッションの解決、ブラウザーリクエストのポリシー、内部情報を含まない安全なエラー変換(RFC 9457 の Problem Details)、テレメトリーです。エンドポイントの担当者が用意するのは、コントラクトと薄いハンドラーだけです。
// composition/ — the only place that knows the enforcement machinery
const endpoints = createEndpointRegistrar({
app, // framework instance (Hono, Fastify, Fetch router...)
sessions, // cookie → validated session → AuthenticatedPrincipal
browserPolicy, // exact-origin allowlist, Fetch Metadata, CSRF, content types
abuse, // rate limits and payload caps, keyed by endpoint contract
errors, // safe problem-details translation
telemetry,
});
// endpoints/api/orders/by-order-id/get.ts — contract + thin handler only
endpoints.register(getOrderContract, async ({ principal, params }) => {
const result = await orders.viewOrder({ principal, orderId: params.orderId });
return toHttpResponse(result);
});
エンドポイントの担当者が認証ミドルウェアを選択、並べ替え、省略することはできません。オプションのフラグはありません。ハンドラーの型は宣言から導出されます(エンドポイント保護のリファレンスで定義されている HandlerFor<Contract>)。public ハンドラーにはプリンシパルが渡されません。保護されたハンドラーには、ハンドラー自身では生成できない AuthenticatedPrincipal が渡されます。構築不能性は慣例ではなく、モジュール境界とインポート境界のゲートによって保証されます。パブリックエンドポイントでは、レジストラーがハンドラーをマウントし、未認証であることを明示した API ドキュメント(security: []。意図的に grep 可能なマーカーであり、フィールドを省略することはありません)を出力します。保護されたエンドポイントでは、構造上、検証チェーンを組み込み、それに対応する OpenAPI のセキュリティメタデータを出力するため、実行時の動作とドキュメントに食い違いが生じることはありません。
これは codebase-design の意味での深いモジュールです。小さく安定したコントラクト(register)の背後に、順序、ヘッダーポリシー、エラー変換、ドキュメント生成を隠しています。レジストラーのコアはフレームワークに依存しない状態に保ち、Hono/Fastify/Express API に触れるのは内部アダプターだけにします。
ここから二つの帰結が導かれます。どちらも新規開発の場合だけでなく、変更のたびに守らなければなりません。チェーンはレジストラー内にのみ置き、それ以外には置きません。エンドポイントモジュールで Origin、CSRF トークン、コンテンツタイプを独自にチェックすると、レジストラーが閉じるために存在する境界を再び開くことになります。不足しているチェックをレジストラーのチェーンに追加し、エンドポイントにはチェック済みの状態を渡してください。変更するルートは、その変更の中で必ずレジストラーを経由させてください。すでにアプリに直接マウントされているルートも対象です。宣言を追加して、マウント先を移してください。マウント先の変更によってアプリファクトリーのシグネチャや依存関係は変わらないため、それらを現状のままにするよう指示されても、バイパスを残す理由にはなりません。状態を変更する種類は、完全なチェーンがそろった場合にのみ登録します。 その種類を追加する変更では、レジストラーの該当する分岐に、ハンドラーの実行前に次のすべてを追加します。Origin の完全一致許可リスト照合、Fetch Metadata チェック、コンテンツタイプチェック、セッション解決、セッションに紐づいた CSRF トークンチェックです。種類の名前にかかわらず、5 つすべてが必要です。セッションを解決してコンテンツタイプだけをチェックする分岐は、ボディを受け入れる読み取りチェーンです。その分岐に変更処理を登録するのは、このスキルが防ぐべき省略です。チェーンに必要なものがコンポジションにまだない場合は、同じ変更でそれも実装してください。許可リストが設定されていない場合、1 つのフロントエンドを提供するサービスでは、リクエスト自身のオリジンと一致させます。セッションレコードにトークンフィールドがない場合は、セッション ID に対する HMAC でトークンを生成します(以下のステップ 3)。協力コンポーネントが不足しているなら、それはこの変更で対応すべき作業であり、チェックを減らしてよい理由にはなりません。
リクエストの順序
保護された読み取りの場合:
- アプリケーションセッションを解決し、検証します。
- 認証情報がないか無効な場合は、安定した 401 を返します。
- アプリケーション内で、要求された製品リソースまたは操作を認可します。
- その後に限り、読み取り、サブスクライブ、認証情報の発行、その他の保護対象の処理を行います。
保護されたブラウザーの変更操作の場合:
- 無効な Origin、不適切な Fetch Metadata、またはサポートされていないコンテンツタイプを拒否します。セッションの有無は明かしません。
- アプリケーションセッションを解決します。
- セッションに結び付いた CSRF 保護を検証します。これは省略できず、後回しにもできません。セッションレコードに同期トークンを保持できない場合は、サーバー側のシークレットを使ってセッション ID に対する HMAC でトークンを生成し、Origin とコンテンツタイプの検証チェーンに変更操作を登録してトークンの検証を後回しにするのではなく、そのトークンを検証します(署名付きダブルサブミット、セッションに結び付いた方式)。
- 主体と検証済みのパラメーターから実施できる、大まかな操作またはパスリソースの認可を行います。
- サイズ上限のある本文を解析して検証します。
- 本文から参照されるすべてのオブジェクト、フィールド、対象、操作を認可します。
- その後に限り、処理を実行します。
コントラクトのパスでは、HTTP とアップグレードのコントラクトの両方で、すべて OpenAPI の {param} 構文を使用します。レジストラーがフレームワークの構文に変換し、カタログにはコントラクト形式で保存します。リソースモデルに即している場合は、主要な対象の識別子をパスに含めます。ただし、本文に依存する認可を避けるためだけに、バッチ、共有、フィールドレベルの操作を無理に変更してはいけません。遅延評価される body() のサンクを使えば、解析前に大まかなパスチェックを行えます。サイズ上限のある本文データはすべて、引き続き、処理の実行前に検証して認可する必要があります。
安定した失敗セマンティクス — 分類ごとに外部から同じ応答を返し、同じコードパスを使用し(原因別のショートカットは使わず)、情報を推測する手掛かりを決して与えません。
| ステータス | 意味 |
|---|---|
| 401 | 認証が必要、または認証が無効になっています |
| 403 | 意図的に開示するリクエストポリシー違反、または既知の禁止事項です |
| 404 | 存在を開示してはならない、アクセス不可またはテナントをまたぐリソースです(RFC 9110 §15.5.4 で明示的に認められています) |
| 415 | サポートされていないコンテンツタイプです |
API、SSE、WebSocket の各エントリーポイントは、プロトコルに応じた失敗応答を返します。HTML のログインページへリダイレクトすることはありません。リダイレクトされた fetch は解析不能な 200 HTML を返し、リダイレクトされた EventSource はコンテンツタイプの不一致により不透明な形で失敗するためです。
ヘキサゴナルアーキテクチャにおける責務の分離
BFF は駆動アダプターです。Cookie とリクエストヘッダー、セッションの解決、Origin/Fetch Metadata/CSRF/コンテンツタイプのポリシー、HTTP およびアップグレードプロトコルの応答、認証済みセッションからプロバイダーに依存しないプリンシパルへの変換、アプリケーション操作の呼び出しを担当します。
type AuthenticatedPrincipal = {
readonly userId: UserId; // branded domain types, not provider IDs
readonly tenantId: TenantId;
};
BFF はプロダクトの認可を担当しません。保護対象のアプリケーション操作はすべて、保護対象の処理を実行する前に、プリンシパルを個別に認可します。これは、呼び出し元が HTTP、CLI、テストハーネス、別の BFF、将来の駆動アダプターのいずれであっても変わらない前提条件です(OWASP ASVS 5.0 §8.3.1:信頼できるサービス層で強制する)。内部のコードが Hono、Cookie、Keycloak、OAuth トークン、プロバイダーのグループ、HTTP ステータスコード、ブラウザーのフィールドに依存することはありません。
認証が答えるのは「誰が呼び出しているのか?」です。これはアダプターの責務です。認可が答えるのは「この呼び出し元は、このプロダクト操作を実行できるか?」です。これはプロダクトの用語で表現されるアプリケーションポリシー(viewOrder が別テナントの注文を拒否するなど)であり、汎用の認可ユーティリティの寄せ集めではありません。ルールが存在する場所で、そのルールを証明してください。つまり、そのテストは別のプリンシパルを使って操作を直接呼び出し、HTTP も Cookie もレジストラーも使わずに、拒否の結果とフェイクへの影響がないことを検証します。ルート経由で実行するテストで証明できるのは配線であり、ルールではありません。追加または変更した認可ルールを HTTP 経由でのみテストしている場合、作業は未完了です。次の駆動アダプターはそのルートを通らないからです。認可が成功した後に限り、プリンシパルを追跡可能なドメインアクターに対応付けます。データベースの行レベルセキュリティは、チェック漏れに対する最後の防御であり、権限の根拠ではありません。
この設計を採用する理由
エンドポイント保護の設計を比較しました。
| 設計 | 評価 |
|---|---|
| 1. パブリック例外を設けた、パスプレフィックスによるグローバルミドルウェア | 暗黙的で、順序の変更に弱い設計です。保護は登録順とパスの照合に左右され、アップグレードリクエストは回避でき、例外リストはレビューされていないパブリック許可リストになります。ミドルウェアのみによる保護は実環境で回避されています(Next.js CVE-2025-29927、Clerk CVE-2026-41248、Traefik fail-open GHSA-4mr2-fg2p-w63c)。 |
| 2. 手作業で保守する一元的なルートセキュリティマップ | 実行時の登録内容と乖離する、第二の信頼できる情報源です。乖離は気付かれないまま進み、フェイルオープンになります。 |
| 3. 任意で付けるルートローカルミドルウェア | 設計上フェイルオープンです。チェックが存在しないこと自体が脆弱性になります(CWE-306)。すべてのルート所有者が順序を再判断し、1 つでも抜けるとレビューで見つかりません。 |
| 4. 必須の機能ローカル宣言+準備済みレジストラー+派生カタログ | 推奨。 構造上フェイルクローズとなり、末端でレビューでき、派生によって一元的に可視化され、テストで強制できます。 |
設計 4 のトレードオフは、承知のうえで受け入れています。レジストラーは、まず構築が必要な仕組みであり、それ自体もテストする必要があります。新たな認証方式が加わるたびに、分類モデルを網羅的な状態に保つ必要があります(共用体を拡張し、ブール値フラグは追加しないでください)。カタログの派生にはコントラクトと本番環境のコンポジションの両方が必要なため、コンポジションは明示的なままにします。また、非常に小規模なサービスであれば、設計 1 と全ルート列挙の監査テストから始めるのも妥当です。ただし、2 つ目の認証方式、または最初のリアルタイムエンドポイントが導入される前に、レジストラーを採用してください。
既存システムに導入する場合は、書き換えるのではなく、段階的に基準を引き上げます。(1)まずルート列挙テストを追加し、現在未分類のルートをすべて明示的な例外リストに記載します。このリストは縮小のみ可能とします。(2)新しいエンドポイントはすべて、初日からレジストラー経由で登録します。(3)既存のルートは、権限の高いものから順に、1 つずつ移行します。(4)ルートの移行ごとに、プロバイダーに依存しない拒否の直接テストを操作単位で追加します。テストを書けない場合、その操作の認可はミドルウェアだけに依存しているため、次に修正すべき対象です。(5)例外をゼロにしてからゲートを強化し、その後で初めてカタログを信頼します。
アンチパターン
- 本番コードで、登録機構を迂回してフレームワークのアプリに直接マウントされたルート。登録機構より前から存在するルートも含みます。そのようなルートに何らかの変更を加える場合、特にセキュリティ修正を行う場合は、同じ変更でそのルートを登録機構に移し、独自のアクセス宣言を付ける必要があります。編集したのにマウントしたままの迂回ルートは、今度は意図的に残したルートとなります。また、その場でロジックだけを修正すると、次のミスが未分類のまま残ります。
- エンドポイントの担当者が自身のチェーンを弱められる
requireAuth: false、skipCsrf、その他のブール値。 - 手作業で編集する保護対象パスのリスト、または認可境界としてのパスプレフィックス照合。
- HTTP ミドルウェアだけで認可を決定すること。HTTP 以外の呼び出し元は認可を完全に迂回できてしまいます。
- プロバイダーのフィールド(トークンクレーム、Keycloak グループ、Cookie 名)をアプリケーションに持ち込む
AuthenticatedPrincipal。 - セッションから導出したプリンシパルではなく、クライアントから渡されたアクター、ユーザー ID、テナント ID を信頼すること。
- テナントをまたぐリソースへの 403。リソースの存在を明らかにするため、オラクルにならない 404 を使用してください。
- API、SSE、WebSocket のエントリーポイントをログインページにリダイレクトすること。
- ブラウザーのストレージにプロバイダートークンやコールバックパラメーターを格納すること(OWASP Session Management Cheat Sheet。BFF があるのは、ブラウザーに Cookie だけを保持させるためです)。
- 開発専用エンドポイントが本番環境の構成から到達可能であること(OWASP API9:2023)。
完了チェック
- マウントされたすべての本番エントリーポイント(生のアップグレード、および以前はアプリに直接マウントされていた変更対象のルートを含む)に、登録機構を通じた明示的なアクセス分類がありますか?
- パブリックエンドポイントのセットを出力でき、それをレビュー済みの許可リストテストで固定していますか?
- 登録機構を使わずにルートをマウントする方法がありますか?ある場合、ゲートは失敗しますか?
- 保護されたハンドラーは、構築できないプリンシパルを宣言によって型付けして受け取りますか?
- プロバイダーに依存しない直接テストにより、各保護対象の操作が、いかなる作用も起こす前に未認可のプリンシパルを拒否することを確認していますか?
- ランタイムの動作、導出されたカタログ、OpenAPI のセキュリティメタデータは一致していますか?規律ではなく、テストで検証されていますか?
- 401、403、404、415 は、サーフェス全体で安定した、オラクルにならないセマンティクスを保っていますか?
- ブラウザーに認証コーディネーターは 1 つだけありますか?また、リアルタイム処理で障害が発生した際、即座にサインアウトするのではなく、重複排除された現在のユーザー確認を 1 回だけ実行しますか?