Beta

Create a tenant

A new tenant starts with its own users, OAuth settings, audit history and logs. You are its first Tenant Admin.

BTL Admin

Backends for frontends

The editor's team takes that guidance seriously but does not want to rebuild the editor. The interface, the files, and the address stay the same. What changes is underneath: a small server at editor.example that takes over every OAuth responsibility the JavaScript used to carry.

This design is called a backend for frontend, often shortened to BFF: a server-side component that belongs to one frontend application, acts as its OAuth client, and makes API calls for it. From the photo service's point of view, the editor now looks much like the printer's website.

A backend that holds the tokens

The backend can keep a credential on a server, so it is registered as a confidential client, photo-editor-backend, separate from the browser-only photo-editor. When you select Open from photo account, the page navigates to the backend's /login address. The backend creates the pending attempt and redirects your browser to the photo service, and the callback returns to the backend at https://editor.example/oauth/callback. The backend exchanges the code with its own client authentication and keeps the tokens in a server-side session record.

The browser receives only a session cookie. From then on, the editor's JavaScript calls the backend instead of the photo API. All domains, tokens, and keys in these examples are fictional.

GET /api/albums/42/photos HTTP/1.1
Host: editor.example
Cookie: __Host-editor-session=demo-session-5
Editor-Request: 1

The backend finds the session from the cookie, takes the access token stored with it, and forwards the request:

GET /albums/42/photos HTTP/1.1
Host: api.photos.example
Authorization: Bearer demo-editor-access-token-2

When the access token has expired, the backend refreshes it first, authenticating as a confidential client, and the page never notices.

A backend that forwards requests needs firm rules about where they go. The editor's backend maps each of its routes to one fixed destination, such as /api/albums/{id}/photos to https://api.photos.example/albums/{id}/photos, and checks that the value it places in the path is only an album identifier. A backend that forwarded to an address taken from the request would attach your access token to a request for whoever supplied that address.

The editor page navigates to its backend's login address. The backend creates a pending attempt with state and a PKCE verifier and redirects the browser to the photo service's authorization endpoint, where you sign in and approve. The browser returns the code to the backend's callback. The backend sends the code, the verifier and its client authentication to the token endpoint and receives an access token and a refresh token, which it keeps in a server-side session. It sets an HttpOnly session cookie in the browser. Later the editor page calls the backend with the cookie and a custom header, and the backend forwards the request to the photo API with the access token. No token reaches the browser. The editor page navigates to its backend's login address. The backend creates a pending attempt with state and a PKCE verifier and redirects the browser to the photo service's authorization endpoint, where you sign in and approve. The browser returns the code to the backend's callback. The backend sends the code, the verifier and its client authentication to the token endpoint and receives an access token and a refresh token, which it keeps in a server-side session. It sets an HttpOnly session cookie in the browser. Later the editor page calls the backend with the cookie and a custom header, and the backend forwards the request to the photo API with the access token. No token reaches the browser.
The editor's backend is the OAuth client. The browser carries the authorization request and the code, then holds only a session cookie. Tokens travel only between the backend, the authorization server, and the photo API.

Three designs side by side

A third design sits between the browser-only editor and this one. A token-mediating backend also runs the authorization code flow as a confidential client and keeps the refresh token, but instead of forwarding API calls, it hands access tokens to the page, which calls the photo API directly. That spares the backend from carrying every request, at the cost of putting access tokens back in the browser.

DesignWhat reaches the browserWhat injected script can carry away
Browser-only clientAccess and refresh tokensThe existing tokens, and new ones from an authorization it starts itself
Token-mediating backendAccess tokens and a session cookieAccess tokens the page holds or asks the backend for
Backend for frontendA session cookie onlyNo tokens

The costs run the other way. The browser-only editor needs only static hosting. A token-mediating backend adds a server for sign-in and refresh. A backend for frontend adds a server that every API call passes through, which has to be operated and kept secure, and which sees all the data flowing between the editor and the photo API. Current guidance strongly recommends the backend for frontend for business applications, sensitive applications, and applications that handle personal data, and a token-mediating backend only where proxying every call is impractical.

What injected script can still do

Moving the tokens does not make cross-site scripting harmless. Suppose the caption bug from Browser applications is still there, and the attacker's script runs in the editor's page as before. What changes is what it can reach.

It can still make requests. The browser attaches the session cookie to the page's calls, and the backend cannot tell the attacker's request from the editor's. While you have the editor open, the script can read your photos and create albums through the backend, with exactly the access the editor has.

It cannot carry away anything that keeps working. The cookie is HttpOnly, so the script cannot read the session identifier. There is no access token or refresh token in the page to copy. And the fresh-authorization trick from Browser applications fails: even if the script sees a code arrive at the callback, redeeming it requires the backend's client credentials and the verifier the backend kept, neither of which ever reaches the browser. When you close the tab, the attacker's reach ends with it.

A compromised page is still serious, but the damage is limited to what can be done through the page while it is open, not whatever a stolen refresh token allows for hours from anywhere. And because every request passes through the backend, it is a natural place to notice and limit unusual patterns of calls.

Protecting the backend's endpoints

The backend now accepts requests authenticated by a cookie, which exposes it to the attack the Correlating requests and responses lesson described at the printer's callback: cross-site request forgery, or CSRF, in which another site causes your browser to send a request that carries your cookies. Cookies were designed to be attached according to where a request is going, not which page caused it. A page on another site could contain a hidden form that posts to https://editor.example/api/albums. If your browser attached the editor's session cookie, the backend would create an album in your photo account.

The editor's backend sets its session cookie like this:

Set-Cookie: __Host-editor-session=demo-session-5; Path=/; Secure; HttpOnly; SameSite=Strict

SameSite=Strict tells the browser to send the cookie only with requests that start from the editor's own site, so the forged form arrives without it. The __Host- prefix, which the printer's website also used, keeps the cookie to editor.example alone. Just as important, it means no other host, including a subdomain such as blog.editor.example, can set a cookie by that name that editor.example would receive. A careless or compromised neighbor cannot plant a session identifier the attacker already knows, the session fixation described in Staying signed in.

Current guidance for this design also recommends a name prefix showing that the server, not a script, set the cookie. The proposed __Host-Http- prefix would do that by adding HttpOnly to the __Host- requirements, and because such a name still begins with __Host-, browsers that do not recognize it still apply the __Host- rules.

One cookie needs different treatment. The callback from the photo service is a navigation that starts on the photo service's site, so a SameSite=Strict cookie would not arrive with it. The short-lived cookie that ties the callback to its pending attempt therefore uses SameSite=Lax, and the backend checks the response with state and PKCE as usual.

SameSite also has a gap. It compares sites, and a site here means the registrable domain, so every subdomain of editor.example counts as the same site. If an attacker took over a forgotten subdomain, its pages could send requests that the browser treats as same-site. The backend closes that gap by requiring a header on every API request: the Editor-Request: 1 line in the page's request to the backend shown earlier. A form cannot add a custom header. A script on another origin that tries to add one triggers a CORS preflight: the browser first asks the backend whether that origin may send such a request, and sends the real request only if the backend approves. The editor's backend approves no other origin, and it rejects any request that arrives without the header.

The editor now has the security properties of the printer's website: a confidential client on a server, tokens that never reach the browser, and a cookie that other sites cannot make your browser send. It keeps what made it a browser application, an interface that runs in the page, and pays for the difference with a server that every API call passes through.

Try it in the Lab

PUT IT INTO PRACTICE

Check your understanding

Try these questions before moving on. If an answer isn't right, use the feedback and try again.

0 of 2 answered correctly

Enable JavaScript to answer these questions and save progress in this browser.

QUESTION 1 OF 2The editor now uses a backend for frontend, and injected script is running in its page. Which of these can the script do?

QUESTION 2 OF 2The backend's session cookie is SameSite=Strict. Why does it also require a custom header on every API request?

We value your privacy

We use cookies and similar technologies to enhance your browsing experience, and analytics to understand our traffic. By clicking "Allow All", you consent to optional analytics. Cookie Policy

Learn identity