SPA Backend for Frontend

Background

In our previous post we ran a Microsoft Entra ID SPA and API OAuth Flow. Our SPA now needs hardening before it is final, and in a real company we would have two major issues to deal with:

  • Concerns about whether using access tokens in the browser is secure enough, and whether HTTP-only cookies should be used instead
  • A blocking usability problem with token renewal and multi tab browsing, due to authorization server cookies being dropped

Security Best Practices for SPAs

In recent years, the RFC6265bis specification proposed updates to cookies to protect user privacy. This also introduced SameSite cookies, with some improved security behaviors.

Security experts now consider it best to use only the latest and strongest HTTP-only and SameSite=strict cookies in an SPA, and to avoid all use of tokens in the browser. These are usually the two main threats:

Threat Best Practice Mitigation
Cross Site Scripting Malicious code execution is by far the biggest threat, so follow OWASP XSS Prevention Best Practices
Cross Site Request Forgery Also use only the latest secure cookies in the browser and follow OWASP CSRF Prevention Best Practices

The best practices are explained further in the OAuth for Browser Based Apps document. The key benefit of using cookies is to limit the impact of cross-site scripting exploits.

High Level Requirements

At a higher level, there were three main requirements I wanted to meet for this blog’s final SPA. When first writing this blog I struggled for quite some time to meet all of them.

Requirement Description
Strong Browser Security Follow current best practices, with only the latest and most secure cookies used in the browser
Globally Equal Performance Deploy the SPA to a content delivery network, so that web latency is roughly the same everywhere
Pure SPA Development Web developers work productively on frontends, with small code bases and no security plumbing

This Blog’s Backend for Frontend

In 2021 I adopted the Token Handler Pattern, which involves using the following API components to deal with the cookie security:

Component Description
OAuth Agent An API driven backend for frontend that simplifies OAuth  work for the SPA and manages a backend client credential. The BFF also issues cookies to the SPA.
OAuth Proxy A plugin that runs in a high-performance API gateway. During API requests the plugin decrypts cookies and forwards JWT access tokens to APIs.

These components are deployed to the API side of the architecture and do not need to run on the computers of web developers, or in the web static content host.

Large Scale Cookie Design

In larger architectures, up-front thinking is recommended when designing cookie security. Cookie domains and paths can be designed in various ways for deployed systems. I also think through these routes in terms of what developers need. This post summarizes the choices I made for this blog’s final SPA.

Security Components

The overall components that need to be deployed are shown here, where the token handler components act as an advanced reverse proxy. The SPA calls a simple API that uses an investments theme, and returns hard coded data for display. This will be done while using best practice cookie security.

Calls from the SPA to token handler components must be SameSite and are also cross origin. The SPA’s cookies are therefore first-party and never impacted by recent browser restrictions to block third-party cookies.

The final SPA’s web content is deployed to an AWS CloudFront web subdomain and does not need securing. The SPA uses APIs deployed as Serverless Lambdas to an API subdomain behind the AWS API Gateway.

API Driven Cookie Issuing

Cookies issued use the following properties. They are API credentials associated to the API subdomain. They are not used on requests for web static content:

  • HTTP Only
  • Secure
  • SameSite=strict
  • Domain=api.authsamples-dev.com
  • Path=/

When using this technique, it is recommended that you own the entire domain, as in this blog’s example deployments. The BFF must also apply OWASP CSRF protection, and restrict access to precise Trusted Web Origins.

Designed URLs

These are the URLs I wanted to use for the deployed AWS environment. The use of a /spa path for the SPA indicates that additional micro-UIs for the same business area could be deployed alongside it and share the same cookies. Similarly, additional APIs could be deployed off the API base URL:

Component Base URL
SPA https://web.authsamples.com/spa/
Investments API https://api.authsamples.com/investments
Investments API Web Route https://api.authsamples.com/investments
OAuth Agent https://api.authsamples.com/oauth-agent
Authorization Server https://login.authsamples.com

The API URL is the main public API gateway entry point for the API, which could be in any domain. The web route is the web entry point in the API gateway and must always be in the same site as the web origin.

AWS Deployed URLs

In AWS I use Serverless Lambda Functions for the investments API, which is deployed behind the AWS API gateway, to provide a low cost and low maintenance online API.

A key requirement was to implement web security outside of the Serverless API project. The web security should execute in the API gateway, then route internally to the Serverless API.

Since this is not supported by AWS lambda technology, I instead ran token handler components at a /tokenhandler path, leading to these URLs:

Component Base URL
SPA https://web.authsamples.com/spa/
Investments API https://api.authsamples.com/investments
Investments API Web Route https://api.authsamples.com/tokenhandler/investments
OAuth Agent https://api.authsamples.com/tokenhandler/oauth-agent
Authorization Server https://login.authsamples.com

This meets the main requirement of doing the cookie related work in the web route, after which a request with a JWT access token is forwarded to the API.

Web Development Environment

The final SPA is developed in React, and developers will run a simple Development Web Host locally at the SPA URL, to serve static content.  All other URLs run in the AWS cloud.

Component Base URL
SPA https://web.authsamples-dev.com/spa/
Investments API https://api.authsamples.com/tokenhandler/investments
Investments API Web Route https://api.authsamples-dev.com/tokenhandler/investments
OAuth Agent https://api.authsamples-dev.com/oauth-agent
Authorization Server https://login.authsamples.com

The authsamples-dev.com site is partly local and partly cloud deployed. For the web origin and API routes to work together, the web domain must be aliased to localhost in the local computer’s hosts file:

127.0.0.1  localhost web.authsamples-dev.com
::1        localhost

Developers then run the SPA in the browser using its domain name, https://web.authsamples-dev.com. There is then a pure SPA development setup, where only the React code and a browser needs to be run locally, while development also uses the recommended cookie security.

Full Stack Development Environment

It gets more complicated when you need to run both the SPA and APIs locally, which should be avoided most of the time. When required, a local token handler must also be run. I do so in Docker containers on port 444. The API then runs on port 446 on the host computer:

Component Base URL
SPA https://web.authsamples-dev.com/spa/
Investments API https://apilocal.authsamples-dev.com:446/investments
Investments API Web Route https://apilocal.authsamples-dev.com:444/investments
OAuth Agent https://apilocal.authsamples-dev.com:444/oauth-agent
Authorization Server https://login.authsamples.com

Developers must then also add the apilocal subdomain to the hosts file. An end-to-end local setup can then be run, where the local SPA calls a local API.

Cloud Native Environments

All components could also be deployed to a local Kubernetes cluster, such as that provided by KIND. Deployed URLs then match my designed URLs:

Component Base URL
SPA https://web.mycluster.com/spa/
Investments API https://api.mycluster.com/investments
Investments API Web Route https://api.mycluster.com/investments
OAuth Agent https://api.mycluster.com/oauth-agent
Authorization Server https://login.mycluster.com

A cloud native deployment to AWS could use the following API URLs, that run from a cloud Kubernetes cluster. In this case the web domain’s backend would continue to use AWS Cloudfront:

Component Base URL
SPA https://web.authsamples-k8s.com/spa/
Investments API https://api.authsamples-k8s.com/investments
Investments API Web Route https://api.authsamples-k8s.com/investments
OAuth Agent https://api.authsamples-k8s.com/oauth-agent
Authorization Server https://login.authsamples-k8s.com

Cookie Limits

This blog follows an approach of storing tokens in encrypted HTTP-only cookies. The backend OAuth and cookie components are then stateless and easy to manage. By default, the tokens are issued by AWS Cognito, which uses a JWT format, with an RS256 signature, for all tokens.

For the final SPA, cookie size limits of desktop and mobile browsers are not exceeded, but the 4KB limit for a response HTTP header is exceeded in the cookie response after a user login. Some HTTP servers, such as NGINX, would therefore need default header size limits to be increased.

To resolve this in the best way, the authorization server should issue smaller tokens. A more efficient JWT algorithm such as ES256 will reduce cookie sizes by around 50%. Issuing access and refresh tokens in an opaque reference token format will reduce them even further.

Where Are We?

We have explained the deployment variations for the Final SPA. When using cookies in browser based  apps it can be useful to think through URLs and deployment scenarios early. Doing so can enable the most productive development setups, and also ensure that the web architecture scales effectively.

Next Steps

  • We will deliver a Final SPA that uses the Token Handler Pattern
  • For a list of all blog posts see the Index Page