NGINX Phantom Token Module

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:

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:

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:

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:

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:

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:

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:

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:

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:

docker-compose up --force-recreate

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

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:

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:

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:

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:

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:

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:

<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:

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:

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:

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.