SPA(Single Page Application)で OAuth を扱う際、アクセストークンやリフレッシュトークンをどこに置くかは長年議論されてきました。localStorage、sessionStorage、メモリ内変数、いずれも XSS の前では十分な防御になりません。BFF(Backend for Frontend)パターンは、トークンをブラウザに渡さず、サーバー側で保持する設計です。今回はその仕組みと実装上の要点を整理します。
前提
本記事は次の環境を想定しています。
- OAuth 2.0 と OpenID Connect を利用したブラウザベースアプリの開発であること
- SPA とそれが呼び出す API、認可サーバーが存在すること
- SPA とそれをサポートするサーバー側コンポーネントを同じ親ドメインに配置できること
- HTTPS が前提であること(Secure Cookie の発行に必要)
**
ブラウザベースアプリが抱える XSS リスク
ブラウザ内で動くアプリケーションコードは、ブラウザの実行環境で起こることすべてに対して脆弱です。XSS は影響範囲が大きく、攻撃コードはアプリと同じコンテキストで動くため、次のような操作が可能になります。
- localStorage や sessionStorage に格納された値の読み取り
- JavaScript からアクセス可能なメモリ内変数の読み取り
- アプリが行えるすべての API 呼び出しの実行
- 組み込み関数の上書き(プロトタイプ汚染)による挙動の変更
依存ライブラリの脆弱性、自前コードの入出力処理の欠陥、サードパーティスクリプトの侵害など、XSS の侵入経路は複数あります。完全に XSS を防ぐことは難しいため、「侵入された場合の影響を限定する」方針が現実的です。
トークンをブラウザに置く限り、XSS でトークンが盗まれる可能性は残ります。盗まれたリフレッシュトークンを攻撃者が手元の環境で使えば、ユーザーが Web サイトを閉じた後も長時間にわたり API を呼び出せます。トークンローテーションやアイドルタイムアウトで影響を軽減することはできても、根本的な解決にはなりません。
トークンをブラウザに置かない設計
BFF パターンは、SPA 専用のサーバー側コンポーネントを用意し、そこに OAuth クライアントの責務を集約します。SPA は OAuth の処理を直接行わず、BFF を経由して認証と API 呼び出しを行います。
役割分担は次のようになります。
- 認可サーバーとの OAuth プロトコル通信:BFF が担当
- アクセストークン、リフレッシュトークンの保持:BFF のみ
- SPA と BFF 間の認証状態の維持:HttpOnly Cookie
- API 呼び出し:SPA は BFF にリクエストし、BFF が Cookie をトークンに変換して API へ転送
この構成では、トークンは BFF とそこから呼び出される API の間でのみ流通し、ブラウザの JavaScript からは見えません。XSS が発生しても、攻撃者がトークンを取り出して別の場所から使うことはできません。攻撃者にできるのは、ユーザーが現在開いているセッションの範囲内で BFF にリクエストを送ることだけです。これでも影響は無視できませんが、トークン窃取と比べれば影響の持続時間と範囲が大きく制限されます。
BFF は OAuth の用語で言う confidential client(機密クライアント)として振る舞います。クライアントシークレットを持ち、認可コードとトークンの交換、リフレッシュ処理をすべてサーバー側で完結させます。
認証フローの流れ
典型的な認証フローは次の通りです。

- SPA がログインを開始すると、BFF に対してログイン要求を送信します
- BFF は PKCE 付きの認可コードフローを開始し、認可サーバーへのリダイレクト URL を生成して SPA に返します
- SPA はその URL にブラウザを遷移させ、ユーザーは認可サーバーで認証します
- 認可サーバーは BFF の redirect URI に認可コードを返します
- BFF は認可コードをトークンと交換し、アクセストークンとリフレッシュトークンを取得します
- BFF はトークンを自身の安全な領域(暗号化された Cookie、サーバー側セッションストア、いずれかの形式)に保存し、SPA には HttpOnly Cookie でセッション識別子のみを返します
- SPA が API を呼ぶ際は、BFF 経由でリクエストを送ります。Cookie が同時に送られ、BFF がそれをアクセストークンに変換して上流の API へ転送します
- アクセストークンが期限切れになった場合、BFF がリフレッシュトークンを使って静かに更新します
SPA から見ると、ログイン状態は Cookie で維持され、API リクエストは通常の fetch 呼び出しで完結します。OAuth のトークンや認可コードを直接扱うコードは SPA には存在しません。
必要なセキュリティ設定
BFF が発行する Cookie には、次の属性を必ず設定します。
- HttpOnly:JavaScript からのアクセスを禁止します。XSS でも Cookie の中身を読まれません
- Secure:HTTPS でのみ送信します
- SameSite=Strict:他サイトからのリクエストでは Cookie が送られないようにします。CSRF 攻撃の主要な経路をふさぐ補助になりますが、これ単独で CSRF 対策が完結するわけではありません(後述)
- __Host- プレフィックス:Cookie 名の先頭に __Host- を付けると、ブラウザはその Cookie を発行元ホストに限定し、サブドメインと共有しないことを保証します。サブドメイン経由のセッション固定・セッションハイジャックへの防御になります
トークンを暗号化 Cookie に格納する(client-side session)場合は、Cookie の内容を暗号化します。サーバー側セッションストアにトークンを置く(server-side session)場合は、Cookie にはセッション識別子のみが入るため暗号化の必要はありません。
CSRF 対策は SameSite だけに依存しない
Cookie ベースの認証を使う以上、BFF は CSRF(クロスサイトリクエストフォージェリ)への防御を必ず実装する必要があります。SameSite=Strict は有効な一手ですが、対策の全体ではありません。とくに今回想定する「SPA を www.example.com、BFF を api.example.com に置く」構成では注意が必要です。SameSite の判定は origin(オリジン)ではなく site(登録可能ドメイン単位)で行われるため、example.com 配下の別サブドメインからのリクエストは「same-site」と見なされ、SameSite=Strict でも Cookie が送られてしまいます。攻撃者が同じ親ドメインの別サブドメインを利用できる状況では、SameSite だけでは防ぎきれません。
そのため、次のいずれか(または併用)で CSRF 防御を補強します。
- BFF と SPA が別オリジンの場合、CORS と Origin ヘッダー検証を防御に利用する。攻撃者がユーザーのブラウザからクロスオリジンで BFF を呼ぼうとしても、プリフライトで拒否され実リクエストがブロックされます
- 状態を変更するリクエストに対して、anti-forgery / double-submit cookie 方式の CSRF トークンを検証する。フロントエンドだけが読めるトークンをカスタムヘッダーに載せ、BFF 側で照合します
**
CORS は正確な origin に限定する
CORS は SPA の正確な origin に限定して許可します。ここで重要なのは、Cookie を伴う認証付き(credentialed)リクエストでは、ブラウザは Access-Control-Allow-Origin にワイルドカード(\)を そもそも許可しない *という点です。したがって BFF は SPA の正確な origin をレスポンスに反映する必要があり、ワイルドカードは選択肢になりません。そして、この「許可する origin を厳密に限定する」こと自体が、上で述べた CORS ベースの CSRF 防御の一部として機能します。
BFF が保持するセッションデータ、特に暗号化された Cookie に格納するトークン情報には、鍵管理が必要です。鍵を BFF の設定として安全に注入し、定期的にローテーションする運用を組み込みます。
なお、SPA と BFF を同じ親ドメインに配置するのは、Same-Site Cookie がファーストパーティ Cookie として機能する条件を満たすためです。たとえば SPA を www.example.com で配信し、BFF を api.example.com で公開すれば、同じ example.com の中で API を呼び出せ、Cookie がファーストパーティとして送られます。
API 駆動 BFF という発展形
BFF を従来型の Web アプリのように作ると、SPA のページ遷移や状態管理に BFF のサーバーレンダリングが絡んでしまい、Web アーキテクチャ全体に影響が出ます。API 駆動 BFF は、この影響を最小限にする構成です。
役割を二つに分けます。
- OAuth Agent OAuth プロトコルの処理(認可コード交換、トークンリフレッシュ、ログアウト)を担う API。SPA から JSON で呼び出される
- OAuth Proxy API ゲートウェイのプラグインとして動作し、Cookie からトークンを取り出して Authorization ヘッダーにセットしてから上流 API へ転送する
この構成では、SPA は通常の REST API として OAuth Agent を呼び出すだけで、ページ遷移や HTML レンダリングは介在しません。SPA のフロントエンド開発体験は BFF を導入する前とほぼ同じに保てます。
OAuth Agent はステートレスに設計でき、複数のレプリカに水平スケールしやすくなります。OAuth Proxy はゲートウェイのプラグインなので、既存の API ゲートウェイ運用に組み込めます。
採用時の注意点
BFF パターンを採用する際には、次の点を考慮します。
- アーキテクチャの追加コンポーネントが増えます。OAuth Agent と API ゲートウェイのプラグインの開発・運用が必要になります
- SPA と BFF を同じ親ドメインに配置する必要があります。CDN 配信などで SPA のドメインを分けたい場合は、ドメイン構成を見直す必要があります
- リバースプロキシで Authorization ヘッダーを単に転送するだけの構成は BFF ではありません。トークンがブラウザに渡るなら、ネットワークホップを増やしただけになります。BFF は confidential client としてトークンを保持することが本質です
- PKCE と BFF は併用するもので、代替関係ではありません。BFF が認可コードフローを実行する際にも PKCE を使います
- BFF が転送先を検証しないと、攻撃者に操作されて意図しないホストへリクエストを転送し、アクセストークンを露出させる恐れがあります。BFF は転送前に宛先ホストを検証し、承認済みリソースサーバーの明示的な allowlist だけにプロキシするよう実装します(例:/bff/orders/create は https://order-api.example.com/create のみに対応させる)
- ログアウトの設計が複雑になります。SPA 側のセッション終了、BFF の Cookie 失効、認可サーバーでの SSO セッション終了、それぞれを連動させる必要があります
**
まとめ
ブラウザベースアプリでトークンを安全に扱う実用的な方法は、トークンをブラウザに置かないことです。BFF パターンは OAuth クライアントの責務をサーバー側に移し、ブラウザには HttpOnly Cookie だけを渡します。これにより XSS が発生した場合でもトークン窃取は起こりません。ただし Cookie ベースである以上、CSRF 対策(SameSite に加えた CORS / Origin 検証や CSRF トークン)と転送先の検証は、設計時に必ず組み込む必要があります。
API 駆動 BFF として OAuth Agent と OAuth Proxy に役割を分けると、SPA の開発体験を維持しつつセキュリティを強化できます。追加コンポーネントの運用コストはかかりますが、トークンを直接ブラウザで扱う設計と比べれば、被害の上限が明確に下がります。XSS を完全に防ぐことができない以上、影響を限定するこの設計は、本番運用の SPA で検討する価値があります。





