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:
version: '3.8'services:my_nginx:image: nginx:1.21.3-alpineports:- 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.templatedocker 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
, which would issue 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:
Directive | Usage |
---|---|
oauth_proxy | Set to on to enable the plugin for a particular API route. |
oauth_proxy_cookie_name_prefix | The cookie prefix used for API calls, which in a real setup is written by an OAuth Agent utility API. |
oauth_proxy_encryption_key | A 32 byte AES256 decryption key, expressed as 64 hex characters. |
oauth_proxy_trusted_web_origin | One or more trusted web origins that are allowed to call the API. This directive can be repeated multiple times if needed. |
oauth_proxy_cors_enabled | Must 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:
version: '3.8'services:my_nginx:image: nginx:1.21.3-alpineports:- 8080:80volumes:- ./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. 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:
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 ContentServer: nginx/1.21.3Date: Fri, 04 Mar 2022 13:04:32 GMTConnection: keep-aliveaccess-control-allow-origin: https://www.example.comaccess-control-allow-credentials: trueaccess-control-allow-methods: OPTIONS,HEAD,GET,POST,PUT,PATCH,DELETEaccess-control-max-age: 86400vary: 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 UnauthorizedServer: nginx/1.21.3Date: Fri, 04 Mar 2022 13:46:11 GMTContent-Length: 88Connection: keep-aliveaccess-control-allow-origin: https://www.example.comaccess-control-allow-credentials: truevary: 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:
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:
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