Curity token handler design overview

Token Handler Design Overview

On this page

Browser Security

In recent years it has been common to implement OpenID Connect for SPAs solely in JavaScript. This is no longer recommended, for two main reasons:

  • Using access tokens in the browser introduces additional browser security threats, which are best prevented using the browser's built-in security mechanisms.
  • Recent browser changes to prevent tracking may result in dropped third party SSO cookies during traditional SPA iframe-based flows, leading to blocking usability issues.

The Token Handler Pattern article provides background on threats in various web architectures, resulting in a current SPA Best Practice of using a Backend for Frontend (BFF) approach. The SPA then implements OpenID Connect security via a backend component that manages token storage and issuing of secure cookies for the browser.

Token Handler Pattern

The token handler pattern keeps all tokens out of the browser. A backend for frontend (BFF) component issues only the most secure HTTP-Only, SameSite=strict cookies to the browser, on behalf of the SPA. These are first-party cookies and not subject to browser restrictions:

Token handler components issue first-party cookies on behalf of the Single Page Application

There are key differences between the token handler pattern and alternative approaches:

  • The SPA's backend for frontend is separated from the web static content host.
  • The SPA makes secured API requests to a high-performance API gateway.
  • The SPA controls its own OpenID Connect flow using API requests.

These design choices maintain a pure SPA architecture, where developers can focus solely on the frontend customer experience, and authorization responsibilities are always implemented on the API side of the architecture.

Business Benefits

The main benefits of using the token handler pattern are summarized in the below table:

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 needing a web backend that issues cookies
Simple SPA CodeThe OpenID Connect and API code in the SPA is mostly one liners, with less code impact than JavaScript or web back end 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)

Hosting Prerequisite

For any web application that uses secure cookies, the domains used need to be designed with care, and this is also true when using the token handler pattern. For a web origin of https://example.com or https://www.example.com, the token handler components must either run on the same domain, or a related domain such as https://api.example.com or https://bff.example.com.

Token Handler Hosting

For secure cookies to work reliably for the SPA they must be first-party, and the token handler components must be hosted on the same parent domain as the SPA's web origin.

OAuth Agent

When the SPA needs to initiate a login or perform other OAuth work it makes Ajax calls to the OAuth Agent. This is a utility API that acts as a backend for frontend for the SPA. The OAuth Agent makes OAuth requests to the authorization server and attaches a client credential when needed. After user authentication, the OAuth Agent writes secure cookies that are returned to the browser.

OAuth Agent Operations

The following operations represent the OAuth Agent's API interface, and all of these are designed to serve SPAs via Ajax requests from the browser:

EndpointDescription
GET /sessionGet the user's authentication state and claims from the ID token.
POST /login/startStart a login by providing the OAuth authorization request URL to the SPA and setting temporary cookies.
POST /login/endEnd a login and return HTTP only encrypted cookies to the browser, with claims from the ID token.
POST /refreshRefresh the current access token and rewrite secure cookies.
POST /logoutClear secure cookies and return an end session request URL.

OAuth Agent Cookies

The OAuth Agent issues an encrypted proxy cookie that transports access tokens to APIs. It also issues an encrypted session cookie that contains the refresh token and the ID token. Finally, a temporary cookie is used between /login/start and /login/end containing login-related values like the OAuth state and PKCE code_verifier. All cookies have the following properties and are issued to the token handler's exact domain:

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.
Path=/Cookies are available to all APIs within the host domain.

The OAuth Agent always encrypts tokens into cookies, so that, for example, a user cannot extract the underlying tokens using browser tools. This forces the SPA to always use its backend for frontend as the entry point for API requests, which enforces any web-specific security checks.

Cookie Limits

The OAuth Agent should ensure that cookies do not exceed maximum sizes that result in technical errors when the SPA sends them. Cookies must not exceed browser limits (of 4KB per cookie) or the default maximum HTTP header size (often 4KB) allowed by the API gateway.

Cookie sizes can be influenced by the algorithm used to sign tokens. If underlying tokens are JWTs, cookies contain the digital signatures of the access token and refresh token. For RS256 JWTs with an average-sized payload, each cookie is around 1.5KB. Therefore each cookie remains comfortably under the cookie limits defined in the RFC 2109 specification.

If you use especially large OAuth tokens, you are still unlikely to exceed the browser cookie size, but you may need to extend your gateway's HTTP header limits for token handler endpoints.

SPA Cookie Size

You can reduce cookie sizes by using opaque access and refresh tokens, or by using a modern and efficient JWT algorithm, such as ES256 or EdDSA.

Stateless Solution

By storing underlying tokens in cookies, the OAuth Agent acts as a stateless API, that receives all state in the cookies of incoming requests. You do not need to manage any backend storage. You can also cluster instances of the OAuth Agent and any instance can process any OAuth request.

Login User Experience

The OAuth Agent never abruptly redirects the SPA, and instead returns OpenID Connect URLs to the SPA, so that the location and page state can be saved before a redirect and then restored after. In addition there are no dropped cookie problems during navigation, since cookies are not needed during web navigation requests. All of this leads to the type of behavior you would expect from an SPA, where the frontend code is in control of the UX.

OAuth Proxy

Once user authentication has completed, the SPA makes API requests to access data, sending the proxy cookie that contains the access token. The OAuth Proxy enforces web-specific security, decrypts the cookie and forwards the access token to APIs. This work just involves general security plumbing and is best kept out of your API code.

Instead, since APIs are already hosted behind an API gateway as a security best practice, the gateway is the simplest place to deal with them. This is done using plugins, which perform well, since they do not require the gateway to make any additional remote calls. These plugins deliver a JWT access token to your microservices.

An example end-to-end API flow is illustrated below, where an OAuth Proxy plugin decrypts a cookie and updates the HTTP authorization header with the access token. If you use opaque access tokens, a Phantom Token Plugin can also run, to read the opaque access token from the authorization header, introspect it and then update the authorization header with a JWT.

Illustration of an end-to-end API Flow

Access Token Usage

The proxy cookie sent by the SPA is short-lived, with a lifetime such as 15 minutes. If the cookie header is somehow stolen it could only be replayed by a malicious party for the lifetime of the access token.

The SPA can call the /refresh endpoint of the OAuth Agent before it expects the access token to expire, or when it receives a 401 response from an API. If the SPA has multiple views that call APIs concurrently, the SPA can synchronize token refresh to prevent any concurrency errors with single use refresh tokens.

Standard OAuth Client Behavior

Although the SPA uses a cookie to transport its access token, it maintains the standard client-friendly OAuth behaviors. The SPA uses a short-lived API message credential and is in full control of access token refresh, for best usability and reliability.

Cross Site Request Forgery Protections

The token handler pattern uses Cross Site Request Forgery (CSRF) protections from the OAuth for Browser-Based Applications document. The main web-specific security requirement is to ensure that untrusted origins cannot use the genuine SPA's cookies. Both the OAuth Agent and OAuth Proxy restrict access to the SPA's precise origin using the following techniques:

  • Same Site Cookies: If a malicious site outside the SPA's parent domain tries to use the cookie, the browser will not send it, since SameSite=strict cookies cannot be sent from other sites.
  • Cross-Origin Resource Sharing (CORS): If a malicious origin inside the SPA's parent domain tries to use the cookie, the browser will not send it, since CORS restricts access to the SPA's precise origin.

The OAuth agent and OAuth proxy can force the browser to run preflight requests when CORS applies. These HTTP OPTIONS requests occur the first time the SPA calls a particular endpoint, after which the browser caches the response. If a malicious origin inside the SPA's parent domain tries to use the cookie, its preflight request is unauthorized and the browser does not send the cookie.

The OAuth Agent and OAuth Proxy must require the SPA to send a fixed header such as token-handler-version=1 on every request, to prevent a malicious origin from sending the cookie in a CORS Simple Request that bypasses the preflights to circumvent precise origin checks.

Web Architecture Styles

In the token handler pattern, your web host is always lightweight and never needs to deal with the intricacies of cookie security. There are a few different web architecture styles you should be aware of.

Unsecured Apps

Data in unsecured apps like blogs or news sites can be pre-rendered to static content before deployment. Technology stacks such as NEXT.js enable this, and you can take full advantage of the technology to deliver a high-performance site with good Search Engine Optimization (SEO).

Yet an OAuth secured web application has different characteristics. The data to display is managed by APIs, requires secure access, varies by user and is not reachable by search engines. You therefore need a different architecture design.

Secured SPAs

In a pure SPA, the web host only delivers static content. The web host never needs to receive secure cookies, since these are only used on the API side of the architecture. Typically, you design a simple first page with a fast time to first render. Subsequent user navigations result in API requests and fast updates that avoid reloading the entire page.

Various techniques can be used to optimize performance. You can use HTTP cache headers to serve already downloaded static resources from the browser cache. You can use compression and bundle splitting to limit the size of downloads, and a content delivery network to reduce their latency. You can also use libraries that pre-fetch and cache API requests.

When a cookie-secured SPA loads, it cannot know if the user is authenticated and must ask the server for the login status. You can apply optimizations to reduce the frequency of these calls. For example, after login you might store an isLoggedIn=true property in local storage and use that value to avoid OAuth Agent calls when opening new browser tabs.

Secured Hybrid Apps

In some cases your web app may have characteristics of both an SPA and a website. For example, in NEXT.js the web host might deliver a main page that pre-renders some unsecured data to HTML as an alternative way of enabling a fast time to first render. The frontend no longer needs to ask the server if the user is authenticated.

To enable this with the token handler pattern you need to ensure that an OAuth Agent cookie flows to the web host, to inform it that the user is authenticated. You might do so using a Same Domain Deployment. The web host can then check for the presence of the cookie and inform the frontend of the authentication state by returning a particular pre-rendered response.

Secured Websites

The token handler pattern is not meant to provide a website solution, but could potentially be used to migrate an older website solution to an SPA. Traditionally, websites read secured data from a database and pre-render it to HTML before returning it to the browser. This is sometimes called Server Side Rendering (SSR).

Yet pre-rendering secured data from APIs can add complexity to your web host and may be less efficient than direct calls from the browser to APIs. If you want to use SSR for secured API data, the web host may need to decrypt cookies to get access tokens to forward to APIs, and will also need to manage token expiry events. In this case, implementing the OAuth Agent logic in your web host, or using a website stack, is likely to provide a simpler solution than the token handler pattern.

Micro-Frontends

The token handler pattern separates web and API concerns to reduce complexity and give you choices. It is possible to deploy multiple SPAs within the same web domain, with seamless navigation between them due to shared cookies. For example, you could deploy secured and unsecured apps at different paths, or you could split a large web application into micro-frontends to keep your SPA code sizes manageable.

Cookies issued by token handler components are sharable by all secured micro-frontends within a web domain, even if frontends use different technology stacks. Yet we only recommend cookie sharing when all apps are for the same business area. The OAuth and SameSite Cookies article provides further recommendations on scaling the use of cookies across applications.

Developer Experience

The token handler pattern can enable frontend developers to run code for just the frontend, and to work solely in a browser-based technology, such as React or Angular. This provides maximum productivity, with static content built locally and no need to run a complex web backend.

Due to the use of the token handler pattern, the SPA's OpenID Connect and API calls follow current best practices for browser based apps, but of course this does not make your app fully secure. Organizations must also follow a secure 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.

Professional Implementations

At Curity, we provide expert implementations of the OAuth Agent and OAuth Proxy, which will work with any standards-based authorization server. This enables companies to simply configure or deploy a working solution, without needing to build their own cookie integrations from scratch. For more information about Curity's solution, see the following resources:

Conclusion

Using cookies optimally in an SPA is challenging, but needs to be done to protect against browser threats. The best current way to manage this is to use the token handler pattern, which improves security without losing any of the benefits of an SPA architecture.

Photo of Gary Archer

Gary Archer

Product Marketing Engineer at Curity

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