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

NGINX OAuth Proxy Module

On this page

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:

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

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

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

bash
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

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

nginx
1234567891011121314151617181920212223
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, which would issue the cookies when a user of the SPA authenticates.

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

nginx
123456789101112131415161718192021222324
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_enabledMust be set to on when the OAuth proxy runs in a different domain to the web host, or to off when they use the same domain. When activated, 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:

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

bash
1
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. The following requests are based on a cross origin setup, so include use of CORS request and response headers. Start by sending a pre-flight request using the HTTP OPTIONS method:

bash
12
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
123456789
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:

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

json
123
{
"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
12345678910111213
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:

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

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

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

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

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