Aller au contenu
Cette page n'a pas encore été traduite.

If the Provider Does Everything Right, Do You Still Need a Backend?

Every year the browser gets a little safer to keep credentials in. Proof of possession has been an RFC for three years. Cookies are starting to be bound to devices. Revocation is arriving as an event rather than a poll. Each of those closes something a Backend-for-Frontend was built to close, which makes the question fair: assemble all of it, and is the backend still doing anything?

I argued in a previous article that a capable provider removes most reasons to keep a server-side session inside a BFF. This is the same question pointed at the pattern itself, and it deserves the same treatment rather than a reflex.

Disclosure: at Abblix we build an OpenID Connect server library, so the stack below is the category we sell into. This piece argues for buying more of it in one situation and less of it in another, and both are marked where they fall.

The strongest stack you can build

Give the provider side everything it has. Access and refresh tokens sender-constrained to a key the page cannot export. A server-chosen nonce in every proof, without which the page can mint a batch of proofs now and ship them out with a token, to be replayed after the tab is closed. The provider's own SSO cookie bound to the device, so the thing that makes silent re-issue possible cannot be lifted off the machine. Access tokens short enough that expiry does the revoking. Revocation pushed to resource servers rather than polled, which Shared Signals gives a standard shape, carrying a CAEP session-revoked event. One audience per resource.

One control belongs above all of those and is not on the list, because it is not something a provider sells. The specification's answer to the attack this article is about is "generally preventing XSS", so a Content Security Policy strict enough to matter, Trusted Types in enforcement rather than report-only, and a build that treats every third-party script tag as a credential holder do more for the scenario below than anything a token mechanism can. An article written by a provider vendor will tend to leave that out. It is first.

Two other candidates are left out on purpose. A gateway that terminates tokens, or an edge doing token exchange, is a server of yours under another name, which concedes the argument rather than answering it. And the browser-side candidate has already been ruled on: the specification walks through running the OAuth flow in a service worker, grants that it keeps tokens out of the page and "essentially neutralizes the first two attack scenarios", then shows that malicious code can unregister it and run its own flow in a fresh browsing context. On that shortcoming, "combined with the significant complexity of registering and maintaining a Service Worker", the RFC concludes that "this pattern is not recommended".

What the stack closes

Theft, which is what it was built for, and the documents are precise about how far.

RFC 9449 on the tokens: with a non-extractable key, DPoP "renders exfiltrated tokens alone unusable". The word doing the work is "alone". A copied token is a dead artifact without a signature over the request it travels with.

How far that reaches depends on where the key actually lives, and RFC 10017, the current best practice for browser-based applications, is careful about it. There is "no guarantee made in [W3C.WebCryptoAPI] that a non-exportable key is actually protected by a Trusted Platform Module (TPM) or stored in an encrypted form on disk", so lifting it off the filesystem "may still be possible if the attacker can get access to the filesystem of the user's machine, for example, via malware". The RFC's verdict on that case: it "effectively makes the potential attack vector equivalent to a session hijacking attack".

Device Bound Session Credentials does the same job one layer over, for the cookie rather than for the signing key: the private key stays where the operating system will not hand it out, "even by equally privileged malware". It does not reach the key a page signs proofs with, so the gap above stays open where the platform leaves it open. It is also an editors' draft whose status note asks not to be cited as more than work in progress. Plan with it. Do not design this quarter around it.

The attacker who steals nothing

Now the case both documents take care to describe, because it is the one their measures do not reach.

DPoP: code running in the page "has access to utilize the signing key; thus, it can produce DPoP proofs to use in conjunction with the token", and "at this application layer, there is most likely no feasible defense against this threat except generally preventing XSS", which puts it outside what DPoP addresses. The cookie draft never mentions an in-page attacker at all, and does not have to, because a device-bound cookie is still attached by the browser to the requests the page makes back to its own origin. What it does rule out is next door: DBSC "will not prevent temporary access to the browser session while the attacker is resident on the user's device", since "the signing capability will likely still be available for any program running as the user".

A BFF does not close this either. Injected code rides the session cookie and makes requests as the user. Two things differ, and the smaller one is reach: behind a BFF the attacker is confined to the endpoints you proxy, which for a BFF that forwards everything under one path is most of what the application can do anyway, so the honest version of this gain is the rate limiting and logging you actually put on that proxy. The RFC makes the same point as an opportunity rather than a property: a BFF "is uniquely placed to observe all traffic between the browser-based application and the resource servers", and if you want anomaly detection or rate limiting, that is the place.

The larger difference is duration. A riding attacker never holds a refresh token, and RFC 9449 is exact about what he can do instead: he can create new proofs and use them "either on the victim's device or on a device under the attacker's control", but only "as long as the client is online". He is tied to a live session. A browser client holding its own refresh token hands him something that outlives the tab.

The scenario that separates them

Then there is the attacker who stops stealing and starts asking.

Code in your origin can run its own authorization flow in a frame, read the authorization code out of that same-origin frame, and exchange it. The result is not a ride on your session. It is a fresh, independent set of tokens. The specification is blunt about the frontend's options:

There are no practical security mechanisms for frontend applications that counter this attack scenario. Short access token lifetimes and refresh token rotation are ineffective, since the attacker has a fresh, independent set of tokens. Advanced security mechanism, such as DPoP [RFC9449], are equally ineffective, since the attacker can use their own key pair to set up and use DPoP for the newly obtained tokens. Requiring user interaction with every Authorization Code flow would effectively stop the automatic silent issuance of new tokens, but this would significantly impact widely established patterns, such as bootstrapping an application on its first page load or single sign-on across multiple related applications, and is not a practical measure.

Short lifetimes and proof of possession are two of the six items in the stack above, ruled out by name for this one case, and rotation with them. The tokens the attacker walks away with carry his key and need nothing from that browser ever again.

The last sentence of that quote is the one a provider vendor has to answer, because it names a measure on our side of the line. The specification gives the provider side a section of its own and opens it by saying that "none of these defenses is recommended, as they do not offer practically usable security benefits". Blocking authorization requests that originate in an iframe "would not work for slight variations of the attack scenario. For example, the attacker can launch the silent flow in a pop-up window or a pop-under window." Making consent mandatory in every flow produces "consent fatigue", the RFC says, "which makes it likely that the user will blindly approve the consent, even when it is associated with a flow that was initiated by the attacker".

So the document that spends nine sections on what an authorization server should do says, about this scenario, that the authorization server has nothing usable to offer. Detection is still worth wiring, since a second authorization for the same client from the same live session carrying a different key is visible to us and can be rate limited or pushed as a risk event, but that arrives after the fact and the attacker who waits an hour looks like anyone else.

What the same document does credit is the client being confidential. In the BFF's own section it lists this scenario as mitigated, "by making the BFF a confidential client. Even when the attacker manages to obtain an authorization code, they are prevented from exchanging this code due to the lack of client credentials." The attacker's flow ends where authentication is required.

Note what that sentence credits, because it is narrower than the pattern. The RFC says the same thing, in the same words, about the token-mediating backend, the middle architecture that hands the access token to the browser. What closes this scenario is a confidential client on your side of the network, not the BFF specifically. The BFF's extra credit is the other two scenarios: with no tokens in the browser, "there are simply no tokens to be stolen".

Check that the sentence is true of your deployment

The RFC states it as a property of the architecture. Four ordinary deployments break it, and none of them looks broken on a diagram.

A BFF registered without a client secret is a public client wearing a backend's name, and the attacker runs its flow himself. A second client at the same provider whose redirect URI resolves into your origin gives him a flow to run even when yours is confidential, and a wildcard redirect registration or open dynamic client registration hands him one on request. A BFF endpoint that passes an access token to the page for a direct API call restores the browser-client case for that audience. And a client secret shared across environments and never rotated lives in more places than your backend.

So the property to verify is not "my BFF is a confidential client". It is: no client at my provider can obtain tokens through a redirect into an origin my frontend code runs in without authenticating. List the clients whose redirect URIs resolve to your frontend's origin, and check that every one of them authenticates at the token endpoint.

What was never a security question

The rest of what a backend does is not about attacks, which is why no provider feature is coming for it.

Serving a logged-in user while the provider is unreachable, for as long as the tokens it holds stay valid. This one is narrower than it sounds, and the previous article named the condition: it takes a server-side session holding the user's state. A BFF that keeps its session in the user's cookie refreshes against the provider like everyone else and goes down with it.

Composing several APIs into one answer, so the browser never learns their addresses, and deciding your own rules in one place. Providers do sell policy engines, so read that the way you would read any capability question: what a policy engine still needs from you is the application state your rules are about, and a decision point on a request path the browser cannot skip.

And an audit of what happened inside your application. The provider hands you the key, the session identifier its tokens already carry. The record is yours to keep.

The answer

Two requirements come out of this, and they are not the same size.

Stopping an attacker in your origin from obtaining tokens of his own needs a specific thing: an OAuth client he cannot impersonate, which in a browser deployment means a confidential client on your side. A BFF is one. So is the token-mediating backend, at the cost of putting an access token back in the browser. That requirement is the one the specification settles, and it is why it closes the BFF's section by calling the architecture "strongly recommended for business applications, sensitive applications, and applications that handle personal data".

Keeping service through a provider outage, and deciding rules that depend on your data, need something more ordinary: a component of yours on the request path. A reverse proxy answers those without ever becoming the OAuth client.

Now the two marks promised at the top. Running with our interest: if you have no backend at all, everything in the first section is a reason to buy a better provider, and this article is an argument for one. Running against it: behind a confidential BFF, most of that stack is optional for you. Proof of possession protects a token held in a browser, and yours is not in one; rotation is something I argued against for this client class in the previous article; per-resource audiences are an API design decision. What still earns its place is device binding on the provider's own SSO cookie and revocation you are told about rather than poll, and in our own server that second one is still wiring the host does rather than something the library does on its own. That is two items out of six, and we sell all six.

If you have no backend today

The distance is shorter than the argument makes it sound. If anything of yours already serves the application's files, that process can hold the tokens: cookie authentication on it, the OpenID Connect handler configured as a confidential client, and a proxy route for the calls the page makes. The frontend change is deleting the token code and sending credentials with the request instead.

If nothing of yours serves the page, because it is a static site or a widget inside somebody else's document, then the stack in the first section is what you have, and the first thing to spend on is not a token mechanism. It is the content security policy that makes the scenario in the middle of this article need a different starting point.