NGINX OAuth Proxy Module

NGINX OAuth Proxy Module

Overview

The OAuth proxy module is used when Single Page Applications (SPA) that use the Token Handler Pattern call APIs. The main role of the module is to decrypt a secure cookie containing an access token, then forward the access token to a target API. The module can be deployed with an NGINX LUA based reverse proxy, and then configured against one or more API routes. This tutorial will show one way to run, deploy and test the plugin, using Docker.

Run NGINX via Docker

First create a docker-compose.yml file, with the following content, to use the Alpine open source version of NGINX:

version: '3.8'
services:
  my_nginx:
    image: nginx:1.21.3-alpine
    ports:
    - 8080:80

Then run the NGINX system in its default state, which will then serve its default Welcome to NGINX page at http://localhost:8080:

docker-compose up --force-recreate

Get the Module

Download URLs are available in the README page of the GitHub repository. For the Alpine version of NGINX the module at the following location should be used:

https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.0.0/alpine.ngx_curity_http_oauth_proxy_module_1.21.3.so

Configure the Module for an API Route

Next copy the configuration files for the module to the local computer:

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

Edit the main nginx.conf file to load the module, before the events directive:

user  nginx;
worker_processes  auto;
error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;

load_module modules/ngx_curity_http_oauth_proxy_module.so;
events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;
    sendfile        on;
    keepalive_timeout  65;
    include /etc/nginx/conf.d/*.conf;
}

Generate a 32 byte encryption key with the following command, which will result in 64 hex characters. In a real setup the same key would be deployed with the OAuth Agent API, which would create the cookies when a user of the SPA authenticates.

openssl rand 32 | xxd -p -c 64

Then update default.conf.template, to define an API route that uses the OAuth proxy module and the generated encryption key. In a real example the proxy-pass directive would route to a downstream API, but this example just echoes back the decrypted value of the access token:

server {
    server_name  localhost;
    listen       80;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    location /api {
        oauth_proxy on;
        oauth_proxy_cookie_name_prefix "example";
        oauth_proxy_encryption_key 4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50;
        oauth_proxy_trusted_web_origin "https://www.example.com";
        oauth_proxy_cors_enabled on;

        proxy_pass http://localhost/mock-api;
    }

    location /mock-api {
        add_header "content-type" "application/json";
        return 200 '{"message": "API was called successfully with an access token"';
    }
}

Full details about the configuration directives are available in the GitHub repository's README file, though they are also briefly summarized below:

DirectiveUsage
oauth_proxySet to on to enable the plugin for a particular API route.
oauth_proxy_cookie_name_prefixThe cookie prefix used for API calls, which in a real setup is written by an OAuth Agent utility API.
oauth_proxy_encryption_keyA 32 byte AES256 decryption key, expressed as 64 hex characters.
oauth_proxy_trusted_web_originOne or more trusted web origins that are allowed to call the API. This directive can be repeated multiple times if needed.
oauth_proxy_cors_enabledWhen set to on, the module writes CORS response headers, which enables the SPA to send and receive API responses.

Run NGINX with the Module

First update the Docker Compose file to deploy the module and configuration files:

version: '3.8'
services:
  my_nginx:
    image: nginx:1.21.3-alpine
    ports:
    - 8080:80
    volumes:
    - ./nginx.conf:/etc/nginx/nginx.conf
    - ./default.conf.template:/etc/nginx/templates/default.conf.template
    - ./alpine.ngx_curity_http_oauth_proxy_module_1.21.3.so:/usr/lib/nginx/modules/ngx_curity_http_oauth_proxy_module.so

Next re-run the Docker Compose deployment:

docker-compose up --force-recreate

Test the Module

You can then act as a Single Page Application that makes API calls via the NGINX reverse proxy. Start by sending a pre-flight request using the HTTP OPTIONS method:

curl -i -X OPTIONS http://localhost:8080/api \
-H "origin: https://www.example.com"

Since this supplies an authorized origin header, the module returns CORS headers that the browser needs in order for the SPA to be allowed to make requests to the target API:

HTTP/1.1 204 No Content
Server: nginx/1.21.3
Date: Fri, 04 Mar 2022 13:04:32 GMT
Connection: keep-alive
access-control-allow-origin: https://www.example.com
access-control-allow-credentials: true
access-control-allow-methods: OPTIONS,HEAD,GET,POST,PUT,PATCH,DELETE
access-control-max-age: 86400
vary: origin,access-control-request-headers

Next make a GET request while sending a secure cookie that was created with the encryption key used in the example configuration:

curl -i -X GET http://localhost:8080/api \
-H "origin: https://www.example.com" \
-H "cookie: example-at=AcYBf995tTBVsLtQLvOuLUZXHm2c-XqP8t7SKmhBiQtzy5CAw4h_RF6rXyg6kHrvhb8x4WaLQC6h3mw6a3O3Q9A"

If you deployed the module with this example encryption key, the cookie will be successfully decrypted and the target API will be called, resulting in the hard coded response from the mock API:

{
    "message": "API was called successfully with an access token"
}

If the NGINX proxy was deployed with a different encryption key, a 401 unauthorized response will instead be returned. CORS headers are again returned, so that the SPA can read the error response:

HTTP/1.1 401 Unauthorized
Server: nginx/1.21.3
Date: Fri, 04 Mar 2022 13:46:11 GMT
Content-Length: 88
Connection: keep-alive
access-control-allow-origin: https://www.example.com
access-control-allow-credentials: true
vary: origin

{
    "code":"unauthorized",
    "message":"Access denied due to missing or invalid credentials"
}

To troubleshoot such errors, view the NGINX logs with the following command:

CONTAINER_ID=$(docker ps | grep nginx | awk '{print $1}')
docker logs -f $CONTAINER_ID

The OAuth proxy module logs the cause, which in this case is a decryption error, and any details returned from OpenSSL libraries, used to perform the decryption, are also logged:

2022/03/04 13:09:16 [warn] 37#37: *10 Problem encountered decrypting data, error number: 0

Integrate the Phantom Token Module

If following Curity best practices you will also be running the Phantom Token Module, which helps to reduce the size of cookies when using the Token Handler Pattern. The end-to-end flow for APIs then looks like this:

API Flow

To run a setup with both modules, first update the nginx.conf file to load them both:

user  nginx;
worker_processes  auto;
error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

load_module modules/ngx_curity_http_oauth_proxy_module.so;
load_module modules/ngx_curity_http_phantom_token_module.so;

events {
    worker_connections  1024;
}

Next configure both modules in the default.conf.template file for the API route. The OAuth Proxy module will run first, to decrypt the secure cookie containing the opaque access token. The Phantom Token module will run next, to introspect the opaque token to get a JWT. The target API will then receive a JWT access token:

server {
    server_name  localhost;
    listen       80;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    location /api {
        oauth_proxy on;
        oauth_proxy_cookie_name_prefix "example";
        oauth_proxy_encryption_key 4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50;
        oauth_proxy_trusted_web_origin "https://www.example.com";
        oauth_proxy_cors_enabled on;

        phantom_token on;
        phantom_token_client_credential api-gateway-client Password1;
        phantom_token_introspection_endpoint curity;

        proxy_pass http://api-internal.example.com:3002
    }

    location curity {

        resolver 127.0.0.11;
        proxy_pass http://login-internal.example.com:8443/oauth/v2/oauth-introspect;
        proxy_cache_methods POST;
        proxy_cache api_cache;
        proxy_cache_key $request_body;
        proxy_ignore_headers Set-Cookie;
    }
}

Conclusion

The Curity NGINX OAuth Proxy module deals with the complexity of secure encrypted cookies, Cross Origin Resource Sharing (CORS) and Cross Site Request Forgery (CSRF), so that this does not need to be coded in your APIs. This tutorial showed how to deploy and test the NGINX module, so that APIs can be called by Single Page Apps that use the Token Handler Pattern. See the following resources for further details on running an end-to-end setup: