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