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:
Scenario | Deployment Folder Location |
---|---|
An SPA that uses an external authorization server | deployments/external |
An SPA that uses the Curity Identity Server as its authorization server | deployments/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.
Component | Description |
---|---|
SPA | The JavaScript application that runs in the browser |
Web Host | The component from which the SPA downloads its static content |
API Gateway | The example deployments host all backend components behind an API gateway |
Authorization Server | The main security component that manages user authentication and token issuance for the SPA |
OAuth Agent | Implements cookie issuing for the SPA, and the main code for its OAuth flows |
OAuth Proxy | An API gateway plugin used during API requests to process cookies and forward access tokens to APIs |
API | An 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:
- The SPA downloads static content from its web host.
- The SPA runs a code flow to authenticate the user.
- After the user authenticates, an authorization response is returned to the SPA containing an authorization code.
- The SPA completes its code flow by calling the OAuth Agent.
- The OAuth Agent sends the authorization code to the authorization server.
- The authorization server returns tokens to the OAuth Agent.
- The OAuth Agent encrypts tokens into cookies that it issues to the browser.
- The SPA makes API requests by sending a secure cookie to the API gateway.
- The OAuth Proxy decrypts the cookie and forwards a JWT access token to the API.
- 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.
{"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:
{"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:
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.
api-gateway:image: apigateway:1.0.0hostname: apigateway-internalports:- 80:3001volumes:- ./apigateway/kong.yml:/usr/local/kong/declarative/kong.ymlenvironment: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.
- name: authorization-serverurl: http://login-internal:8080routes:- name: authorization-server-routehosts:- login.example.compaths:- /
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.
{"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.
curity-idsvr:image: curity/idsvr-unstable:9.2.0-d6e393af32hostname: login-internalextra_hosts:- login.example.com:host-gatewayports:- 6749:6749volumes:- ../../license.json:/opt/idsvr/etc/init/license/license.json- ./idsvr/config-backup.xml:/opt/idsvr/etc/init/config.xmlenvironment:ADMIN: 'true'LOGGING_LEVEL: 'INFO'IDSVR_BASE_URL: 'http://login.example.com'curity-data:image: postgres:16.3hostname: dbservervolumes:- ./idsvr/data-backup.sql:/docker-entrypoint-initdb.d/data-backup.sqlenvironment: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.
<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:
<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:
- name: example-apiurl: http://api-internal:3001routes:- name: example-api-routehosts:- bff.product.examplepaths:- /apiplugins:- name: corsconfig:origins:- http://www.product.examplecredentials: truemax_age: 86400- name: oauth-proxyconfig:cookie_key: |------BEGIN ENCRYPTED PRIVATE KEY-----MIH0MF8GCSqGSIb3DQEFDTBSMDEGCSqGSIb3DQEFDDAkBBA9BLL4kbxiN4dOlIZEnxxsAgIIADAMBggqhkiG9w0CCQUAMB0GCWCGSAFlAwQBKgQQ72a5HguvLkDUto5QgLjuygSBkCOR6pSeXPp1ym5f+oPwYzT99EXbyxELUI43r01IcfY1i8Ib+rPbHB0KabRjKb/MnJXaRVS3iaQDYmTLK4SF6YcA3wRJtUZWTeCr79PbzkAootHyRjrYT6jNhOm/DLJHiRtNFyRTX+E9r9uMnYACmv6o0lsWScb0NrwPlyW3Ft14ayibtRvCnpEaCB3FgLlR9w==-----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.
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:
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:
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:
Domain | Value |
---|---|
Web Origin | www.product.example |
Backend for Frontend | bff.product.example |
Authorization Server | login.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.
Domain | Value |
---|---|
Web Origin | myapp.com |
Backend for Frontend | api.myapp.com |
Authorization Server | login.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.
Domain | Value |
---|---|
Web Origin | www.product.example |
Backend for Frontend | www.product.example |
Authorization Server | login.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.
cd spanpm 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:
{"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 Values | New Values |
---|---|
http://www.product.example | https://www.product.example |
http://bff.product.example | https://bff.product.example |
http://login.example.com | https://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:
api-gateway:image: apigateway:1.0.0hostname: apigateway-internalports:- 443:3001volumes:- ./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.crtenvironment: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