Integrating with OpenResty

On this page


The OpenResty gateway is a popular open source product that combines NGINX with an out-of-the-box LUA support. This tutorial shows how to quickly integrate an API gateway plugin that implements the Phantom Token Pattern. The tutorial demonstrates a working deployment, which runs the following workflow:

  • An OAuth client gets an opaque access token.
  • The client sends the opaque access token to an API via OpenResty.
  • The OpenResty plugin introspects the opaque token to get a JWT.
  • The plugin forwards the JWT access token to the API.
  • The API uses the JWT to implement its authorization.


To run this tutorial make sure you have the following:

Run OpenResty

Create a minimal docker-compose.yml file with the following contents:

image: openresty/openresty:
- 8080:80

Note that we use the fat version of the Alpine image as this one already contains all the packages needed for luarocks. Luarocks is a package manager for LUA that you will use later to install the additional plugin.

Run OpenResty with the following command, which will download the Docker image:

docker compose up --force-recreate

At this stage you can browse to http://localhost:8080 locally to see the standard OpenResty home page:

OpenResty Welcome

For now, stop the container using Control-C (Ctrl+C).

Build a Custom Image

You started the default version of OpenResty. Now you need to extend the Docker image with a LUA plugin that handles the token introspection. To automatically start the OpenResty container with a plugin you need to create a custom Docker image. Create an empty Dockerfile file, then copy the following contents:

FROM openresty/openresty:
RUN apk update && apk add git
RUN git config --global url."https://".insteadOf git:// && \
git config --global advice.detachedHead false
RUN luarocks install lua-resty-phantom-token 2.0.0

The luarocks command uses git to download the lua-resty-phantom-token plugin, so the preceding commands install and configure git.

The resulting Docker image deploys a file called phantom-token.lua inside a resty folder within the lua_package_path (this might be a location such as /usr/local/openresty/luajit/share/lua/5.1/resty). If you prefer, you can instead clone the LUA Phantom Token Plugin repository and manually copy the plugin/access.lua file to the same location (rename the file to phantom-token.lua).

Run the following command from the folder where you saved the Dockerfile file to build the custom image:

docker build -t openresty_with_phantom_token .

Configure OpenResty

Next, create a file default.conf in the same folder as your Docker Compose file. This is a configuration file that defines the HTTP behavior of OpenResty and that integrates the phantom token plugin. Paste the following to the configuration file:

error_log logs/error.log info;
lua_shared_dict phantom-token 10m;
server {
server_name localhost;
listen 8080;
location / {
root /usr/local/openresty/nginx/html;
index index.html index.htm;
location /api {
rewrite_by_lua_block {
local config = {
introspection_endpoint = 'https://curityserver:8443/oauth/v2/oauth-introspect',
client_id = 'introspection-client',
client_secret = 'Password1',
token_cache_seconds = 900,
verify_ssl = false
local phantomTokenPlugin = require 'resty.phantom-token'
proxy_pass http://host.docker.internal:3000/api;

Note lines 21-23, where you configure how OpenResty communicates with the Curity Identity Server's introspection endpoint. Make sure to tweak these settings if you use your own instance of the Curity Identity Server.

Do not disable SSL verification in production

In the above code example, in line 25 you can see that SSL verification is disabled. This is set so that OpenResty accepts self-issued certificates from the Curity Identity Server. This is just a convenience setting used for demo purposes. You should never use this setting in production environments.

In this example, OpenResty uses the Docker embedded DNS server to resolve these host names:

Base URLComponent
https://curityserver:8443The Curity Identity Server, running within the Docker network.
http://host.docker.internal:3000An example API, running on the host computer.

Deploy Components

Update the Docker Compose file to use the custom OpenResty image and the OpenResty configuration. Also, deploy the Curity Identity Server. The below example shows the complete Docker Compose file.

image: openresty_with_phantom_token:latest
hostname: openrestyserver
- 8080:8080
- ./default.conf:/etc/nginx/conf.d/default.conf
image: curity.azurecr.io/curity/idsvr:9.0.0
hostname: curityserver
- 6749:6749
- 8443:8443
- ./license.json:/opt/idsvr/etc/init/license/license.json
PASSWORD: 'Password1'

Make sure to put the Curity Identity Server license in a file called license.json in the same directory as the Docker Compose file.

Re-run the deployment command:

docker compose up --force-recreate

Next, open a second terminal window and run the following command to call the API via OpenResty, to verify that the plugin is running:

curl http://localhost:8080/api

This should result in a 401 unauthorized response, since the request does not contain a valid access token. The gateway does not forward it to the API.

"message":"Missing, invalid or expired access token"

Run an API on The Host

To see the phantom token plugin's work in action you need an API that will receive the JWT access token. You can run any API of your choice, but this tutorial provides a default Node.js API that runs on port 3000, with just enough code to verify that it received a correct JWT. The API also decodes the JWT (without validating it!) and returns the token payload. Create an empty api.js file and copy the following content.

const http = require('http');
const port = 3000;
const decodeJWTPayload = (jwt) => {
const payload = jwt.split('.')[1];
return JSON.parse(Buffer.from(payload.replaceAll('-', '+', '_', '/'), 'base64').toString('ascii'));
const server = http.createServer((req, res) => {
const auth = req.headers['authorization'];
let jwt = '[NONE]';
if (auth && auth.startsWith('Bearer ')) {
jwt = auth.substring(7);
const responseBody = {
message: `API Received JWT: ${jwt}`
if (jwt !== '[NONE]') {
responseBody.tokenPayload = decodeJWTPayload(jwt);
res.writeHead(200, { 'Content-Type': 'application/json' });
server.listen(port, () => {
console.log(`Server listening on port ${port}`);

Run the API with the following command:

node api.js

Configure OAuth Clients

If you deployed a fresh instance of the Curity Identity Server with the Docker Compose option, you need to run the basic setup wizard. See the tutorial First Configuration that summarizes the process. For this tutorial you can accept all the default settings.

You need to configure two clients in the Curity Identity Server:

  • A simple Client Credentials client. You will use the client to call the API with the opaque access token.
  • An introspection client. OpenResty uses this client for the phantom token plugin.

If you configured the Curity Identity Server with the default options from the configuration wizard, then you can directly import the configuration from below. Save it to an XML file, then, from the admin UI, open the Changes menu and select Upload. Choose the Merge option to merge the uploaded configuration with the existing one, then upload. Remember to commit the changes using the Commit option from the Changes menu.

If you run your own instance of the Curity Identity Server you can still merge the following configuration. Just make sure to update the token service ID in line 4.

<config xmlns="http://tail-f.com/ns/config/1.0">
<profiles xmlns="https://curity.se/ns/conf/base">
<type xmlns:as="https://curity.se/ns/conf/profile/oauth">as:oauth-service</type>
<authorization-server xmlns="https://curity.se/ns/conf/profile/oauth">

Test the End-to-End Flow

To test the flow end-to-end, you need to first get the opaque token from the authorization server, then call the API using that token. Use the following curl command to run the Client Credentials flow to get an opaque access token:

curl -k -u 'test-client:Password1' -X POST https://localhost:8443/oauth/v2/oauth-token \
-d grant_type=client_credentials \
-d scope=read

Test with OAuth Tools

You can also test any OAuth flows and API calls with OAuth Tools. Read more about how to configure and use the tool in the Test Using OAuth Tools tutorial.

Once you have the access token, make an API call with the following command. Exchange the <opaque_access_token> string for the actual token value received in the previous response.

curl -H "Authorization: Bearer <opaque_token_value>" http://localhost:8080/api

The API request is routed via OpenResty, and the phantom token plugin introspects the opaque access token, then forwards a JWT to the API. The example API echos back the JWT and prints its payload. A real API would continue by validating the JWT, then working with scopes and claims to implement the API's authorization logic.

"message": "API Received JWT: eyJraWQiOiIxN...",
"tokenPayload": {
"jti": "7c1e6a18-666e-4467-86b7-f95f0cd50689",

Plugin Settings

The plugin offers some settings that you can use to tailor its behavior. Have a look at the README.md file in the plugin's repository to learn what are the options.


You can quickly integrate the phantom token pattern with OpenResty by using the Curity LUA plugin. Once this is working on a development computer it is then easy to use Docker in the same way to publish to deployed environments. Using the phantom token approach results in a secure solution, where no sensitive token details are exposed to internet clients.

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