Integrating with Nginx

Integrating with Nginx

tutorials

Overview

Curity provides 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. In this tutorial we will show how to quickly integrate the module, producing an end to end solution:

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

docker cp my_nginx:/etc/nginx/conf.d/default.conf ./default.conf.template
docker cp my_nginx:/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

Handle CORS Requests

If you are making cross domain API requests from an OAuth secured Single Page Application, you will also need to deal with pre flight OPTIONS requests. These should not be forwarded to the phantom token module, since they will never include an access token. Instead it is simplest to configure NGINX to return CORS headers for trusted web origins:

location /api {
            
    add_header 'Access-Control-Allow-Origin' '${TRUSTED_WEB_ORIGIN}' always;
    add_header 'Access-Control-Allow-Methods' 'OPTIONS, GET' always;
    add_header 'Access-Control-Allow-Headers' '*' always;
    if ($request_method = 'OPTIONS') {
        return 204;
    }
        
    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;
}

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.

Let’s Stay in Touch!

Get the latest on identity management, API Security and authentication straight to your inbox.

Keep up with our latest articles and how-tos using RSS feeds