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.
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.
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.
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.
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.
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.
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).
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.
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.
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.