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-alpineports:- 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:
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.templatedocker 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-alpineCOPY 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:
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.5ports:- 8080:80volumes:- ./nginx.conf:/etc/nginx/nginx.conf- ./default.conf.template:/etc/nginx/templates/default.conf.templateenvironment:NGINX_HOST: localhostNGINX_PORT: 80API_HOST: host.docker.internalAPI_PORT: 3000INTROSPECTION_CLIENT_ID: api-gateway-clientINTROSPECTION_CLIENT_SECRET: Password1INTROSPECTION_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=3000jwks_endpoint=http://localhost:8443/oauth/v2/oauth-anonymous/jwksissuer_claim=http://localhost:8443/oauth/v2/oauth-anonymousaudience_claim=api.example.com
Then run the API, and this particular sample requires Maven and Java 8+ to run:
cd kotlin-api-jwt-validationmvn packagejava -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:
URL | Description |
---|---|
http://localhost:8443/oauth/v2/oauth-introspect | The introspection endpoint of the Curity Identity Server |
http://localhost:3000/api/data | The 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:
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:
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.
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