トークンをブラウザに置かない方法、BFF
一言でいうと
SPAがaccess tokenを直接持つと、そのページで動くスクリプトが1つ汚染されるだけでトークンが外に出てしまいます。BFF(Backend for Frontend)はトークンをサーバーだけに置き、ブラウザーにはHttpOnlyのセッションCookieだけを渡します。盗めるトークンがブラウザーにそもそも存在しないようにする設計です。
なぜ必要なのか
しばらくの間、SPAの標準的な構成はこうでした。ブラウザーが発行者からaccess tokenを受け取ってlocalStorageに入れ、APIを呼ぶたびにAuthorization: Bearerで付けます。サーバーが状態を持たないので楽です。問題は、そのトークンを読める主体がこのページで動くすべてのスクリプトだという点にあります。広告タグ、分析スクリプト、汚染されたnpmパッケージ、XSSの1行など、何であってもトークンを読んで攻撃者のサーバーへ送れますし、そのトークンは攻撃者の手の中で有効期限まで使えます。ユーザーがタブを閉じても意味がありません。
IETFはこの問題を長く磨き上げ、RFC 10017 OAuth 2.0 for Browser-Based Applications(ドラフト名はdraft-ietf-oauth-browser-based-apps)として発行しました。複数の構成を比べたうえで、6.1節にBFFを置いています。要点は1文です。トークンがBFFにしかないので、ブラウザーから持ち出せるトークンがありません。
どう動くのか
BFFは、フロントエンドと同じオリジンに立つ小さなサーバーです。このサーバーがOAuthクライアントの役割をまるごと引き受けます。
브라우저 ── GET /login ─────────────▶ BFF (state 를 담은 임시 세션 발급)
브라우저 ── 302 ▶ 발급자 /authorize (로그인) ── 302 ▶ BFF /callback?code=..
BFF ─────── code + client_secret ──▶ 발급자 /token → access token
BFF: 토큰을 서버 쪽 세션에 저장, 세션 ID 재발급, HttpOnly 쿠키만 내려줌
브라우저 ── GET /bff/api/me + 쿠키 ─▶ BFF ── Authorization: Bearer ▶ 하류 API
守るべきルールは6.1.3節にまとまっています。
- 機密クライアント。BFFは発行者に資格情報(クライアントシークレット)を持ち、認可コードグラントを使います。コードがアドレスバーやログから漏れても、シークレットなしではトークンに交換できません。
- Cookie。
SecureとHttpOnlyはMUST、SameSite=StrictとPath=/、そしてHTTPだけで設定されたことを示す__Host-Http-のような接頭辞はSHOULDです。Cookieにはセッションが入るだけで、その値は下流のAPIにとって何の意味もありません。 - 転送。BFFはリクエストからCookieを外し、ユーザーのaccess tokenを付けて下流へ渡します。下流のAPIはCookieを知らず、Bearerトークンだけを信頼します。トークンがない、または無効な場合は、RFC 6750 3節のとおり401と
WWW-Authenticate: Bearerを返します。 - CSRF。ブラウザーがCookieを自動で載せるため、BFFにはCSRF対策がMUSTです。RFCは、SameSite=Strictと、カスタムリクエストヘッダーを要求してクロスオリジンのリクエストを必ずCORSのプリフライトに通す方法を挙げています。このラボは前のモジュールで作った、セッションにバインドしたCSRFトークンをカスタムヘッダーで送り、2つの性質を同時に得ます。
- ログアウト。BFFのセッションだけを消しても、発行者側のトークンは有効期限まで生きています。RFC 7009の取り消しエンドポイントに先に知らせます。
現場での姿
BFFを導入した後で最も多い事故は、「利便性のため」にトークンが再び漏れることです。セッション確認のレスポンスにaccess_tokenを一緒に載せたり、フロントが下流のAPIを直接呼びたいからと、JSが読めるCookieでトークンをもう1つ返したりします。その瞬間、BFFは名前だけになります。逆方向もあります。下流のAPIが「内部ネットワークだから」とトークンなしのリクエストを受け付けると、BFFを飛ばした呼び出しがそのまま通ります。
限界も知って使う必要があります。6.1.4.1節は、悪意あるスクリプトがユーザーのブラウザーを経由してBFFへリクエストを送るクライアントハイジャックは、BFFでも防げないと書いています。BFFがなくすのはトークンの窃取であって、XSSそのものではありません。ユーザーがタブを閉じれば攻撃も終わること、その違いがBFFの価値です。
次のラボですること
1つのPodの中で発行者(8312)・下流API(8311)・BFF(8310)を起動し、ブラウザーの役をcurlで務めてログインの流れをたどります。BFFが返したCookieの属性を点検し、RedisのBFFセッションから本物のトークンを取り出して、ブラウザーが見たレスポンスのどこにもその値がないことを確認します。続いて、BFF経由の呼び出しは200、下流への直接呼び出しは401になるか、CSRFトークンのない書き込みが防がれるか、ログアウト後に古いトークンが下流で無効になるかまで、リクエストで証明します。