Unfortunately, history shows us that frontend web applications are hard to secure. XSS has plagued web applications for almost two decades now. While modern JavaScript frameworks make it a bit better, there are still plenty of potential XSS attack vectors in modern applications. These cheat sheets provide more details on securing React and Angular applications.
So, it’s game over then? Well, yes and no.
From a security perspective, it is virtually impossible to secure tokens in a frontend web application. Malicious JavaScript code can do anything the application can do, so if the application can access tokens, so can the malicious code. Security patterns, such as hiding refresh tokens in a Web Worker, can help but are not a definitive solution to address the scenarios we discussed before.
Concretely, non-sensitive frontend applications can rely on refresh tokens with refresh token rotation. For example, an application handling restaurant reviews is not considered sensitive, which reduces the impact of a successful XSS attack. For sensitive applications, this advice does not hold. For example, applications handling personal information, healthcare data or financial operations are extremely sensitive. In such applications, a successful XSS attack has a significant impact.
That's why sensitive frontend applications should avoid handling tokens in the browser. Instead, they can rely on a Backend for Frontend (BFF) pattern, where token handling is deferred to a minimalistic server-side component. The image below illustrates the concept of a BFF:
BFFs are traditionally used to aggregate various APIs into a single coherent API to serve a client application. In our scenario, the BFF also assumes a minimal security role. It accepts requests from the client application, augments them with OAuth 2.0 access tokens, and forwards the request to the API. Similarly, any response from the API is forwarded to the client application.
So, how does a BFF work in practice?
The details of a BFF
In this scenario, the BFF becomes the OAuth 2.0 client application. Since the BFF runs on a backend system, it can be configured as a confidential client. The BFF is the client, so it initializes the OAuth 2.0 flow that runs in the user's browser. After the first step of the flow, the BFF receives an authorization code, which it exchanges for tokens with the authorization server using client authentication. With the access token, the BFF can forward API requests. With the refresh token, the BFF can obtain a new access token when necessary. Note that using the refresh token requires the BFF to authenticate to the authorization server.
A BFF is shared among hundreds or even thousands of client instances, each operating on behalf of a different user. The BFF keeps track of these users with a cookie-based session. The BFF can keep that session on the server (e.g., in a simple memory store) but can also push it to the client (e.g., in an encrypted session object). The former results in a stateful BFF, while the latter allows the BFF to become stateless. Both approaches are valid.
Note that the BFF needs to follow cookie security best practices to guarantee the security of the cookie. Concretely, this means that to set a cookie with the name “MyBFFCookie,” the following header has to be used: Set-Cookie: __Host-MyBFFCookie=…; Secure; HttpOnly; SameSite. Get more details on cookie security.
When the client sends a request, the BFF uses the session information in the request to retrieve the user's tokens. If the access token is still valid, the BFF can directly forward the request. If the access token has expired, the BFF uses the user's refresh token to obtain a fresh access token before forwarding the request.
Finally, note that from the perspective of the user, nothing changes. The user experience between a frontend client application and a frontend application backed by a BFF is identical.
The benefits of a BFF
A backend-for-frontend offers significant security benefits over browser-based applications. The BFF acts as the OAuth 2.0 client, allowing it to apply security best practices for confidential clients. Concretely, this means that:
The BFF is required to authenticate to the authorization server when exchanging an authorization code or refresh token.
The BFF can rely on robust key-based authentication mechanisms (e.g., mTLS).
The BFF can use sender-constrained access tokens and sender-constrained refresh tokens.
Access tokens and refresh tokens are never exposed to the browser.
But what about potentially malicious JavaScript code running in the frontend application? In this scenario, the malicious code can no longer access the tokens since they are only available to the BFF. Cookie security measures (i.e., the HttpOnly attribute) prevent the malicious code from stealing the session with the BFF.
The malicious code can still modify the behavior of the client application. Concretely, the attacker can perform a session riding attack by sending malicious API calls through the BFF. Such API calls are indistinguishable from legitimate requests sent by the client.
However, the BFF is in full control here. Consequently, the BFF can limit the API surface by preventing the client from accessing certain endpoints. Additionally, the BFF can apply traffic analysis patterns to detect suspicious behavior. Examples include detecting a suspiciously large number of operations or observing sensitive operations in an unexpected order.
Finally, keep in mind that the BFF does nothing to stop the malicious code from executing. The attacker can still extract sensitive information or perform social engineering attacks on the user. The only way to prevent such attacks is by following strict secure coding guidelines for the frontend application.