/images/resources/tutorials/writing-clients/spa/spa-using-token-handler.jpg

SPA using the Token Handler Pattern

On this page

Note

Curity Identity Server is used in this example, but other OAuth servers can also be used.

Overview

This tutorial and video show how to run a Single Page Application (SPA) that implements Backend for Frontend (BFF) security using the Token Handler Pattern. This provides the business benefits summarized in the Token Handler Overview:

GoalDetails
Best Browser SecurityFollows current best security practices for browser based apps, with no tokens in the browser and only SameSite=strict cookies used to call APIs
Great User ExperienceAll UI actions, including OpenID Connect redirects, are initiated in the browser so that the app has best control over usability
Modern Developer ExperienceThe UI is developed solely in a client based technology stack such as React, without the need to use cookies to call a web back end
Simple SPA CodeThe OpenID Connect and API code in the SPA is mostly one liners, with less code impact than Javascript or website security libraries
Deploy AnywhereThe SPA is built to static content that does not need securing and could be deployed to a Content Delivery Network (CDN)

Components

The example will run an SPA, API and Identity Server on a development computer, where the difficult security work is managed by plugging in components provided by Curity. An automated deployment is provided, representing the following architecture:

Token Handler

There are more moving parts to deploy when using this design pattern, due to the separation of web and API concerns. An additional OAuth Agent is used to do the OpenID Connect flow on behalf of the SPA, and an OAuth Proxy is used to deal with cookie related concerns during API calls.

Get the Code

First download the GitHub repository which consists of three small components:

Code Layout
ComponentDescription
Single Page ApplicationA simple SPA that interacts with an OAuth Agent and OAuth Proxy
Web HostThe web host serves static content and contains very little code
Example APIA simple Node.js API that validates requests using JWT access tokens

The code example demonstrates a coding design pattern where almost all of the OAuth work is done by API components provided by Curity or a similar provider. The SPA is coded in Typescript and React but the code is very simple, so it will be straightforward to port the code to any other web technology of your choice.

SPA Code

The following sections will first show how to run the SPA on a development computer and then describe the behavior in the OAuthClient and ApiClient classes, which deal with authentication and data access.

Run the SPA

This section will build and run both the simple components contained in the GitHub repository and also these open source token handler components. The example also deploys an instance of the Curity Identity Server.

ComponentDescription
OAuth AgentThe main utility API component, which deals with OAuth requests, OAuth responses and the issuing of secure cookies
OAuth ProxyA small component that runs in a reverse proxy hosted in front of APIs, to decrypt cookies and forward tokens

SPA Behavior

The simple SPA will first present an unauthenticated view:

Unauthenticated View

When Sign In is clicked, the SPA will perform OpenID Connect in an API driven manner, resulting in a secure solution where navigation actions such as multi-tab browsing also work reliably:

Authenticated View

Prerequisites

First ensure that the following components are installed:

You will also need a license file for the Curity Identity Server. If you do not have one you can get a free community license from the Curity Developer Portal by signing in with your GitHub account.

Configure URLs

The following URLs are used by default for the integrated system, and the initial token handler setup uses HTTP URLs to reduce infrastructure on a development computer:

ComponentBase URL
Single Page Apphttp://www.example.com
OAuth Agenthttp://api.example.com/oauth-agent
Example APIhttp://api.example.com/api
Curity Identity Serverhttp://login.example.com:8443

For URLs to work you will need to add the following entries to the local computer's hosts file:

bash
12
127.0.0.1 www.example.com api.example.com login.example.com
:1 localhost

If you prefer, you can override the default URLs by editing the deploy.sh script. If you want to deploy the token handler components to the web host's domain, set the web and API subdomains to be the same. You can also change the main domain name. In either case, also reflect these changes in your hosts file.

text
1234
export BASE_DOMAIN='mycompany.com'
export WEB_SUBDOMAIN='www'
export API_SUBDOMAIN='www'
export IDSVR_SUBDOMAIN='login'

Build Components

Run the following command to build code into Docker images. This will also download and build token handler components and store files in a resources subdirectory of the SPA project:

bash
1
./build.sh

Deploy Components

Next copy the license.json file into the root folder of the SPA project and then run the following script, which will deploy all components in a Docker Compose network:

bash
1
./deploy.sh

Run the SPA

You can now open a browser, navigate to http://www.example.com and sign in using a preconfigured user account of demouser / Password1 that is included in the deployment. Once authenticated, test other operations including multi-tab browsing, API calls, or navigating back to the app from an email link.

OpenID Connect Integration

The SPA implements security via simple REST requests to the following OAuth Agent endpoints, which result in SameSite=strict cookies being issued:

OperationDescription
POST /login/startPre-processing and download of the authorization request URL
POST /login/endComplete a login from the authorization response returned to the browser
GET /userInfoGet information about the user that the SPA needs to display
GET /claimsReturn claims from the ID token to the SPA, containing authentication related information
POST /refreshAsk the API to rewrite cookies when the SPA receives a 401 from microservices
POST /logoutPre-processing and download of the end session request URL

The SPA's OAuthClient class calls these endpoints using simple one liners:

typescript
1234567891011121314151617181920212223242526272829303132333435363738394041
public async handlePageLoad(pageUrl: string): Promise<any> {
const request = JSON.stringify({
pageUrl,
});
const response = await this.fetch('POST', 'login/end', request);
if (response && response.csrf) {
this.antiForgeryToken = response.csrf;
}
return response;
}
public async startLogin(): Promise<string> {
const data = await this.fetch('POST', 'login/start', null)
return data.authorizationRequestUrl;
}
public async getUserInfo(): Promise<any> {
return await this.fetch('GET', 'userInfo', null);
}
public async getClaims(): Promise<any> {
return await this.fetch('GET', 'claims', null);
}
public async refresh(): Promise<void> {
await this.fetch('POST', 'refresh', null);
}
public async logout(): Promise<string> {
const data = await this.fetch('POST', 'logout', null);
this._antiForgeryToken = null;
return data.url;
}

When the SPA wants to perform an OpenID Connect redirect it makes an API request to get the request URL. The SPA is then able to perform its own actions before and after the redirect, so that usability is good. This might include saving the app location and page state before the user logs in, then restoring it upon completion.

Page Loads

The Single Page App is restarted in the browser whenever any of the following actions take place:

  • User initially navigates to the app
  • The response to an authorization request is received
  • Authenticated user refreshes the page
  • Authenticated user opens a new browser tab or window
  • The response to an end session request is received

In each case the SPA sends the OAuth Agent's End Login endpoint its current URL and asks it to process an authorization response if required. This means the SPA itself knows nothing about OpenID Connect, and the implementation can even be changed without redeploying the SPA. The API then returns a response containing login state the SPA needs.

json
12345
{
isLoggedIn: true,
handled: true,
csrf: '9ntG18Us3XOQeN6pqPDq5V30egfIBZrEBMGolwko2OP3h8Ul02hkrQW4StbOFVx3'
}

The fields returned are summarized in the following table:

FieldUsage
isLoggedInThe SPA can use this to show or hide UI elements, such as Login or Logout menu items
handledThis is set to true when a login has just completed, and the SPA can then perform post login actions
csrfThis token can be sent as a request header during data changing API requests

API Calls

The SPA's ApiClient class sends the secure cookie by setting the withCredentials field to true. On data changing requests the SPA also sends the CSRF token as a request header:

typescript
12345678910111213141516171819202122232425
private async fetchImpl(method: string, path: string): Promise<any> {
const url = `${this.apiBaseUrl}/${path}`;
const options = {
url,
method: method,
headers: {
accept: 'application/json',
'content-type': 'application/json',
},
withCredentials: true,
};
if (this.oauthClient.antiForgeryToken) {
options.headers['x-example-csrf'] = this.oauthClient.antiForgeryToken;
}
const response = await axios.request(options);
if (response.data) {
return response.data;
}
return null;
}

When API requests from the SPA reach the reverse proxy, the secure cookie will be decrypted and its access token forwarded to APIs. During an authenticated user session the access token will expire occasionally, so the SPA deals with expiry conditions via the following steps:

  • If a 401 response is received from an API call
  • Then try to refresh the access token, which will rewrite the secure cookie
  • Then retry the API call with the updated secure cookie
  • If the refresh request fails then redirect the user to sign in again
typescript
1234567891011121314151617181920212223
private async fetch(method: string, path: string): Promise<any> {
try {
return await this.fetchImpl(method, path);
} catch (e) {
if (!this.isApi401Error(e)) {
throw ErrorHandler.handleFetchError('Business API', e);
}
await this.oauthClient.refresh();
try {
return await this.fetchImpl(method, path);
} catch (e) {
throw ErrorHandler.handleFetchError('Business API', e);
}
}
}

Web Content Delivery

The demo app's static content is served in a standard SPA manner via a simple web host. This involves code to write Web Security Headers, then to serve static web content. A real world SPA would need to write additional security headers and possibly perform other tasks as part of serving its content:

typescript
12345678910111213
app.use((request: express.Request, response: express.Response, next: express.NextFunction) => {
let policy = "default-src 'none';";
policy += " script-src 'self';";
policy += ` connect-src 'self' api.example.com;`;
policy += " img-src 'self';";
policy += " style-src 'self' cdn.jsdelivr.net;";
policy += " object-src 'none'";
response.setHeader('content-security-policy', policy);
next();
});
app.use(express.static('./content'));

The code example separates Web and API concerns so that the web backend is very light. This enables the SPA to be deployed anywhere, such as to a Content Delivery Network (CDN), while deeper concerns such as OpenID Connect security are managed by APIs.

Use Financial-grade Security

Since a backend client and a client secret is used by the OAuth Agent, any financial-grade website feature is also available to the SPA. See the Financial-grade OAuth Agent tutorial to run the example SPA while using these cutting-edge security features:

Token Handler Components

SPA code is very simple when using the token handler pattern, but the end-to-end setup requires other components to be deployed to support the SPA. See the Example Deployment for further details on how these supporting components work.

Video Tutorial

The following video provides a visual walkthrough of how to run the code example and also describes some key points about the token handler components:

Conclusion

The example SPA implements OpenID Connect and then uses only SameSite=strict cookies to call APIs. By using the Token Handler Pattern this can be done with very simple code. You then have the best security options, while maintaining all of the benefits of an SPA architecture.

Join our Newsletter

Get the latest on identity management, API Security and authentication straight to your inbox.

Start Free Trial

Try the Curity Identity Server for Free. Get up and running in 10 minutes.

Start Free Trial