The Backend for Frontend Pattern for Single Page Applications

The Backend for Frontend Pattern for Single Page Applications

architect

The Evolution of Web Applications

The web is constantly evolving in every aspect. From content to technologies and architectures, modern web applications continue to change. The monolithic web application (a single backend serving the HTML-based user interfaces) used to be a prevalent style. Lately, this monolithic approach has been replaced by microservices on the backends and Single Page Applications (SPAs) on the frontend. This change has caused a significant shift in the way security is handled.

In a traditional approach, a user’s access to a web application was controlled using a cookie-based session.

Cookie-based session in a web application

In this setup, the frontend client uses cookies to authorize a user during calls to the backend. The cookie is stored by the browser and sent to the backend with every request so that authorization decisions can be performed based on the session data kept in storage. With this approach, it doesn’t matter whether the application is a monolith or the backend is driven by microservices.

In contrast, an SPA has no dedicated backend at all. The SPA code itself is often served by a Content Delivery Network (CDN) through static files (JavaScript code, stylesheets, images, etc.). The app calls APIs, which return relevant data. As there is no dedicated backend for the SPA, the user’s session cannot be kept in a cookie. Instead, access tokens are used to enable the SPA to call APIs on behalf of the authenticated user. OAuth2, OpenID Connect, and JWT are popular standards used for this purpose, though proprietary solutions are also utilized.

SPA calling APIs with access tokens

The Security Issues of an SPA

The shift from handling authorization with cookies to access tokens has some severe security implications. The frontend code, which runs in an insecure environment (the user’s browser), requires access tokens to call APIs. Very often, the SPA will also possess a token that grants offline access to a user’s resources. This could be a refresh token that can obtain new access tokens without interaction from the user. As these credentials are readable by the Single Page App, they are vulnerable to Cross-Site Scripting attacks (XSS).

Any malicious code that manages to run in the context of the SPA will potentially be able to read the access and refresh tokens (This could be done by loading them directly from browser storage or intercepting them when sent in HTTP requests). The attacker will be able to perform any action the token permits. In the case of accessing APIs, this could potentially give an attacker access to functionalities not normally accessible through the UI, especially when over-privileged tokens are issued to the SPA. The attacker can also send the tokens to an external application and perform actions even when the user closes their application. If the attacker manages to extract a refresh token in this way, they will be able to access the victim’s data for as long as that refresh token remains valid (which can be days in some setups).

The above examples show how dangerous an XSS attack is for an SPA. This is because, unfortunately, current browsers offer no way for an SPA to securely store tokens. Developers should thus always make XSS mitigation a priority.

Another looming issue for SPAs is the impending end of support for third-party cookies by the major browsers. This means that the User Experience of obtaining tokens from Authorization Servers or OpenID Connect Providers can be hampered. Keeping refresh tokens in cookies (as is sometimes implemented by SPAs) can also become a problem should the Token Service operate on a different domain than the SPA.

Web applications that rely on cookies to gain access to protected resources are in some ways better protected from the previously described attacks. Yet, they are not 100% bulletproof. Session identifiers stored in HTTP-only cookies cannot be accessed by any script run in the browser. This means that an XSS attack cannot read a cookie to steal the session identifier and use it straight from the attacker’s app to access the user’s resources.

On the other hand, such an attack can perform calls to the website’s backend, relying on the browser to automatically add a session cookie to such calls. The attacker will still be able to access the user’s data, but will be limited to the lifetime of the user’s session (no offline access is possible in such a scenario). Websites that rely on cookies are also subject to Cross-Site Request Forgery (CSRF) and need to be additionally protected from this attack vector. SPAs are immune to CSRF, as requests to APIs rely on headers added by scripts, not by credentials appended automatically by the browser.

Modern browsers offer new ways of further protecting applications. It’s now possible to limit cookies used by the application only to secure HTTP traffic (thus inaccessible to scripts or insecure traffic). It’s also possible to limit requests to those originating from the same domain as the website. This can be achieved by using the SameSite cookie attribute set to strict. This setting can increase application security but may cause UX issues. For example, a user clicking a link to a website from a genuine email will not be recognized as authenticated, as the request is coming from outside the website’s origin.

Proper setting of CORS headers in your application can further inhibit CSRF attacks. Setting strict limitations in Content Security Policy headers will block malicious code from sending requests outside your app (e.g., to steal refresh tokens). Unfortunately, users can disable CSP protections in browsers, either explicitly or by installing dubious plugins. Developers should be aware of this when designing application security.

Currently, SPAs have no means of keeping access and refresh tokens secure from malicious code. Even if developers attempt to protect their apps from XSS attacks (as they should), such an attack can still occur through a vulnerability in a third-party library. The only way to protect tokens from being accessed by any malicious code is to keep them away from the browser. This can be achieved by adding a lightweight backend component to handle tokens and issue secure cookies to the frontend — a Backend-For-Frontend (BFF) component.

Backend for Frontend session handling

Using this approach, all communication from the SPA to the Authorization Server will now go through the BFF, and tokens will not reach the SPA at all. The BFF now issues session cookies to the SPA. Even though we’re dealing with an SPA, the security level is on par with a website with a backend. The cookies are then used during requests to APIs and are exchanged for an access token, either by the BFF or a dedicated plugin in an API gateway.

As the BFF code is not available in the browser, the BFF can be a confidential client, further increasing the security of token issuance. The client will now use some form of authentication (client secret, Mutual TLS, etc.) to retrieve tokens from the Authorization Server.

Important Features of a BFF

It Is Still an SPA?

The BFF pattern provides an SPA a level of security similar to regular web apps, but it still is an SPA. This means that nothing in terms of app deployment must change — it can still be delivered through a CDN. The app does not need a cumbersome backend to process all business features of the app, just a lightweight component capable of dealing with security. The SPA itself will usually need very few changes to start talking to the Authorization Server through a BFF.

Stateful vs. Stateless

The BFF implementation can either be stateful or stateless. A stateful implementation would have access to some kind of persistence mechanism (e.g., a database) where tokens from the Authorization Server would be stored. The BFF would then issue cookies with a session ID and read tokens from the store.

On the other hand, a stateless BFF implementation does not need any additional persistence layer. Tokens are encrypted by the BFF and placed in session cookies. As those cookies are HTTP-only, no JavaScript has access to these values. Even if they were read, the tokens are encrypted, so the encryption key is needed to get the actual token. A stateless implementation retains the same level of security as a stateful one. Still, it does not require an additional storage component (which has to be synchronized among all instances, which can additionally complicate the solution). In a stateless implementation, though, cookies sent to the browser are substantially larger, as they contain the full encrypted value of tokens. Such cookies can get especially large when JWTs are used for tokens (e.g., ID tokens).

Different Deployment Options

A BFF component can be deployed in different ways. It will often be deployed as any other backend service; instances with the deployed code will be spawned in data centers and exposed through a load balancer or gateway.

Since BFF is intended to be lightweight, serverless functions are another valid deployment option. Many CDNs offer static content delivery and the ability to run serverless functions on their infrastructure (take Cloudflare workers, for example). This means that a BFF component, in some cases, can be run without the need for any additional infrastructure maintenance.

Whatever the deployment option, there is one thing companies should consider — the BFF component should be accessible on the same domain as the SPA, a sibling domain (e.g., www.example.com for the SPA, and bff.example.com for the BFF), or a child domain. This enables the BFF to issue cookies with the SameSite=strict parameter, which is recommended to maintain a high level of security.

Login Usability

When using a BFF, the SPA only actually needs to send the cookie in API requests and not during requests for web content. This means that navigation from email links does not cause any usability problems. In addition, the SPA is in control of when the actual OAuth redirects occur, and can write custom code before and after such redirects (e.g., to save and load application state).

Hiding Complexity of Authorization Flows

The BFF is responsible for all communication with the Authorization Server. This hides the complexity of authorization flows from the SPA. The BFF can expose a simple API to the SPA, and the SPA does not need to know the details of implemented flows. This allows the BFF to choose different strategies for sensitive scenarios. For example, the BFF can implement Pushed Authorization Requests, JWT Authorization Requests, or JWT Security Authorization Response Mode, and the complexity of these can be left out from the SPA.

Curity’s Implementation

Even though a BFF is a lightweight component, it can get tricky to implement everything correctly. This is why we at Curity have created a sample implementation of a BFF as a reference for this pattern. Two projects were developed: the BFF itself, written in Node.JS using the Express framework, and a plugin for the Kong API Gateway, which can handle cookies issued by the BFF. These components demonstrate how the implementation can look, but we’re planning to release them in a production-ready state in the future.

You can view the code for these projects on our GitHub:

You can also have a look at our BFF tutorial which shows how to set up all the components together.

Conclusion

The Backend for Frontend pattern is a recommended architecture solution for Single Page Applications. As long as browsers have no way of storing tokens securely, it is better to keep tokens out of the browser altogether. Using a BFF does not mitigate all attack vectors; instead, it switches back to sessions and cookies. These attack vectors are better understood and possess superior security mechanisms. Sessions in cookies also offer better protection of credentials (an attacker can use the session, but they cannot steal it to perform actions offline). As the example implementation shows, the BFF component can be easily implemented and can simplify the code on the SPA side.

Keep up with our latest articles and how-tos RSS feeds.