TT Lab
Get started
Learn Learning paths Courses

Sessions and Tokens — From the Browser to the Mesh

How not to keep tokens in the browser: the BFF

Continue in TT Lab

In one line

If an SPA holds the access token itself, a single compromised script running on that page lets the token leak out. A BFF (Backend for Frontend) keeps the token only on the server and gives the browser only an HttpOnly session cookie. It is a design that leaves no token in the browser to steal.

Why this was needed

For a while the standard setup for an SPA was like this. The browser gets an access token from the issuer, puts it in localStorage, and attaches it as Authorization: Bearer every time it calls the API. It is convenient because the server is stateless. The problem is that the party that can read that token is "every script running on this page". An ad tag, an analytics script, a compromised npm package, a single line of XSS — anything can read the token and send it to the attacker's server, and that token stays valid in the attacker's hands until it expires. Closing the tab does not help.

The IETF refined this problem for a long time and published it as RFC 10017 OAuth 2.0 for Browser-Based Applications (draft name draft-ietf-oauth-browser-based-apps). After comparing several architectures, it puts the BFF in section 6.1. The core is one sentence — the token exists only in the BFF, so there is no token to take out of the browser.

How it works

A BFF is a small server that stands at the same origin as the frontend. This server takes over the OAuth client role entirely.

브라우저 ── 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

The rules to follow are gathered in section 6.1.3.

What it looks like in the field

After you add a BFF, the most common accident is the token leaking again "for convenience". The session check response also carries access_token, or the frontend wants to call the downstream API directly and you hand down the token once more as a cookie that JS can read. At that moment the BFF is a BFF in name only. There is the opposite direction too. If the downstream API accepts a request with no token "because it is the internal network", a call that skips the BFF goes straight through.

You also have to use it knowing its limits. Section 6.1.4.1 notes that client hijacking, where a malicious script sends requests to the BFF through the user's browser, cannot be blocked even by a BFF. What a BFF removes is token theft, not XSS itself. That the attack also ends when the user closes the tab is the difference, and that difference is the value of a BFF.

What you will do in the next lab

You bring up the issuer (8312), the downstream API (8311), and the BFF (8310) in one Pod, play the browser role with curl, and walk through the login flow. You check the attributes of the cookie the BFF handed down, pull the real token out of the BFF session in Redis, and confirm that that value is nowhere in the responses the browser saw. Then you prove with requests that a call through the BFF is 200 and a direct downstream call is 401, that a write without a CSRF token is blocked, and that after logout the old token dies downstream.