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 backend component to handle tokens and issue secure cookies to the frontend - often referred to as a
Backend for Frontend (BFF) approach. The Token Handler Pattern is a modern evolution of BFF, where the SPA’s OpenID Connect security is implemented in an API driven manner:
Using this approach, all communication from the SPA to the Authorization Server will now go through the token handler, and tokens will not reach the SPA at all. The token handler 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 token handler or a dedicated plugin in an API gateway.
As the token handler code is not available in the browser, it can act as a confidential client for the SPA, 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.
A token handler performs two main responsibilities, and an optimized deployment can use two separated components:
|OAuth Proxy||A component used on API requests, to receive a secure cookie, then retrieve access tokens for sending to APIs. This could be deployed as a small plugin that runs in the reverse proxy or API gateway via which APIs are called.|
The Token Handler 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 token handler.
The token handler 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 token handler would then issue cookies with a session ID and read tokens from the store.
A token handler 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 the token handler pattern 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 token handler 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 token handler must be accessible on the same domain as the SPA, a sibling domain (e.g.,
www.example.com for the SPA, and
api.example.com for the token handler), or a child domain. This enables the token handler to issue cookies with the
SameSite=strict parameter, which is recommended to maintain a high level of security.
When using a token handler, the SPA receives first-party cookies that work reliably in all modern browsers. The SPA only actually needs to send the cookie in API requests and not during requests for web content. This means that web navigation actions that might drop cookies never impact the end user. 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 token handler is responsible for all communication with the Authorization Server. This hides the complexity of authorization flows from the SPA. The token handler can expose a simple API to the SPA, and the SPA does not need to know the details of implemented flows. This allows the token handler to choose different strategies for sensitive scenarios. For example, the token handler can implement Pushed Authorization Requests, JWT Authorization Requests, or JWT Security Authorization Response Mode, and the complexity of these can be hidden from the SPA.
Even though a token handler is a lightweight component, it can get tricky to implement everything correctly. This is why we at Curity have created a sample implementation as a reference for this pattern. Two token handler components were developed: the OAuth Agent and a small OAuth Proxy which can receive cookies issued by the agent. These components demonstrate how the implementation can look, but we’re planning to release them in a production-ready state in the future.
- OAuth agent implemented as a Node.js Express API
- OAuth proxy implemented as a Kong API Gateway plugin
You can also have a look at our Token Handler Tutorial, which shows how to run an integrated solution, consisting of an example SPA and API, a reverse proxy, the Curity Identity Server and the token handler components.
The Token Handler 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 token handler 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 token handler component can be easily integrated and can simplify the code on the SPA side.