Skip to content
Abblix

The BFF Pattern in OAuth 2.0 and OpenID Connect

Backend-For-Frontend is the architecture in which a server-side component, not the browser, is the OAuth client. It runs the Authorization Code Flow, keeps the access token and refresh token, and hands the browser nothing but a session cookie. Every call the frontend makes to a protected API goes through it.

The pattern exists because of one fact that no browser-side mechanism changes: whatever the legitimate frontend code can reach, injected code running in the same origin can reach too. Moving the tokens out of that origin is the only measure that removes them from the attacker's reach rather than making them harder to use.

TL;DR

OAuth 2.0 for Browser-Based Applications names three architectures for browser applications and ranks them by security. BFF is the most secure of the three, and it gives the backend three jobs:

  • Be the confidential client towards the authorization server.
  • Hold the tokens inside a cookie-based session, so none of them reach the browser.
  • Proxy the frontend's API calls, attaching the access token server-side.

The browser keeps a session cookie and nothing else. An attacker with script execution in the page can still act as the user through that cookie, but cannot take a token away, cannot use one from another machine, and cannot reach an API the backend does not expose.

What counts as a BFF

The name is overloaded in general architecture writing, where a BFF often means any per-client API aggregation layer. In OAuth the term is narrower, and the distinguishing property is not proxying but client identity: the backend becomes the OAuth client for the frontend application. An API gateway that forwards a token the browser supplied is not a BFF, however much proxying it does.

Three responsibilities follow from that, as set out in section 6.1 of the browser-based apps document:

  • It authenticates to the authorization server as a confidential client, with credentials the browser never sees.
  • It manages access and refresh tokens within a cookie-based session, so no token is exposed to the browser application.
  • It forwards frontend requests to the resource server, adding the correct access token on the way.

The three architectures, ranked

The specification presents the options in decreasing order of security, which is the useful way to read them.

Where the tokens liveWho calls the APIWhat script injection gets
BFFbackend sessionthe backend, on the frontend's behalfrequests through the user's browser only
Token-mediating backendbackend issues them to the browserthe frontend, directlythe access token, usable anywhere until it expires
Browser-based OAuth clientbrowserthe frontend, directlyaccess and refresh tokens, and the ability to run its own flow

The middle option is worth naming because it looks like a BFF from the outside. A token-mediating backend also runs the flow as a confidential client, which does protect the refresh token, and it is genuinely lighter than a BFF because it does not proxy every call. But it hands the access token to the browser, and from the moment it does, that token is exfiltratable and usable from anywhere. The choice between the two is a real trade-off, not a mistake in one direction.

The third option is the plain SPA-as-client design. It is what PKCE was introduced to make safe, and PKCE does make the code exchange safe. What it cannot do is protect a token after the exchange, because the token is then sitting in an environment the attacker shares.

How a login runs

The sequence differs from a plain SPA login in one structural way: navigation, not fetch, drives the parts that involve the authorization server.

  • The frontend asks the backend whether a session exists.
  • With no session, the frontend navigates the browser to the backend's login endpoint. It does not construct the authorization request itself.
  • The backend redirects the browser to the authorization server, where the user authenticates.
  • The authorization server redirects back to the backend's callback endpoint, again by navigation, at a point where the frontend is not even loaded.
  • The backend exchanges the code for tokens, stores them against a new session, sets the session cookie, and redirects the browser to the application.
  • The frontend loads, asks about the session again, and this time gets an authenticated answer.

The redirect at the end matters more than it looks: it puts the application URL in the address bar without the authorization response attached, so the code never enters the browser history.

The endpoints a BFF exposes

Four, and standard OAuth libraries for server-side confidential clients already implement three of them.

  • Check session. Called by the frontend with the session cookie. Answers whether a session is active, and usually returns the identity information the UI needs, so the frontend does not have to parse an ID token it should never receive.
  • Login. Reached by browser navigation, not by fetch. Responds with a redirect to the authorization server. A fetch here fails on the cross-origin redirect, which is the most common first mistake when building one of these.
  • Callback. Receives the authorization code by navigation, exchanges it, establishes the session, redirects to the application.
  • Logout. Behaviour depends on the application: ending the local session is the minimum, and RP-initiated logout at the provider is the usual addition.

Objections worth answering

The pattern attracts the same handful of counterarguments, and each has a precise answer rather than a dismissive one.

Doesn't refresh token rotation solve token theft?

Rotation defends against an attacker who stole a token and is using it from somewhere else: the stolen copy gets replayed, the server sees the reuse, and the family is revoked. An attacker with script execution inside the origin is not somewhere else. That code can watch every refresh exchange and keep the newest token for itself, so the legitimate application never presents a stale one and the reuse detector never fires. Rotation remains worth having, and it does not address tokens in a compromised frontend.

Doesn't DPoP solve it?

DPoP binds a token to a key, and in a browser that key can be made non-extractable through the Web Crypto API, so injected code cannot copy it. It does not need to. The same code can call the same Web Crypto API, sign valid proofs, and use the tokens from inside the browser. DPoP is a real defence against exfiltration and replay from another machine; it does not move the trust boundary, which is what BFF does.

This does keep tokens out of JavaScript's reach, and it misses a second property: scope of authority. An access token in a cookie is still sent with every request, and if it is accepted by several APIs, a compromised frontend can proxy requests to all of them. With a BFF the browser holds a session cookie scoped to one backend, and the backend decides which token goes to which API.

True, and it is the honest limit of the pattern. BFF does not stop XSS. What it removes is token theft and the escalation that follows: injected code cannot extract a token, cannot use one from another machine, cannot survive the session, and cannot reach an API the backend does not expose. The remaining attack surface is the backend's own API, which is under the application's control and can carry rate limiting, anomaly detection and request validation that a frontend cannot enforce.

Doesn't this violate the OAuth model?

It changes who the client is, deliberately. In the classic model the access token is issued to the application the user is looking at; here the backend is the client and the browser is a session-authenticated consumer of that backend's API. The browser-based apps document recommends exactly this, on the reasoning that a browser is a hostile place to keep credentials. Treating a backend as the confidential client it actually is applies the OAuth model rather than bending it.

Isn't CORS enough to stop exfiltration?

A strict CORS policy stops fetch and XMLHttpRequest from delivering stolen data to a third-party origin. It does not stop a hidden form submission, navigator.sendBeacon, or an image tag whose URL carries the data in a query string. CORS is a layer, not the layer.

Where PKCE fits

The flow a BFF runs is still an Authorization Code Flow, and it still passes a code through the browser. Proof Key for Code Exchange protects that leg, and the security BCP recommends it for confidential clients too, so a BFF should send code_challenge even though it holds a client secret. The two mechanisms cover different halves of the problem: PKCE makes an intercepted code useless, and BFF makes sure that what the interception could have reached is not a token in the first place.

Building one

The pattern is deliberately described here without a framework, because the design questions are the same everywhere: which endpoints exist, what the cookie holds, where the proxy sits.