The Token Handler Pattern for Single Page 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.
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 goes via an
OAuth Agent component, and tokens will not reach the SPA at all. The OAuth Agent then 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. This translation is best done using a dedicated
OAuth Proxy plugin, hosted in the API gateway.
As the code for the OAuth Agent is running outside of 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.
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 an OAuth Agent.
The implementation of token handler components 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 OAuth Agent would then issue cookies with a session ID and read tokens from the store.
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).
Token handler components can be deployed in different ways. They 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 the OAuth Agent and OAuth Proxy, in some cases, can be run without the need for any additional infrastructure.
Whatever type of hosting is used, the token handler components 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 backend), or a child domain. This enables cookies issued to be first party and prevents them being dropped by browsers. The cookies should also use the
SameSite=strict parameter, to maintain a high level of security.
When using the token handler pattern, 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 OAuth Agent is responsible for all communication with the Authorization Server. This hides the complexity of authorization flows from the SPA. A simple API can be exposed to the SPA, and the SPA does not need to be aware of the security details. This allows the OAuth Agent to choose different strategies, such as Pushed Authorization Requests, JWT Authorization Requests, or JWT Security Authorization Response Mode, without any code changes in the SPA.
Even though the token handler components are lightweight, it can get tricky to implement everything correctly. This is why we at Curity have created sample implementations as a reference for this pattern. For further information, including code examples that can be quickly run in a fully integrated setup on a development computer, see the below resources:
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 this pattern 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). The example SPA shows that the OAuth Agent can be easily integrated, and also simplifies the SPA's security code.