Final SPA – Overview

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