Token Handler End-to-End Tutorial

Token Handler End-to-End Tutorial

tutorials

Overview

This tutorial and video explain how to run an end-to-end code example for a Single Page Application (SPA) that implements OpenID Connect via the Token Handler Pattern. This provides a Backend for Frontend (BFF) for the SPA, and also provides a very clean separation of web and API concerns. There are quite a few moving parts that need to be understood though.

Traditional Single Page Apps

In recent years it has been common to implement OpenID Connect for SPAs solely in Javascript. This is no longer recommended, as explained in the OAuth for Browser Based Apps specification, for two main reasons:

  • Using access tokens in the browser has more security risks than using secure cookies, so SPAs should instead use cookies with recent SameSite improvements
  • Recent browser changes to prevent tracking may result in dropped third party SSO Cookies during traditional SPA flows, leading to blocking usability issues

To solve these problems it is now recommended to use a Backend for Frontend approach, where the SPA security is routed via a back end component that can manage token storage and issuing of secure cookies for the browser.

SPA Behavior

The example SPA will use a Curity Token Handler that provides extra REST API endpoints, to call the Authorization Server on behalf of the SPA. The token handler will issue application level secure cookies that can be used from the site of the SPA’s web origin. All of the good features of SPAs will be maintained, and combined with browser security equivalent to that of a website based app:

  • Recommended Browser Security
  • Modern User Experience
  • Simple SPA Code
  • Deploy Anywhere

Components

The end-to-end solution for an SPA now includes a couple of extra components, illustrated below:

Components

A software company provides the following application components, which only require simple OAuth code:

ComponentDescription
Single Page AppThe web app, which for this code example is a simple app implemented with React
Web Content DeliveryThis serves web static content, and many companies use a Content Delivery Network (CDN)
MicroservicesIn the code example the SPA calls a simple NodeJS API via a reverse proxy

The Token Handler consists of two toolbox components, and most work is done in the first of the following two components. Software companies should not need to develop either of these, and is it recommended to instead plug in a tested third party implementation:

ComponentDescription
OAuth AgentCalled by the SPA to perform the intricate OAuth work, while managing secrets and tokens, after which SameSite cookies are issued to the browser
OAuth ProxyThis usually runs as a small reverse proxy plugin, to receive the secure cookie from browsers, then extract access tokens and forward JWTs to microservices

Curity API Components

Curity provide an initial implementation of the toolbox components, which customers can use to implement their SPA security. At the time of writing, these components are at a demo level to enable the design patterns to be understood.

If these components do not meet your needs they can be adapted as required. The solution is not Curity specific and these components will work with any standards based Authorization Server.

Run the End-to-End Solution

Install Prerequisites

First ensure that you have both NodeJS and Docker Desktop installed, since these components will be used to build and deploy the browser and API code.

Configure Domains

The code example uses the following test domains as one possible deployment setup:

DomainUsage
www.example.comThe SPA’s web origin, whose web host provides static content to the browser
api.example.comThe domain where microservices and the Backend for Frontend are hosted
login.example.comWhere the Curity Identity Server is hosted, which issues tokens for the UI and API

These domains must first be added to your /etc/hosts file as follows:

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

Token Handler Hosting

For SameSite cookies to work, the Token Handler components must be hosted on the same parent domain as the SPA's web origin, and the code example uses a value of `example.com`.

Get the Code

Clone the repository with the following commands:

git clone https://github.com/curityio/web-oauth-via-bff
cd web-oauth-via-bff

Then view the code components and note that these all require only simple code. The Code Example article describes how the OAuth work for the SPA is implemented.

SPA Code

Build the Code

Next run the build script, which compiles the code for all components and deploys it to Docker images:

cd code
./build.sh

Note that this also downloads and builds the Token Handler’s (OAuth Agent) API within the code folder, and you can study its code or adapt it if required:

Token Handler Code

Supply a Curity Identity Server License File

Before deploying the system you will 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.

Docker Components

The deployment folder contains these components in total:

ComponentDescription
Web HostA simple web host to serve the SPA’s static content to the browser
Example APIA simple API that the SPA uses to access data, and which receives JWTs
OAuth AgentThe Curity provided API implements the OAuth work and cookie issuing on behalf of the SPA
Reverse Proxy + OAuth ProxyKong Open Source is deployed, along with a plugin for routing requests to APIs with JWTs
Curity Identity ServerThe Curity Identity Server issues tokens to the OAuth agent on behalf of the SPA
Curity SQL DatabaseA database is deployed to provide a well known test user name and password

These are all deployed to a development computer via a Docker Compose file:

Docker Compose

Deploy the System

The entire system can be deployed via the following commands:

cd deployment
./deploy.sh

This script downloads two reverse proxy plugins to the deployment folder, and you can study or adapt plugin behaviour if required. We will look at this plugin’s behavior shortly:

Plugin Code

Browse to the SPA

You can then browse to http://www.example.com to run the SPA, which will initially present an unauthenticated view:

Unauthenticated View

You can then sign in with the test user demouser using password Password1, after which an authenticated view is displayed. The SPA can then download user info from the token handler’s userInfo endpoint and can also securely call the Example API:

Authenticated View

Multi-Tab Browsing

Next test some multi tab and navigation operations. These areas can sometimes be a source of problems for web OAuth solutions, but note that the code example handles them reliably:

  • Reload the page via a keystroke such as Cmd+R
  • Open multiple browser tabs or windows and test signing in or out
  • Follow an email link to your authenticated session

View Logs

While using the system you can look at logs to understand what is happening in the OAuth Agent or OAuth Proxy components:

Deployed Components

When troubleshooting, it can be useful to view the log output of only a specific component in its own terminal window. The following standard Docker commands can be used for this:

export BFF_API_CONTAINER_ID=$(docker container ls | grep bff-api | awk '{print $1}')
docker logs -f $BFF_API_CONTAINER_ID
export KONG_CONTAINER_ID=$(docker container ls | grep reverse-proxy | awk '{print $1}')
docker logs -f $KONG_CONTAINER_ID

Browser Security Overview

Website OAuth Flow

The security used by the Token Handler and SPA is equivalent to that used by a website based app, with these main characteristics:

  • Code Flow + PKCE, along with the use of a client secret
  • Use of recent SameSite cookie improvements

Using a Token Handler does not provide better security than a traditional website app, but provides a better separation of concerns, leading to simple code, best usability and the ability to deploy to a Content Delivery Network (CDN).

Developer Setup

Web developers will need to run the token handler on their workstation, and the preferred way to do this is to use a Docker container. Curity expect to provide additional options for developers in future, such as integrating with the Webpack Dev Server.

OAuth Messages

The token handler returns an Authorization Code Flow + PKCE request URL to the SPA, with parameters similar to these:

GET /oauth/v2/oauth-authorize
    ?client_id=spa-client
    &redirect_uri=http%3A%2F%2Fwww.example.com%2F
    &response_type=code
    &code_challenge=k2XFDLJ_mIVEt25-phu4_3O4Jqrd8QqTGGbeEsql3Rc
    &code_challenge_method=S256
    &state=NlAoISfdL1DxPdNGFBljlVuB1GDjgGARmqDcxtHhV8iKNYu6ECS2KOavDHpI3eLN
    &scope=openid%20profile

After login, the token handler sends a request payload similar to the following to the Curity Identity Server, to exchange the code for tokens:

POST /oauth/v2/oauth-token

grant_type=authorization_code&
client_id=spa-client&
client_secret=Password1&
authorization_code=bMYnHhHHRFRnT7sbuPmqIzHpb8RFfqtg&
redirect_uri=http://www.example.com/

When an access token expires, the SPA can ask the token handler to refresh access tokens. The token handler then sends a request payload similar to the following to the Authorization Server:

POST /oauth/v2/oauth-token

grant_type=refresh_token&
client_id=spa-client&
client_secret=Password1&
refresh_token=76fd951484a3d4cc31f869bb8fc93df ...

These messages improve upon the security of older SPA solutions, since a client secret can be used, and since no tokens are used in the browser.

Secure Cookies Issued

In total the token handler uses 5 different secure cookies, with different data in each. Each of the cookies are strongly protected using AES256 symmetric encryption, with the encryption key known only by the server:

Cookie NameContains
example-loginA temporary cookie used between start / end login, to verify that the state before and after login matches, and to store the PKCE code verifier
example-authThe main SPA cookie contains the refresh token representing the user’s authenticated session
example-idThis cookie contains the ID token, which the API provides to the SPA via its /userInfo endpoint
example-atThis cookie contains the current access token, which is used later when calling APIs
example-csrfThis cookie can be used for additional CSRF protection, as described in OWASP CSRF defense in depth

All cookies have the following properties, though the paths of the refresh and ID cookies are restricted to only the token handler endpoints where they are needed.

PropertyDescription
HTTP OnlyThe cookie cannot be read by Javascript code
SecureThe cookie requires an SSL connection
SameSite=strictThe cookie can only be sent from the web origin, and not from malicious domains
Domain=api.example.comThe cookie is only needed when calling APIs
Path=/The access token cookie is available to all APIs within the API domain

Note that the SPA origin is www.example.com but that cookies only need to be issued to the API domain at api.example.com, which is in the Same Site. An SPA should not need to send cookies during web requests, so it is preferred to limit the scope of the cookie to API requests. If you have particular requirements then the cookie domain can be extended to .example.com and also sent during web requests. Note however that this increases the potential for this type of usability problem:

  • User signs in
  • User follows an email link back to their authenticated session
  • The cookie is dropped because it is SameSite=strict
  • The user is redirected to re-authenticate again

OAuth Client Settings

You can login to the Admin UI to view the OAuth client settings for the SPA, using these connection details:

  • https://localhost:6749/admin
  • User: admin
  • Password: Password1

The spa-client settings are only used by the token handler, which uses the following Curity configuration settings, which are equal to those of a website app:

<client>
    <id>spa-client</id>
    <client-name>spa-client</client-name>
    <description>SPA that uses the Token Handler Pattern</description>
    <secret>Secret1</secret>
    <redirect-uris>http://www.example.com/</redirect-uris>
    <scope>openid</scope>
    <scope>profile</scope>
    <user-authentication>
        <allowed-post-logout-redirect-uris>http://www.example.com/</allowed-post-logout-redirect-uris>
    </user-authentication>
    <capabilities>
        <code>
        </code>
    </capabilities>
    <use-pairwise-subject-identifiers>
        <sector-identifier>spa-client</sector-identifier>
    </use-pairwise-subject-identifiers>
    <validate-port-on-loopback-interfaces>true</validate-port-on-loopback-interfaces>
</client>

Secure Development

The OAuth security in the code example follows current best practices for browser security, but of course this does not make your app fully secure. Software companies must also follow a security development lifecycle, and in particular protect against Cross Site Scripting (XSS). If there is an XSS vulnerability then secure cookies could be abused by an attacker to steal data.

API Security Overview

API Request Routing

Once authentication has been completed, the SPA will make API calls to access data, as described in the Code Example article. Your APIs will expect to receive JWTs and the usual way to manage this is via the Phantom Token Pattern.

For SPAs we now have an additional requirement, to decrypt the secure cookie and enforce CSRF checks before the phantom token pattern runs. The overall workflow will look like this:

API Flow

Reverse Proxy Behavior

The reverse proxy is the preferred place to implement this type of utility operation, rather than complicating your API code. The kong.yml configuration for the code example focuses only on the routing of web requests to a single API:

- name: business-api
  url: http://apiserver:3002
  routes:
  - name: business-api-route
    paths:
    - /api

  plugins:

  - name: bff-token
    config:
      encryption_key: NF65meV>Ls#8GP>;!Cnov)rIPRoK^.NP
      cookie_name_prefix: example
      verify_csrf: true
      trusted_web_origins:
      - http://www.example.com
  
  - name: phantom-token
    config:
      introspection_endpoint: http://curityserver:8443/oauth/v2/oauth-introspect
      client_id: api-gateway-client
      client_secret: Password1
      token_cache_seconds: 900
      trusted_web_origins:
      - http://www.example.com

The plugin components are implemented in the lightweight LUA Scripting Language and run in this sequence:

ComponentDescription
OAuth Proxy PluginDecrypts the secure cookie containing the access token, then forwards the opaque access token to the next plugin
Phantom Token PluginIntrospects the opaque token to get a JWT to forward to the API, then caches the JWT for subsequent requests

OAuth Proxy Plugin

The only new reverse proxy component is the OAuth proxy plugin, which deals with decrypting cookies and forwarding the access token. For data changing commands, the plugin can also verify that a CSRF request header matches that in the CSRF cookie.

End-to-End Data Access

The example API included with the code example requires a valid JWT for every API request, and returns a 401 error if this is not the case. The below Get Data button allows API calls to be tested all the way through, and you can view reverse proxy logs to see the plugins being used:

Authenticated View

Deployment Variations

The deployment setup for the code example consists only of a bff path and a single api path, in order to explain the routing mechanics. Real world systems often deal with multiple web domains, multiple types of client or alternative technical setups.

Branded Web Domains

In some companies the microservices are hosted on a domain such as api.example.com but the web apps are hosted on branded domains named after products. In this setup an additional API domain would be spun up, as a child or sibling of the web domain:

Token Handler Technology Choices

The token handler’s OAuth Agent can be implemented and deployed in various forms and some examples are listed below. Curity will be providing additional implementations in order to give software companies additional choices.

  • Container Based API
  • Serverless Lambda
  • Reverse Proxy Plugin
  • Web Host Extension

We recommended implementing the OAuth Proxy within a reverse proxy or API gateway rather than in your API’s own code, since these components have proven routing and high throughput capabilities.

Multiple Types of Client

Using the Token Handler Pattern means that there will be different entry points to microservices for web clients and other clients. This type of setup is quite common already for APIs that require Mutual TLS connections from business partners:

Client TypeSecurity Model
Mobile clientsAccess tokens are used to call APIs via the default entry point
Web clientsSecure cookies are used to call APIs via a web entry point
B2B clientsAccess tokens are used to call APIs over a Mutual TLS channel

A reverse proxy makes it easy to support multiple API security models via different entry points, but without impacting underlying microservices. A possible configuration for an Orders API is shown below, where the reverse proxy manages the different entry points, so that the API receives all requests on the same URL and only needs to work with JWTs:

- name: orders-api
  url: http://apiserver:3002
  routes:

  - name: default
    paths:
    - /api/orders
    plugins:
      - name: phantom-token

  - name: web
    paths:
    - /api/web/orders
    plugins:
      - name: bff-token
      - name: phantom-token

  - name: mutual-tls
    paths:
    - /api/b2b/orders
    plugins:
      - name: mutual-tls
      - name: phantom-token

Video

The following video visually shows how to deploy the code example, then uses the deployed system and explains some further details:

Conclusion

We demonstrated an end-to-end solution where the main components a company develops can continue to be implemented in a simple and standard way:

  • Web apps make simple REST requests to a Token Handler to perform their OAuth work in an API driven manner
  • Microservices continue to validate JWTs in the standard way, and do not need to deal with cookies

Meanwhile the more complex plumbing was implemented once as Curity provided toolbox components, which would need to be deployed for each of your web domains. This separation of concerns leads to web apps that uses modern and recommended security. The separated architecture is then very easy to scale and extend.

Keep up with our latest articles and how-tos RSS feeds.