Background
Previously we provided an explanation of the Backend for Frontend setup for the final SPA, where remote API components issue cookies for the SPA. Next we will summarize the main frontend behaviours of the completed SPA, then get it running locally.
Key Features
Our Final SPA improves on the previous Updated SPA, to meet some key requirements. It is difficult to satisfy all of these at the same time:
Requirement | Description |
---|---|
Best Browser Security | Only the strongest SameSite=strict cookies will be used, with no tokens in the browser |
Globally Equal Performance | Our static web content will be delivered to many locations via a Content Delivery Network (CDN) |
Pure SPA Development | Web developers work productively on frontends, with small code bases and no security plumbing |
Security Architecture
As explained in the previous post, the following components are used by the SPA to implement end-to-end security. The security is based on the Token Handler Example, where the OAuth Agent handles cookie issuing on behalf of the SPA:
After user login, the SPA sends HTTP-Only cookies in API requests. The SPA calls directly to a high-performance API gateway that is hosted in the same site as the SPA’s web origin. An OAuth proxy component runs in the gateway, which decrypts cookies and forwards JWT access tokens to APIs.
Cloud Hosting
The overall system is also deployed to AWS. The SPA’s web static content is deployed to S3, then Cloudfront replicates the web resources to many global locations, without the need to manage servers or containers:
In AWS, the API is implemented as Serverless Lambda Functions, and exposed to the internet via the AWS API Gateway. This provides a low cost solution for my online demos.
Online Deployment
Any reader can log in to the AWS deployed version of the above architecture from this blog’s Cloud Samples Quick Start page, then test OAuth lifecycle events via the demo app’s buttons:
Local Execution
The SPA is provided in a GitHub code sample, which manages only frontend concerns. The deployment resources handle use cases for hosting static content. The next post will show how to run the code sample locally.
Development URLs
The following URLs are accessed during local development, and only the first of these runs on the local computer:
Component | Base URL |
---|---|
SPA | https://web.authsamples-dev.com/spa/ |
Investments API | https://api.authsamples.com/investments |
Investments API Web Route | https://api.authsamples-dev.com/tokenhandler/investments |
OAuth Agent | https://api.authsamples.com-dev/tokenhandler/oauth-agent |
Authorization Server | https://login.authsamples.com |
The current post summarises the main web behaviours, and the next post explains how to run the SPA locally.
OAuth Clients
In AWS Cognito the deployed SPA is registered as follows, to point to an AWS Cloudfront web origin. The SPA is now registered as a confidential client, and its backend for front end uses a client credential in grant requests for tokens:
A second entry exists for running the SPA during development, which is configured equivalently, except that in this case the configured web URLs use a development web origin, configured in the local computer’s hosts file.
React Code
The SPA’s code has been updated to use React as a web framework, to enable better features during coding of views. A tree of views is rendered, which leads to concurrent API requests. Both login and token renewal operations will need to be synchronized, when tokens expire.
return (
<>
<TitleView {...getTitleProps()} />
<HeaderButtonsView {...getHeaderButtonProps()} />
{model.error && <ErrorSummaryView {...getErrorProps()} />}
{model.isLoaded &&
<>
<SessionView {...getSessionProps()} />
<Routes>
<Route path='/companies/:id' element={<TransactionsContainer {...getTransactionsProps()} />} />
<Route path='/loggedout' element={<LoginRequiredView {...getLoginRequiredProps()} />} />
<Route path='/*' element={<CompaniesContainer {...getCompaniesProps()} />} />
</Routes>
</>
}
</>
);
React code is built to its own JavaScript bundle file, separate to other dependencies in the node_modules folder. This demonstrates a way to split the overall app’s code into multiple, roughly evenly sized, modules.
Path Based Routing
Earlier SPAs in this blog used hash based routing, and the final SPA has been updated to use React’s support for path based routes. The main demo SPA continues to use a base path expressed in the index.html file:
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width, initial-scale=1, shrink-to-fit=no'>
<base href='/spa/' />
<title>OAuth Demo App</title>
<link rel='stylesheet' href='bootstrap.min.css'>
<link rel='stylesheet' href='app.css'>
</head>
<body>
<div id='root' class='container'></div>
<script type='module' src='vendor.bundle.js'></script>
<script type='module' src='react.bundle.js'></script>
<script type='module' src='app.bundle.js'></script>
</body>
</html>
The React application then reads the base path from the index.html file when the app loads, and supplies it to the React router. The app then uses relative routes, within the ‘/spa/‘ base path.
Mobile First
Since the SPA is now deployed to the AWS cloud, we would expect it to be accessed on small mobile devices, so the final SPA has been made more presentable on smaller screens:
Web Performance
The cleanest separation of concerns and simplest code should lead to high performance, as long as frontend views and API endpoints are also designed intelligently. The following technical factors also impact performance, and there are trade offs between performance and other qualities:
Aspect | Responsibilities |
---|---|
Content Delivery | Web latency is roughly equal for all global users, since a CDN ensures that all web downloads are fairly local |
Static Content Caching | Cache control headers are used for web assets, so that future requests are served from the browser cache |
API Response Caching | The SPA also caches API responses, to prevent redundant API requests, such as during back navigation |
Bundle Splitting | As the SPA grows there should be a way to modularize downloads of JavaScript and CSS |
Requests to APIs use CORS, which adversely impacts performance a little. Pre-flight OPTIONS requests are sent when a user first accesses an API endpoint. Then, no further OPTIONS requests are required for that user and endpoint for 24 hours.
This blog’s final SPA connects by default to serverless APIs, which have suboptimal performance in a few areas, due to slow startup and an inability to perform memory caching. The SPA performs best when run against cloud-native APIs and using a high performance cloud-native API gateway.
Time to First Render
The real time to first render in a secured web application is not immediate and requires the following steps. Multiple static content and HTTP requests are involved. Modern networks and the latest HTTP/2 functionality help to ensure good performance:
Step | Description |
---|---|
Unauthenticated Access | The time to download HTML resources and run code to detect whether the user is authenticated |
OAuth Login | The time to redirect to the authorization server and present login screens, then return to the SPA |
Secure SPA Access | The application reload, followed by API requests to get secured data specific to the user |
I aim to have a good awareness of factors that impact performance, but avoid optimizations that add complexity to the architecture. Instead, I aim for the best separation of concerns, where code is kept simple and will grow effectively.
Web Code Scalability
Some companies run multiple micro-frontends for the same business area within a single web domain. This keeps web code manageable as it grows and also enables bundle splitting in the simplest way.
The final web code example is hosted at an /spa path to show that other apps could be added alongside it, and share the same cookies.
This would enable multiple teams to each work on only one frontend at a time, and avoid loading the code for all other apps. Each frontend would call APIs using shared cookies, and it would be possible to seamlessly navigate across apps without OpenID Connect redirects.
Legal and Regulatory
A real cookie secured SPA may also need to ask the user to consent to use of cookies, and provide information on how any personal information is used, to comply with regulations such as GDPR. This blog’s SPA omits this step, since it uses only fictional data and fictional user accounts.
Where Are We?
We have summarised the behavior of the Final SPA, to complete the web architecture’s foundational security work. We have updated to a modern web framework, and the architecture enables both URLs and code to be scaled effectively as the web code base grows.
Next Steps
- Next we will explain How to Run the React SPA
- For a list of all blog posts see the Index Page