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

Token Handler Deployment Example

On this page

The SPA using Token Handler code example explains the basic steps to run the code example. This tutorial dives deeper into the deployment details, so that you understand the moving parts of the solution and the key configuration settings. Once understood, you can adapt the deployment to any other deployed environment.

Deployment Scenarios

First, clone the GitHub repository at the top of this page. Open its folder in your preferred editor and locate the folder that matches your deployment scenario:

ScenarioDeployment Folder Location
An SPA that uses an external authorization serverdeployments/external
An SPA that uses the Curity Identity Server as its authorization serverdeployments/curity

Deployed System

The deployed system consists of a number of components, whose roles are summarized in the following table. You can find these components listed in the docker-compose.yml file within the deployment folder.

ComponentDescription
SPAThe JavaScript application that runs in the browser
Web HostThe component from which the SPA downloads its static content
API GatewayThe example deployments host all backend components behind an API gateway
Authorization ServerThe main security component that manages user authentication and token issuance for the SPA
OAuth AgentImplements cookie issuing for the SPA, and the main code for its OAuth flows
OAuth ProxyAn API gateway plugin used during API requests to process cookies and forward access tokens to APIs
APIAn example API through which the SPA accesses its business data

SPA Flow

In order for the SPA to authenticate users and call APIs with secure cookies, the following flow takes place:

SPA Flow
  1. The SPA downloads static content from its web host.
  2. The SPA runs a code flow to authenticate the user.
  3. After the user authenticates, an authorization response is returned to the SPA containing an authorization code.
  4. The SPA completes its code flow by calling the OAuth Agent.
  5. The OAuth Agent sends the authorization code to the authorization server.
  6. The authorization server returns tokens to the OAuth Agent.
  7. The OAuth Agent encrypts tokens into cookies that it issues to the browser.
  8. The SPA makes API requests by sending a secure cookie to the API gateway.
  9. The OAuth Proxy decrypts the cookie and forwards a JWT access token to the API.
  10. The API validates the JWT and uses its claims to implement authorization.

In the example deployment, both the authorization server and backend for frontend (BFF) components are hosted behind the same API gateway. The gateway processes incoming requests based on their external host names. When the SPA sends requests to https://login.example.com the gateway routes requests to the authorization server. When the SPA sends requests to https://bff.product.example they are routed to token handler components.

Configuration Settings

The deployment folder contains the configuration for each component. The main settings you need to configure are explained in the following sections.

SPA

The SPA uses a JSON configuration file, which it downloads from its web host at runtime. The configuration file is located at spa/config.json within the deployment folder and contains the following content. Primarily this is the backend for frontend base URL, which will vary for the stages of your deployment pipeline. In the SPA's code, these URLs and paths are used by the  OAuthAgentClient and ApiClient classes.

json
123456
{
"bffBaseUrl": "http://bff.product.example",
"oauthAgentPath": "/oauthagent/example",
"oauthUserinfoPath": "/oauthuserinfo",
"apiPath": "/api"
}

Web Host

Since the deployment example is Docker-based, for a development computer, the web host is deployed as a Docker container and runs behind the API gateway. It uses the following main configuration settings in its webhost/config.json file within the deployment folder:

json
1234
{
"port": 3001,
"bffBaseUrl": "http://bff.product.example",
}

You can use any kind of web host with the token handler pattern, such as a content delivery network. The web host's content security policy should allow the SPA to connect to its backend for frontend URL. The code for the example web host does this with the following code:

javascript
1234567891011
let policy = "default-src 'none';";
policy += " script-src 'self';";
policy += ` connect-src 'self' ${configuration.bffBaseUrl};`;
policy += " child-src 'self';";
policy += " img-src 'self';";
policy += " style-src 'self' https://cdn.jsdelivr.net;";
policy += ` font-src 'self';`;
policy += " object-src 'none';";
policy += " frame-ancestors 'none';";
policy += " base-uri 'self';";
policy += " form-action 'self';";

API Gateway

The example uses the Kong open-source API gateway, which is deployed as a custom docker image that contains the Curity Kong OAuth Proxy Plugin. In the docker-compose.yml file the plugin is activated using the KONG_PLUGINS environment variable.

yaml
12345678910111213
api-gateway:
image: apigateway:1.0.0
hostname: apigateway-internal
ports:
- 80:3001
volumes:
- ./apigateway/kong.yml:/usr/local/kong/declarative/kong.yml
environment:
KONG_DATABASE: 'off'
KONG_DECLARATIVE_CONFIG: '/usr/local/kong/declarative/kong.yml'
KONG_PROXY_LISTEN: '0.0.0.0:3001'
KONG_LOG_LEVEL: 'info'
KONG_PLUGINS: 'bundled,cors,oauth-proxy'

Within the deployed system, components interact within a Docker network, using internal URLs. For example, the authorization server uses an internal host name of login-internal. When the SPA makes a request to http://login.example.com, the gateway receives the request and routes to the internal URL. You can see the API gateway routes in the apigateway/kong.yml file.

yaml
12345678
- name: authorization-server
url: http://login-internal:8080
routes:
- name: authorization-server-route
hosts:
- login.example.com
paths:
- /

In some deployments, the authorization server could be a platform as a service (PaaS) solution such as Microsoft Entra ID. In this case, components would route to the authorization server using public URLs instead of internal URLs.

API

The example API uses a JSON configuration file, located at api/config.json within the deployment folder, with the following main content. Any OAuth-secured API includes settings like these so that it can validate JWT access tokens according to best practices. In the example deployment, the JWKS URI is an internal location within the Docker network.

json
123456
{
"port": 3001,
"jwksUri": "http://login-internal:8443/oauth/v2/oauth-anonymous/jwks",
"issuer": "http://login.example.com/oauth/v2/oauth-anonymous",
"audience": "api.example.com",
}

Authorization Server

The docker-compose.yml file includes a Docker deployment for the authorization server. If using an external authorization server, a basic deployment of Keycloak is used, along with a SQL database containing a pre-shipped user account with which you can test logins. A similar deployment is used when you use the Curity Identity Server as the authorization server.

yaml
123456789101112131415161718192021222324
curity-idsvr:
image: curity/idsvr-unstable:9.2.0-d6e393af32
hostname: login-internal
extra_hosts:
- login.example.com:host-gateway
ports:
- 6749:6749
volumes:
- ../../license.json:/opt/idsvr/etc/init/license/license.json
- ./idsvr/config-backup.xml:/opt/idsvr/etc/init/config.xml
environment:
ADMIN: 'true'
LOGGING_LEVEL: 'INFO'
IDSVR_BASE_URL: 'http://login.example.com'
curity-data:
image: postgres:16.3
hostname: dbserver
volumes:
- ./idsvr/data-backup.sql:/docker-entrypoint-initdb.d/data-backup.sql
environment:
POSTGRES_USER: 'postgres'
POSTGRES_PASSWORD: 'Password1'
POSTGRES_DB: 'idsvr'

Note that the authorization server is configured with an external URL that it returns to the browser when redirecting it to authentication screens.

OAuth Agent

The main OAuth Agent configuration is explained in the Create a Token Handler tutorial. When the Curity Identity Server is used as the authorization server, the OAuth Agent configuration is located in the idsvr/config-backup.xml file within the deployment folder. The OAuth Agent derives most settings it needs from the referenced OAuth client.

xml
123456789101112131415
<application>
<id>example</id>
<token-handler xmlns="https://curity.se/ns/conf/apps/tokenhandler">
<single-page-application-base-url>http://www.product.example</single-page-application-base-url>
<internal-client>
<client-id>spa-client</client-id>
</internal-client>
<proxy-keystore>
<id>oauth-agent-example-publickey</id>
</proxy-keystore>
<proxy-type>kong</proxy-type>
<cookie-prefix>th-</cookie-prefix>
</token-handler>
</application>
</applications>

When using an external authorization server, the OAuth Agent uses the following configuration settings, in the oauthagent/config-backup.xml file within the deployment folder:

xml
12345678910111213141516171819202122232425
<application>
<id>example</id>
<token-handler xmlns="https://curity.se/ns/conf/apps/tokenhandler">
<single-page-application-base-url>http://www.product.example</single-page-application-base-url>
<external-client>
<client-id>spa-client</client-id>
<client-secret>xyNcensqT1FG0Zs0CMQtC1dDW1Vw1Luz</client-secret>
<scope>openid</scope>
<scope>profile</scope>
<authorization-endpoint>http://login.example.com/realms/example/protocol/openid-connect/auth</authorization-endpoint>
<token-endpoint>http://login-internal:8080/realms/example/protocol/openid-connect/token</token-endpoint>
<logout>
<logout-endpoint>http://login.example.com/realms/example/protocol/openid-connect/logout</logout-endpoint>
<post-logout-redirect-uri>http://www.product.example/</post-logout-redirect-uri>
</logout>
<token-issuer>http://login.example.com/realms/example</token-issuer>
<redirect-uri>http://www.product.example/callback</redirect-uri>
</external-client>
<proxy-keystore>
<id>oauth-agent-example-publickey</id>
</proxy-keystore>
<cookie-prefix>th-</cookie-prefix>
<proxy-type>kong</proxy-type>
</token-handler>
</application>

Any OAuth endpoints that the OAuth Agent returns to the browser must be configured using the external URL of the authorization server. The token endpoint can be an internal URL if applicable. Note also that any secret values, like the OAuth Agent's client secret, are protected by the Curity configuration system.

API Gateway Plugins

Next, look at routes from the SPA to the example API (or the OpenID Connect userinfo endpoint) in the apigateway/kong.yml file, to understand plugins that run during API requests. First, the CORS plugin is used to allow the browser to send cookies in cross origin requests to the backend for frontend. Next, the OAuth Proxy Plugin is configured, to decrypt the proxy cookie, set the HTTP authorization header with the access token, then forward the request to internal API endpoints:

yaml
123456789101112131415161718192021222324252627
- name: example-api
url: http://api-internal:3001
routes:
- name: example-api-route
hosts:
- bff.product.example
paths:
- /api
plugins:
- name: cors
config:
origins:
- http://www.product.example
credentials: true
max_age: 86400
- name: oauth-proxy
config:
cookie_key: |-
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIH0MF8GCSqGSIb3DQEFDTBSMDEGCSqGSIb3DQEFDDAkBBA9BLL4kbxiN4dOlIZE
nxxsAgIIADAMBggqhkiG9w0CCQUAMB0GCWCGSAFlAwQBKgQQ72a5HguvLkDUto5Q
gLjuygSBkCOR6pSeXPp1ym5f+oPwYzT99EXbyxELUI43r01IcfY1i8Ib+rPbHB0K
abRjKb/MnJXaRVS3iaQDYmTLK4SF6YcA3wRJtUZWTeCr79PbzkAootHyRjrYT6jN
hOm/DLJHiRtNFyRTX+E9r9uMnYACmv6o0lsWScb0NrwPlyW3Ft14ayibtRvCnpEa
CB3FgLlR9w==
-----END ENCRYPTED PRIVATE KEY-----
cookie_key_pass: default

For the curity deployment scenario, the authorization server issues opaque access tokens. Therefore, the gateway also configures a phantom token plugin to run after the OAuth Proxy plugin. This plugin introspects the access token and makes a further update to the request's HTTP authorization header, to replace the opaque access token with a JWT access token.

API Flow

Cryptographic Keys

The deployment includes a fixed encrypted private key that is included in the OAuth Proxy. The corresponding public key is included in the OAuth Agent configuration. This was created according to the instructions in the Create a Token Handler tutorial. For further details on configuring keys using the Curity configuration system, see the configuration management tutorials.

Troubleshoot Errors

When getting started with the token handler pattern it is common to make configuration mistakes. In this case, the OAuth Agent and OAuth Proxy return error responses to the SPA. These components also write logs containing error details. To troubleshoot and gain further insight into errors, you can view logs for the OAuth Agent using a command of this form:

bash
12
OAUTH_AGENT_CONTAINER_ID=$(docker ps | grep oauth-agent | awk '{print $1}')
docker logs -f $OAUTH_AGENT_CONTAINER_ID

Similarly you can use the following command to view logs for the API gateway, which includes any log output from plugins:

bash
12
API_GATEWAY_CONTAINER_ID=$(docker ps | grep apigateway | awk '{print $1}')
docker logs -f $API_GATEWAY_CONTAINER_ID

Adapt the Deployment

You can adapt the deployment in various ways by editing your deployment folder. Then re-run the deploy.sh script to recreate the Docker deployment with updated settings. This section walks through some example use cases.

Change External URLs

You can rename external URLs using a search and replace operation within your deployment folder. These are the default values that the SPA uses:

DomainValue
Web Originwww.product.example
Backend for Frontendbff.product.example
Authorization Serverlogin.example.com

For example, you could update to the following custom values, then run the SPA using a new custom URL. When changing domains, ensure that the web domain and the backend for frontend remain in the same parent site, so that cookies issued remain first-party and the browser allows the SPA to send them.

DomainValue
Web Originmyapp.com
Backend for Frontendapi.myapp.com
Authorization Serverlogin.mycompany.com

Same Origin Deployment

A special case of renaming URLs is a same origin deployment, where all backend for frontend components run in the web host's domain behind an API gateway. This enables you to avoid any CORS requests from the browser.

DomainValue
Web Originwww.product.example
Backend for Frontendwww.product.example
Authorization Serverlogin.example.com

SPA Developer Setup

You can easily adapt the example deployment to a development setup. To do so, first delete the web host component from your deployment folder's docker-compose.yml file. Then, do a search and replace in your deployment folder, to replace occurrences of www.product.example with www.product.example:3000.

Re-run the deployment, then run the following commands to run the SPA code example in development mode, with static content served by the webpack development server.

bash
12
cd spa
npm start

Then browse to http://www.example.com:3000 and change code in the example SPA to see fast feedback in the browser. This deployment demonstrates a pure SPA developer experience while also using HTTP-only SameSite=strict cookies. In development mode, the SPA's config.json file uses the following settings by default, and connects to backend components via the API gateway, which run on port 80:

json
123456
{
"bffBaseUrl": "http://bff.product.example",
"oauthAgentPath": "/oauthagent/example",
"oauthUserinfoPath": "/oauthuserinfo",
"apiPath": "/api"
}

HTTPS Setup

By default, the examples run using plain HTTP, to reduce infrastructure on a development workstation. You can update the example deployment and run HTTPS URLs in the browser. To do so, first inspect the files in the GitHub repository's certs folder. If required, change the domains listed in the server.ext file. Then run the create-certs.sh script to generate certificates and keys using the OpenSSL tool. Next, do a search and replace on external URLs within your deployment folder, to update them to use HTTPS, as in this example:

Old ValuesNew Values
http://www.product.examplehttps://www.product.example
http://bff.product.examplehttps://bff.product.example
http://login.example.comhttps://login.example.com

Then, update your docker-compose.yml file so that the API gateway listens on port 443 and uses the generated certificate files:

yaml
12345678910111213141516171819
api-gateway:
image: apigateway:1.0.0
hostname: apigateway-internal
ports:
- 443:3001
volumes:
- ./apigateway/kong.yml:/usr/local/kong/declarative/kong.yml
- ../../certs/example.ssl.key:/usr/local/share/certs/example.ssl.key
- ../../certs/example.ssl.crt:/usr/local/share/certs/example.ssl.crt
- ../../certs/example.ca.crt:/usr/local/share/certs/example.ca.crt
environment:
KONG_DATABASE: 'off'
KONG_DECLARATIVE_CONFIG: '/usr/local/kong/declarative/kong.yml'
KONG_PROXY_LISTEN: '0.0.0.0:3001 ssl'
KONG_SSL_CERT: '/usr/local/share/certs/example.ssl.crt'
KONG_SSL_CERT_KEY: './usr/local/share/certs/example.ssl.key'
KONG_LUA_SSL_TRUSTED_CERTIFICATE: './usr/local/share/certs/example.ca.crt'
KONG_LOG_LEVEL: 'info'
KONG_PLUGINS: 'bundled,cors,oauth-proxy'

You may also need to do some trust configuration so that the OAuth Agent trusts the authorization server's root certificate authority. Finally, ensure that your browser trusts the root certificate created at certs/example.ca.crt. For example, on macOS you can do so by importing the root certificate into your keychain. You can then browse to https://www.product.example, after which cookies issued by the OAuth Agent include the Secure property.

Conclusion

The token handler pattern separates concerns to enable cookie security to be externalized from your application level components. This tutorial explained how to compose a deployment and enable an end-to-end flow. Once you understand the logic, you can also adapt the deployment to meet your use cases. You can then continue to develop and run a pure 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