Secure Single Page Applications by Preventing Token Theft

Key Takeaways

  • SPAs break the classic OAuth model: Unlike server-rendered apps, SPAs must expose access tokens to browser JavaScript to call APIs directly.

  • Bearer tokens are inherently risky in browsers: Anyone holding the token can use it; this is normally mitigated by TLS in backend-to-backend calls, but not in-browser.

  • Old mitigations are losing effectiveness: Silent SSO via hidden iframes no longer works reliably due to third-party cookie deprecation.

  • Refresh token rotation only detects theft, doesn't prevent it: And it's completely bypassed by XSS attacks, which can circumvent most browser-based protections.

  • The real fix: keep tokens out of JavaScript's reach entirely: Using server-set, script-inaccessible cookies instead of storing tokens in the browser.

  • Backend for Frontend (BFF) is the recommended pattern: Endorsed by the OAuth working group as best practice for SPA security.

  • Curity's Token Handler is a concrete implementation of BFF: Encrypts tokens in cookies, hides them from app code, and uses an API proxy to decrypt tokens before forwarding them to APIs.

On this page

Ever since Web 2.0 emerged, the path for websites from pages to applications has been set. Today, it’s more common than not to build websites as single page applications (SPAs), rendering the content in the browser using JavaScript directly. 

In contrast to classic websites, where the content is rendered on the server side and the server calls the APIs needed to get the data, the single page application must fetch the data from the API directly from the browser.

Single Page Application Security Issues

From an identity security perspective, this has been a challenge for many years. The main problem is that when using OAuth, which is the only security standard for API access in question, the application needs to pass the access token in the request to the API. In a classic website, this is done by the backend, so the access token is never exposed to the browser. However, in the SPA, this must be done in the browser. 

The Challenge with Access Tokens

To understand why this is an issue, we need to talk about the token itself. Access tokens in OAuth are commonly Bearer tokens. This means that the party that sends the token does not need to prove that the token belongs to that party. Anyone who gets hold of that token can use it. Usually, this is acceptable as the token travels from backend to backend over TLS. However, with single page applications, this is not the case.

Using Single Sign-On

Several different approaches have been taken over the years to mitigate the risk. One is to have the lifetime of the access token fairly short and require a new login (often using single sign-on — SSO — in a hidden iframe) to obtain more tokens. This, however, is harder and harder to achieve because of the third-party cookie deprecation. It means that iframes to other domains don’t allow cookies to flow, so SSO doesn’t work in that scenario.

Refresh Token Rotation

This issue has led to the use of refresh tokens in the browser. These are meant for confidential backends, and should be protected by client authentication. Since that is not possible in the browser, the only mitigation is to use refresh token rotation. This means making sure a refresh token can only be used once, and if the same refresh token is used twice, all tokens are invalidated, thus surfacing the fact that theft has been going on.

Debugging Security Vulnerabilities

Now, all of the measures above may look good on the surface, but in practice, they provide no real safeguards against token theft. As Dr. Philippe de Ryck points out in some of his many excellent videos, if an attacker manages to get a cross-site scripting attack live on your site, all bets are off. The attacker can circumvent most measures taken to protect the tokens.

With all of this in mind, the solution is obvious: don’t put tokens in the browser where the JavaScript code can access them. The browser has mechanisms for letting the server set content that the scripts cannot access. The name of the mechanism is cookies.

The OAuth working group in the IETF has been discussing this issue and working on a best practices document to guide OAuth users to do the right thing. The pattern they recommend is the backend for frontend (BFF) pattern. It involves creating a minimal generic backend that can be reused between frontends and adds the confidentiality layer required for single page applications to become secure.

Introducing the Token Handler Pattern

At Curity, we have further developed this approach. We have evolved this into a real architecture, which we call the Token Handler pattern, and we have implemented this as a first-class feature in the Curity Identity Server. The Token Handler encrypts tokens in cookies, disallowing any application code from ever seeing those, and abstracts the OAuth server interaction away from the application. The tokens can later be decrypted by the API proxy before being sent to the API.

This means that no other applications or backends are needed except for the Curity Identity Server and the API proxy, just like in any other OAuth flow.

type: embedded-entry-inline id: 3Y5zFmld8n0NNfZj8A9Hxs

An important detail is that it’s also possible to set up the Token Handler part of the server to run against other authorization servers, providing a generic solution to the SPA security problem. The solution runs completely without needing a persistence layer, as everything is encrypted and stored in the user’s browser. It’s even possible to use financial-grade extensions such as PAR and JARM, which provide strong security OAuth solutions even for single-page applications.

Related resources

Frequently Asked Questions

How does the Token Handler pattern affect page load performance compared to storing tokens directly in the browser?

Introducing an API proxy and cookie decryption step adds a network hop, so performance impact would depend on proxy placement (e.g., edge vs. centralized) and caching strategy — something to benchmark for latency-sensitive applications.

Does the Token Handler pattern work with mobile apps, or is it SPA-specific?

Mobile apps have different token storage mechanisms (secure device storage) and typically don't face the same browser-JavaScript exposure problem, so it's worth clarifying whether this pattern is purely a web/SPA solution or has mobile applicability.

How does the Token Handler pattern handle multiple SPAs or microservices that need to share authentication state?

Since cookies are typically scoped per domain, this raises questions about how a single Token Handler instance handles multiple frontend applications or subdomains without requiring duplicate logins.

What are the deployment/infrastructure requirements for running a Token Handler (e.g., additional servers, reverse proxy configuration)?

Setting up an API proxy component suggests some infrastructure changes are needed, but specifics on deployment topology, scaling, or hosting requirements aren't detailed.