In the past, I worked as an architect on web security solutions at a couple of companies where there was often a conflict between two requirements. The first was that end-users must receive the best customer experience in the browser. Secondly, Web apps must run securely in the browser and also pass security reviews.
The preferred coding approach was for web developers to create Single Page Applications (SPAs) so that the Web UI was highly responsive to end-user actions. Developing SPAs also led to adopting OpenID Connect. Yet there was always a nagging doubt that the security implementation was weaker than in older Website apps.
In this post, I will explain how an API-driven solution called the Token Handler Pattern is the preferred way to harden SPA security. But first, here are some high-level goals that many software businesses care about and must consider when designing security solutions for Single Page Apps:
- Strongest Browser Security
- Superb User Experience
- Productive Web Development
- Simple and Reliable Security Code
- Deploy Anywhere
If I had to describe Single Page Apps in one sentence, I would say they are focused on a Separation of Web and API Concerns. This decoupled nature starts with how the browser retrieves content:
A separated development setup is also possible. This could include web specialists who build a great user experience, and API developers who carefully manage the backend data.
Also, deployment to cloud providers usually works differently for Web and API content. For example, APIs can be developed in a language such as Java and deployed as containers to a cloud-native platform such as Kubernetes, with support for multi-cloud deployment. Meanwhile, web hosting simply involves making static content available, and a Content Delivery Network (CDN) can be used to improve global performance and reduce costs.
In the example setup below, the microservice instances are clustered and hosted in a single location, whereas static web content is deployed to 20 global regions:
SPA Security Problem
To increase browser security, I would follow best practices, such as avoiding refresh tokens in the browser, using short-lived access tokens stored only in memory, and ensuring that access tokens are opaque and unreadable.
If the browser page was reloaded, the library would spin up a hidden iframe and send a redirect with the SSO cookie, resulting in a new access token. This was a little complex but worked alright.
Recently, however, two things have happened that make this model non-viable. For one, browsers have introduced restrictions on third-party cookies, meaning the SSO cookie gets dropped during token renewal, so there are blocking usability problems. Secondly, browsers have introduced stronger SameSite Cookies, and using tokens in the browser is now considered less secure in comparison.
The recommended security option is now to use a Backend for Frontend (BFF), where a backend component assists the SPA and issues secure cookies on its behalf. Companies will want to ensure that integrating a BFF does not have any adverse effects on their SPAs.
The Website Pattern
A commonly adopted Backend for Frontend approach is for web developers to use a website technology stack for their SPA, with a ‘web back end’ that both serves static content and also writes secure cookies:
This approach definitely solves the security problem since the SPA can now use SameSite=strict cookies. However, I never liked this option, since the solution becomes more complex and causes the following challenges:
- Web developers may have to run backend technologies to get web content.
- Web static content cannot easily be deployed to a CDN.
- OpenID Connect redirects may be triggered abruptly before the SPA can save its state.
- Proxying all microservice requests via the web backend requires code and complexity.
- Requiring secure cookies for web requests adds navigation complexity, such as users having to reauthenticate if navigating back to the app from an email link.
The big problem here is that website stacks are not designed for separating Web and API concerns. Technology choices for SPAs require greater insight than this.
The Token Handler Pattern
A modern evolution of the Backend for Frontend pattern is to use an API driven approach, so that the SPA implements its security via a Token Handler, which consists of two components:
|OAuth Agent||An API that produces OAuth requests, handles responses, then writes tokens into SameSite=strict HTTP only encrypted cookies|
|OAuth Proxy||A small plugin that usually runs in a reverse proxy, to decrypt secure cookies and forward OAuth access tokens to APIs|
The Token Handler consists of 2 stateless components developed by security experts, which software companies can simply plug in. When the SPA wants to perform OAuth or API operations, it makes cross-origin Ajax requests to the Token Handler. By outsourcing the OAuth work, the SPA security code is considerably simplified:
The SPA remains in control of usability and performs its own redirects based on data returned from the Token Handler:
- The OAuth agent calculates URLs that the SPA uses for front channel requests.
- The OAuth agent creates back channel requests and attaches a client secret.
- The OAuth agent issues SameSite=strict cookies for the SPA.
Introducing secure cookies in this manner will have minimal impact on the Web app, the web host, or microservices, and retains all of the good features of a SPA architecture.
This solution does have a hosting prerequisite of being able to run the Token Handler on an API domain in the ‘Same Site’ as the SPA’s web origin, which is often a ‘sibling domain’ as above, and cloud providers have hosting features that enable this.
Two extra components need to be deployed, and this also needs to work for web developers, who now need to run a local token handler. This is best managed via Docker, and by adding custom web and API domains to the computer’s hosts file, which point to localhost:
This extra setup is worth the effort, and web developers can then continue to focus only on code that runs in the browser, in technologies such as React. This also ensures a developer setup that is technically similar to deployed environments, and can be useful for finding issues early.
SameSite Cookie Details
It took me a little while to properly understand SameSite cookies, but the rules are fairly clear if you own a top-level domain named example.com:
- Host the web content on https://example.com or https://www.example.com.
- Host the Token Handler in a child or sibling domain, such as https://api.example.com.
A cookie issued by the Token Handler will contain properties similar to this:
- HTTP Only
- AES256 encrypted
Secure cookies are not needed in requests to the web domain, since this only serves static content and there is nothing to secure. This eliminates quite a few potential usability problems due to dropped cookies during navigation.
Instead, the cookie only needs to be sent during Ajax requests to APIs. Cookies issued by the token handler cookies are in the ‘Same Site’ as the SPA, so they are always first-party and not impacted by browser restrictions.
The Token Handler Pattern provides security hardening for SPAs. Doing so by adding secure cookies at the application level. This provides a next-generation Backend for Frontend that separates Web and API concerns so that you avoid compromising the web architecture.
For further information on this design pattern, including an end-to-end code example that you can run via Docker on your local computer, have a look at these resources:
- SPA Best Practices
- Token Handler Pattern Article
- SPA Code Example
- Token Handler End-to-End Tutorial
- Video Walkthrough
Don't miss our upcoming webinar, Hardening Single Page Application Security, taking place on November 9. We will discuss challenges confronting SPAs developers and operators, how you can use OAuth correctly and incorrectly in SPAs, and how to overcome these challenges and errors using the Token Handler pattern.