/images/resources/code-examples/code-examples-nginx.jpg

NGINX Phantom Token Module

On this page

An NGINX module that introspects access tokens according to RFC 7662, producing a Phantom Token that can be forwarded to back-end APIs and Web services. This module, when enabled, filters incoming requests, denying access to those which do not have a valid OAuth access token presented in an Authorization header. From this header, the access_token is extracted and introspected using the configured endpoint. The JWT obtained from the introspection endpoint replaces the access token in the header of the request that is forwarded by NGINX to the back-end. If the token is not valid or absent, no request to the back-end is made and the caller is given a 401, unauthorized, error.

Integrated Behavior

In this tutorial we will show how to quickly integrate the module, to provide an end to end solution with the following behavior:

  • An OAuth Client will get an opaque access token
  • We will send the opaque access token to an API via NGINX
  • The opaque token will be introspected to get a JWT, which will be forwarded to the API
  • The API will successfully validate the JWT and return some data

Run the Default NGINX Docker Image

First ensure that Docker Desktop is installed locally as a prerequisite. Next create a local folder called nginx and add a minimal docker-compose.yml file:

dockerfile
123456
version: '3.8'
services:
my_nginx:
image: nginx:1.19.5-alpine
ports:
- 8080:80

Then run NGINX with the following command, which will download the docker image:

bash
1
docker-compose --force-recreate

At this stage you can browse to http://localhost:8080 locally to see a working NGINX home page:

NGINX Welcome

Next copy the following two files locally, which we will customize in order to integrate the module:

123
CONTAINER_ID=$(docker ps | grep nginx | awk '{print $1}')
docker cp $CONTAINER_ID:/etc/nginx/conf.d/default.conf ./default.conf.template
docker cp $CONTAINER_ID:/etc/nginx/nginx.conf ./nginx.conf

Build a Custom Docker Image

Download the phantom token library for Alpine Linux 1.19.5 from the GitHub Releases Page and save it to the local nginx folder. Then create a custom Dockerfile to copy in the module:

dockerfile
12
FROM nginx:1.19.5-alpine
COPY alpine.ngx_curity_http_phantom_token_module_1.19.5.so /usr/lib/nginx/modules/ngx_curity_http_phantom_token_module.so

We can then build the custom Docker image with the following command:

bash
1
docker build -f Dockerfile -t my_nginx:1.19.5 .

In total we should now have the following 5 files in the local nginx folder:

NGINX Files

Deploy the Phantom Token Module

First add a line to the main nginx.conf file to load the phantom token module:

nginx
1234567
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
load_module modules/ngx_curity_http_phantom_token_module.so;

Next update the default.conf.template file, both to define an API route, and also to configure the phantom token module:

nginx
123456789101112131415161718192021222324252627
proxy_cache_path cache levels=1:2 keys_zone=api_cache:10m max_size=10g inactive=60m use_temp_path=off;
server {
server_name ${NGINX_HOST};
listen ${NGINX_PORT};
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
location /api {
proxy_pass http://${API_HOST}:${API_PORT}/api;
phantom_token on;
phantom_token_client_credential ${INTROSPECTION_CLIENT_ID} ${INTROSPECTION_CLIENT_SECRET};
phantom_token_introspection_endpoint curity;
phantom_token_scopes read;
}
location curity {
proxy_pass ${INTROSPECTION_ENDPOINT};
proxy_cache_methods POST;
proxy_cache api_cache;
proxy_cache_key $request_body;
proxy_ignore_headers Set-Cookie;
}
}

The values in curly brackets are environment variables, which we will supply by updating the docker-compose.yml file. This enables the configuration to be defined once and then deployed down a pipeline:

docker
1234567891011121314151617
version: '3.8'
services:
my_nginx:
image: my_nginx:1.19.5
ports:
- 8080:80
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./default.conf.template:/etc/nginx/templates/default.conf.template
environment:
NGINX_HOST: localhost
NGINX_PORT: 80
API_HOST: host.docker.internal
API_PORT: 3000
INTROSPECTION_CLIENT_ID: api-gateway-client
INTROSPECTION_CLIENT_SECRET: Password1
INTROSPECTION_ENDPOINT: http://host.docker.internal:8443/oauth/v2/oauth-introspect

We can now redeploy NGINX, this time with the phantom token module activated:

bash
1
docker-compose up --force-recreate

Finally, double check that the configuration has been correctly applied from the template by running these commands:

bash
12
CONTAINER_ID=$(docker container ls | grep my_nginx | awk '{print $1}')
docker exec -it $CONTAINER_ID sh -c "cat /etc/nginx/conf.d/default.conf"

Output should look similar to the following and your NGINX + phantom token setup is now complete:

nginx
123456789101112131415161718192021222324252627
proxy_cache_path cache levels=1:2 keys_zone=api_cache:10m max_size=10g inactive=60m use_temp_path=off;
server {
server_name localhost;
listen 80;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
location /api {
proxy_pass http://host.docker.internal:3000/api;
phantom_token on;
phantom_token_client_credential api-gateway-client Password1;
phantom_token_introspection_endpoint curity;
phantom_token_scopes read;
}
location curity {
proxy_pass http://host.docker.internal:8443/oauth/v2/oauth-introspect;
proxy_cache_methods POST;
proxy_cache api_cache;
proxy_cache_key $request_body;
proxy_ignore_headers Set-Cookie;
}
}

Run an API on the Host

We will run an API on the host, and call it via port 8080, so that the request is routed via NGINX. The phantom token module will introspect incoming access tokens to get a JWT, then forward them to the real API endpoint, at http://localhost:3000/api/data.

If you prefer, you can choose a different technology from the API Code Examples, or plug in your own API. This tutorial will use the Kotlin Example API, which you can get with these commands:

bash
1
git clone https://github.com/curityio/kotlin-api-jwt-validation

Next edit the API's api.properties file to ensure that it points to your instance of the Curity Identity Server:

1234
port=3000
jwks_endpoint=http://localhost:8443/oauth/v2/oauth-anonymous/jwks
issuer_claim=http://localhost:8443/oauth/v2/oauth-anonymous
audience_claim=api.example.com

Then run the API, and this particular sample requires Maven and Java 8+ to run:

bash
123
cd kotlin-api-jwt-validation
mvn package
java -jar target/secureapi-1.0-SNAPSHOT.jar

The docker container will route requests to these URLs on the host, and for this to work we configured a host name of host.docker.internal, which points to localhost on the host computer:

URLDescription
http://localhost:8443/oauth/v2/oauth-introspectThe introspection endpoint of the Curity Identity Server
http://localhost:3000/api/dataThe sample API endpoint which will receive a digitally signed JWT

We can now call the sample API with the following command, though we are not supplying a real token to NGINX, so the request will fail OAuth validation:

bash
1
curl -i -H "Authorization: Bearer abc123" http://localhost:8080/api/data

As expected, this results in an introspection failure, and you can understand the cause from the phantom token module's log output, which is routed to stderr:

Introspect 401

Test the End to End Flow

Next we need to configure a simple OAuth client in order to get an opaque access token, and you can use a client of your choice. We will use a simple Client Credentials client that uses a read scope. An additional introspection client is also required, and is used by the phantom token module in the NGINX docker container. The combined configuration XML for both clients is shown below:

xml
123456789101112131415161718192021222324
<client>
<id>api-client</id>
<client-name>api-client</client-name>
<secret>Password1</secret>
<audience>api.example.com</audience>
<scope>read</scope>
<capabilities>
<client-credentials/>
</capabilities>
<use-pairwise-subject-identifiers>
<sector-identifier>api-client</sector-identifier>
</use-pairwise-subject-identifiers>
</client>
<client>
<id>api-gateway-client</id>
<client-name>api-gateway-client</client-name>
<secret>Password1</secret>
<capabilities>
<introspection/>
</capabilities>
<use-pairwise-subject-identifiers>
<sector-identifier>api-gateway-client</sector-identifier>
</use-pairwise-subject-identifiers>
</client>

We can now get an opaque access token via the following simple curl request:

bash
123
curl -u 'api-client:Password1' -X POST http://localhost:8443/oauth/v2/oauth-token \
-d grant_type=client_credentials \
-d scope=read

Take the access token from the response and then run a second curl request to send a valid token through NGINX to our API:

bash
1
curl -H "Authorization: Bearer 0672b353-8fc0-4d02-be43-e20ccffd1f62" http://localhost:8080/api/data

You should now see a successful end to end flow, where the Kotlin API validates the JWT and echoes back some scopes and claims contained in it:

Introspection Success

For debugging purposes it can also be useful to test the introspection manually from the host, with the following command. Notice that a special form of OAuth introspection is used, with accept=application/jwt, so that the response is a digitally signed JWT and not a JSON object:

bash
1234
curl -u 'api-gateway-client:Password1' \
-H "accept: application/jwt" \
-X POST http://localhost:8443/oauth/v2/oauth-introspect \
-d token=0672b353-8fc0-4d02-be43-e20ccffd1f62

Single Page Applications

For details on how to integrate the phantom token module into API solutions for Single Page Applications (SPA), and to deal with Cross Origin Resource Sharing (CORS) during requests to APIs, see the following links:

Conclusion

The phantom token pattern can be quickly implemented with NGINX by plugging in the Curity module. 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