├── security-handling.png ├── .gitattributes ├── token-handler-pattern.png ├── testing ├── integration │ ├── alpine │ │ └── Dockerfile │ ├── amazon2023 │ │ ├── nginx.repo │ │ └── Dockerfile │ ├── amazon2 │ │ ├── nginx.repo │ │ └── Dockerfile │ ├── centosstream9 │ │ ├── nginx.repo │ │ └── Dockerfile │ ├── docker-compose.yaml │ ├── debian11 │ │ └── Dockerfile │ ├── debian12 │ │ └── Dockerfile │ ├── ubuntu20 │ │ └── Dockerfile │ ├── ubuntu22 │ │ └── Dockerfile │ ├── ubuntu24 │ │ └── Dockerfile │ ├── encrypt.js │ ├── nginx.conf.template │ ├── deploy.sh │ └── test.sh ├── localhost │ ├── openssl_install.sh │ └── nginx.conf └── t │ ├── http_options.t │ ├── config.t │ ├── decryption.t │ ├── http_get.t │ └── http_post.t ├── NOTICES ├── config ├── .gitignore ├── Makefile ├── src ├── oauth_proxy.h ├── oauth_proxy_configuration.c ├── oauth_proxy_encoding.c ├── oauth_proxy_utils.c ├── oauth_proxy_decryption.c ├── oauth_proxy_module.c └── oauth_proxy_handler.c ├── configure ├── LICENSE └── README.md /security-handling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/curityio/nginx_oauth_proxy_module/HEAD/security-handling.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # The module is coded in C, so prevent tests changing the GitHub language displayed 2 | t/* linguist-vendored -------------------------------------------------------------------------------- /token-handler-pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/curityio/nginx_oauth_proxy_module/HEAD/token-handler-pattern.png -------------------------------------------------------------------------------- /testing/integration/alpine/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # Alpine has an up to date NGINX image so use that 3 | # 4 | 5 | ARG NGINX_VERSION 6 | FROM nginx:$NGINX_VERSION-alpine 7 | RUN apk add --no-cache valgrind 8 | -------------------------------------------------------------------------------- /testing/integration/amazon2023/nginx.repo: -------------------------------------------------------------------------------- 1 | [nginx-stable] 2 | name=nginx stable repo 3 | baseurl=http://nginx.org/packages/amzn/2023/$basearch/ 4 | gpgcheck=1 5 | enabled=1 6 | gpgkey=https://nginx.org/keys/nginx_signing.key 7 | module_hotfixes=true 8 | 9 | [nginx-mainline] 10 | name=nginx mainline repo 11 | baseurl=http://nginx.org/packages/mainline/amzn/2023/$basearch/ 12 | gpgcheck=1 13 | enabled=0 14 | gpgkey=https://nginx.org/keys/nginx_signing.key 15 | module_hotfixes=true -------------------------------------------------------------------------------- /testing/integration/amazon2/nginx.repo: -------------------------------------------------------------------------------- 1 | [nginx-stable] 2 | name=nginx stable repo 3 | baseurl=http://nginx.org/packages/amzn2/$releasever/$basearch/ 4 | gpgcheck=1 5 | enabled=1 6 | gpgkey=https://nginx.org/keys/nginx_signing.key 7 | module_hotfixes=true 8 | 9 | [nginx-mainline] 10 | name=nginx mainline repo 11 | baseurl=http://nginx.org/packages/mainline/amzn2/$releasever/$basearch/ 12 | gpgcheck=1 13 | enabled=0 14 | gpgkey=https://nginx.org/keys/nginx_signing.key 15 | module_hotfixes=true -------------------------------------------------------------------------------- /testing/integration/centosstream9/nginx.repo: -------------------------------------------------------------------------------- 1 | [nginx-stable] 2 | name=nginx stable repo 3 | baseurl=http://nginx.org/packages/centos/$releasever/$basearch/ 4 | gpgcheck=1 5 | enabled=1 6 | gpgkey=https://nginx.org/keys/nginx_signing.key 7 | module_hotfixes=true 8 | 9 | [nginx-mainline] 10 | name=nginx mainline repo 11 | baseurl=http://nginx.org/packages/mainline/centos/$releasever/$basearch/ 12 | gpgcheck=1 13 | enabled=0 14 | gpgkey=https://nginx.org/keys/nginx_signing.key 15 | module_hotfixes=true -------------------------------------------------------------------------------- /testing/integration/amazon2/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # For Amazon Linux we need to install some dependencies and then install nginx 3 | # http://nginx.org/en/linux_packages.html#Amazon-Linux 4 | # 5 | 6 | FROM amazonlinux:2 7 | ARG NGINX_VERSION 8 | 9 | RUN yum install -y yum-utils 10 | 11 | COPY amazon2/nginx.repo /etc/yum.repos.d/nginx.repo 12 | 13 | RUN yum-config-manager --enable nginx-mainline 14 | 15 | # 'yum list available nginx --showduplicates' shows versions available 16 | RUN yum install -y nginx-1:$NGINX_VERSION-1.amzn2.ngx.x86_64 valgrind 17 | -------------------------------------------------------------------------------- /testing/integration/amazon2023/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # For Amazon Linux we need to install some dependencies and then install nginx 3 | # http://nginx.org/en/linux_packages.html#Amazon-Linux 4 | # 5 | 6 | FROM amazonlinux:2023 7 | ARG NGINX_VERSION 8 | 9 | RUN yum install -y yum-utils 10 | 11 | COPY amazon2023/nginx.repo /etc/yum.repos.d/nginx.repo 12 | 13 | RUN yum-config-manager --enable nginx-mainline 14 | 15 | # 'yum list available nginx --showduplicates' shows versions available 16 | RUN yum install -y nginx-$NGINX_VERSION-1.amzn2023.ngx.x86_64 valgrind 17 | -------------------------------------------------------------------------------- /testing/integration/centosstream9/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # For CentOS we need to install some dependencies and then install nginx 3 | # CentOS 8 is end of life so we use the latest CentOS Stream instead 4 | # http://nginx.org/en/linux_packages.html#CentOS 5 | # 6 | 7 | FROM quay.io/centos/centos:stream9 8 | ARG NGINX_VERSION 9 | 10 | RUN yum install -y yum-utils 11 | COPY centos7/nginx.repo /etc/yum.repos.d/nginx.repo 12 | RUN yum-config-manager --enable nginx-mainline 13 | 14 | # 'yum list available nginx --showduplicates' shows versions available 15 | RUN yum install -y nginx-1:$NGINX_VERSION-1.el9.ngx.x86_64 valgrind 16 | -------------------------------------------------------------------------------- /NOTICES: -------------------------------------------------------------------------------- 1 | Apache library used for base64 decoding 2 | --------------------------------------- 3 | Apache Portable Runtime Utility Library 4 | Copyright (c) 2000-2014 The Apache Software Foundation. 5 | 6 | This product includes software developed at 7 | The Apache Software Foundation (http://www.apache.org/). 8 | 9 | Portions of this software were developed at the National Center 10 | for Supercomputing Applications (NCSA) at the University of 11 | Illinois at Urbana-Champaign. 12 | 13 | This software contains code derived from the RSA Data Security 14 | Inc. MD5 Message-Digest Algorithm, including various 15 | modifications by Spyglass Inc., Carnegie Mellon University, and 16 | Bell Communications Research, Inc (Bellcore). -------------------------------------------------------------------------------- /testing/integration/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # Deploy a Docker containers and run it via valgrind, to test for memory leaks 3 | ############################################################################## 4 | 5 | version: '3.8' 6 | services: 7 | 8 | custom_nginx: 9 | image: nginx_${DISTRO}:${NGINX_VERSION} 10 | ports: 11 | - 8081:8081 12 | volumes: 13 | - ./nginx.conf:${CONF_PATH} 14 | - ../../build/${MODULE_FILE}:${MODULE_FOLDER}/ngx_curity_http_oauth_proxy_module.so 15 | command: > 16 | sh -c "/usr/bin/valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes --verbose --log-file=/valgrind-results.txt ${NGINX_PATH}" 17 | -------------------------------------------------------------------------------- /config: -------------------------------------------------------------------------------- 1 | addon_base_name=oauth_proxy 2 | protocol=http 3 | company_name=curity 4 | ngx_addon_name=ngx_${company_name}_${protocol}_${addon_base_name}_module 5 | 6 | OAUTH_PROXY_SRCS="\ 7 | $ngx_addon_dir/src/oauth_proxy_module.c \ 8 | $ngx_addon_dir/src/oauth_proxy_configuration.c \ 9 | $ngx_addon_dir/src/oauth_proxy_handler.c \ 10 | $ngx_addon_dir/src/oauth_proxy_decryption.c \ 11 | $ngx_addon_dir/src/oauth_proxy_encoding.c \ 12 | $ngx_addon_dir/src/oauth_proxy_utils.c \ 13 | " 14 | 15 | if test -n "$ngx_module_link"; then 16 | ngx_module_type=HTTP 17 | ngx_module_name=$ngx_addon_name 18 | ngx_module_srcs="$OAUTH_PROXY_SRCS" 19 | 20 | . auto/module 21 | else 22 | HTTP_MODULES="$HTTP_MODULES $ngx_addon_name" 23 | NGX_ADDON_SRCS="$NGX_ADDON_SRCS $OAUTH_PROXY_SRCS" 24 | fi 25 | -------------------------------------------------------------------------------- /testing/integration/debian11/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # For Debian we need to install some dependencies and then install nginx 3 | # http://nginx.org/en/linux_packages.html#Debian 4 | # 5 | 6 | FROM debian:bullseye 7 | ARG NGINX_VERSION 8 | 9 | RUN apt update 10 | RUN apt install -y curl gnupg2 ca-certificates lsb-release debian-archive-keyring 11 | 12 | RUN curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor \ 13 | | tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null 14 | 15 | RUN echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \ 16 | http://nginx.org/packages/mainline/debian `lsb_release -cs` nginx" \ 17 | | tee /etc/apt/sources.list.d/nginx.list 18 | 19 | # 'apt list -a nginx' shows versions available 20 | RUN apt update 21 | RUN apt install -y nginx=$NGINX_VERSION-1~bullseye valgrind 22 | -------------------------------------------------------------------------------- /testing/integration/debian12/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # For Debian we need to install some dependencies and then install nginx 3 | # http://nginx.org/en/linux_packages.html#Debian 4 | # 5 | 6 | FROM debian:bookworm 7 | ARG NGINX_VERSION 8 | 9 | RUN apt update 10 | RUN apt install -y curl gnupg2 ca-certificates lsb-release debian-archive-keyring 11 | 12 | RUN curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor \ 13 | | tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null 14 | 15 | RUN echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \ 16 | http://nginx.org/packages/mainline/debian `lsb_release -cs` nginx" \ 17 | | tee /etc/apt/sources.list.d/nginx.list 18 | 19 | # 'apt list -a nginx' shows versions available 20 | RUN apt update 21 | RUN apt install -y nginx=$NGINX_VERSION-1~bookworm valgrind 22 | -------------------------------------------------------------------------------- /testing/integration/ubuntu20/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # For Ubuntu we need to install some dependencies and then install nginx 3 | # http://nginx.org/en/linux_packages.html#Ubuntu 4 | # 5 | 6 | FROM ubuntu:20.04 7 | ARG NGINX_VERSION 8 | 9 | RUN apt update 10 | RUN apt install -y curl gnupg2 ca-certificates lsb-release ubuntu-keyring 11 | 12 | RUN curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor \ 13 | | tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null 14 | 15 | RUN echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \ 16 | http://nginx.org/packages/mainline/ubuntu `lsb_release -cs` nginx" \ 17 | | tee /etc/apt/sources.list.d/nginx.list 18 | 19 | # 'apt list -a nginx' shows versions available 20 | RUN apt update 21 | RUN apt list -a nginx 22 | RUN apt install -y nginx=$NGINX_VERSION-1~focal valgrind 23 | -------------------------------------------------------------------------------- /testing/integration/ubuntu22/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # For Ubuntu we need to install some dependencies and then install nginx 3 | # http://nginx.org/en/linux_packages.html#Ubuntu 4 | # 5 | 6 | FROM ubuntu:22.04 7 | ARG NGINX_VERSION 8 | 9 | RUN apt update 10 | RUN apt install -y curl gnupg2 ca-certificates lsb-release ubuntu-keyring 11 | 12 | RUN curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor \ 13 | | tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null 14 | 15 | RUN echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \ 16 | http://nginx.org/packages/mainline/ubuntu `lsb_release -cs` nginx" \ 17 | | tee /etc/apt/sources.list.d/nginx.list 18 | 19 | # 'apt list -a nginx' shows versions available 20 | RUN apt update 21 | RUN apt list -a nginx 22 | RUN apt install -y nginx=$NGINX_VERSION-1~jammy valgrind 23 | -------------------------------------------------------------------------------- /testing/integration/ubuntu24/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # For Ubuntu we need to install some dependencies and then install nginx 3 | # http://nginx.org/en/linux_packages.html#Ubuntu 4 | # 5 | 6 | FROM ubuntu:24.04 7 | ARG NGINX_VERSION 8 | 9 | RUN apt update 10 | RUN apt install -y curl gnupg2 ca-certificates lsb-release ubuntu-keyring 11 | 12 | RUN curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor \ 13 | | tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null 14 | 15 | RUN echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \ 16 | http://nginx.org/packages/mainline/ubuntu `lsb_release -cs` nginx" \ 17 | | tee /etc/apt/sources.list.d/nginx.list 18 | 19 | # 'apt list -a nginx' shows versions available 20 | RUN apt update 21 | RUN apt list -a nginx 22 | RUN apt install -y nginx=$NGINX_DEPLOY_VERSION-1~noble valgrind 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Object files 5 | *.o 6 | *.ko 7 | *.obj 8 | *.elf 9 | 10 | # Linker output 11 | *.ilk 12 | *.map 13 | *.exp 14 | 15 | # Precompiled Headers 16 | *.gch 17 | *.pch 18 | 19 | # Libraries 20 | *.lib 21 | *.a 22 | *.la 23 | *.lo 24 | 25 | # Shared objects (inc. Windows DLLs) 26 | *.dll 27 | *.so 28 | *.so.* 29 | *.dylib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | *.i*86 36 | *.x86_64 37 | *.hex 38 | 39 | # Debug files 40 | *.dSYM/ 41 | *.su 42 | *.idb 43 | *.pdb 44 | 45 | # Kernel Module Compile Results 46 | *.mod* 47 | *.cmd 48 | .tmp_versions/ 49 | modules.order 50 | Module.symvers 51 | Mkfile.old 52 | dkms.conf 53 | 54 | # macOS files 55 | .DS_Store 56 | 57 | # IDE files 58 | .idea 59 | .vscode 60 | 61 | # Project specific files that are downloaded or built 62 | nginx-*.tar.gz* 63 | /nginx-* 64 | OpenSSL*.tar.gz* 65 | openssl-* 66 | testing/t/servroot 67 | /testing/integration/nginx.conf 68 | .build.info 69 | build 70 | encryption.key 71 | valgrind* 72 | response.txt 73 | log.txt 74 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | -include .build.info 2 | 3 | # There is no `all` target in the NGINX Makefile, but it's a common default, so we add it. When this is used though, 4 | # we always pass on `default` since `all` is unknown to the NGINX Makefile 5 | default all: .build.info 6 | cd $(NGINX_SRC_DIR) && $(MAKE) -e default 7 | 8 | module modules: .build.info $(NGINX_SRC_DIR)/Makefile 9 | ifneq (, $(filter y yes Y YES Yes, $(DYNAMIC_MODULE))) 10 | cd $(NGINX_SRC_DIR) && $(MAKE) -f Makefile modules 11 | else 12 | $(error Rerun the configure script and indicate that a dynamic module should be built) 13 | endif 14 | 15 | build install upgrade: .build.info $(NGINX_SRC_DIR)/Makefile 16 | cd $(NGINX_SRC_DIR) && $(MAKE) -e $@ 17 | 18 | clean: 19 | test -d "$(NGINX_SRC_DIR)" && $(MAKE) -C $(NGINX_SRC_DIR) $@ || true 20 | rm -rf .build.info nginx-$(NGINX_VERSION) nginx-$(NGINX_VERSION).tar.gz* t/servroot 21 | 22 | test: all 23 | cd testing && PATH=$(NGINX_SRC_DIR)/objs:$$PATH prove -v -f t/*.t 24 | 25 | .build.info $(NGINX_SRC_DIR)/Makefile: 26 | $(error You need to run the configure script in the root of this directory before building the source) 27 | -------------------------------------------------------------------------------- /testing/localhost/openssl_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ###################################################################################################### 4 | # Install OpenSSL on a development computer, for the best developer experience and use of intellisense 5 | ###################################################################################################### 6 | 7 | cd "$(dirname "${BASH_SOURCE[0]}")" 8 | cd ../.. 9 | OPENSSL_VERSION='openssl-3.5.0' 10 | 11 | # 12 | # Get the code 13 | # 14 | curl -O -L https://github.com/openssl/openssl/releases/download/$OPENSSL_VERSION/$OPENSSL_VERSION.tar.gz 15 | if [ $? -ne 0 ]; then 16 | >&2 echo 'Problem encountered downloading OpenSSL source code' 17 | exit 1 18 | fi 19 | 20 | # 21 | # Unzip it 22 | # 23 | rm -rf $OPENSSL_VERSION 2>/dev/null 24 | tar xzvf $OPENSSL_VERSION.tar.gz 25 | if [ $? -ne 0 ]; then 26 | >&2 echo 'Problem encountered unzipping OpenSSL archive' 27 | exit 1 28 | fi 29 | 30 | # 31 | # Configure it 32 | # 33 | cd $OPENSSL_VERSION 34 | ./config 35 | if [ $? -ne 0 ]; then 36 | >&2 echo 'Problem encountered configuring OpenSSL' 37 | exit 1 38 | fi 39 | 40 | # 41 | # Build it 42 | # 43 | make 44 | if [ $? -ne 0 ]; then 45 | >&2 echo 'Problem encountered compiling OpenSSL' 46 | exit 1 47 | fi 48 | 49 | # 50 | # Deploy header and libraries to standard places so that it is programmable against 51 | # 52 | sudo make install 53 | if [ $? -ne 0 ]; then 54 | >&2 echo 'Problem encountered deploying OpenSSL' 55 | exit 1 56 | fi 57 | -------------------------------------------------------------------------------- /testing/integration/encrypt.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const crypto = require('crypto'); 5 | const {exit} = require('process'); 6 | const GCM_IV_SIZE = 12; 7 | const CURRENT_VERSION = 1; 8 | 9 | try { 10 | 11 | var args = process.argv.slice(2); 12 | if (args.length < 1) { 13 | throw new Error('Please supply the plaintext data as a command line parameter'); 14 | } 15 | 16 | const payloadText = args[0]; 17 | const encryptionKeyHex = fs.readFileSync('./encryption.key', 'ascii'); 18 | const encryptionKeyBytes = Buffer.from(encryptionKeyHex, 'hex'); 19 | const ivBytes = crypto.randomBytes(GCM_IV_SIZE); 20 | const cipher = crypto.createCipheriv('aes-256-gcm', encryptionKeyBytes, ivBytes); 21 | 22 | const versionBytes = Buffer.from(new Uint8Array([CURRENT_VERSION])); 23 | const plaintextBytes = Buffer.from(payloadText); 24 | 25 | const encryptedBytes = cipher.update(plaintextBytes); 26 | const finalBytes = cipher.final() 27 | 28 | const ciphertextBytes = Buffer.concat([encryptedBytes, finalBytes]); 29 | const tagBytes = cipher.getAuthTag(); 30 | 31 | const allBytes = Buffer.concat([versionBytes, ivBytes, ciphertextBytes, tagBytes]); 32 | const base64urlencoded = allBytes.toString('base64') 33 | .replace(/=/g, "") 34 | .replace(/\+/g, "-") 35 | .replace(/\//g, "_"); 36 | 37 | console.log(base64urlencoded); 38 | exit(0); 39 | 40 | } catch (e) { 41 | 42 | console.log(`utils.encrypt error: ${e.message}`); 43 | exit(1); 44 | } 45 | -------------------------------------------------------------------------------- /src/oauth_proxy.h: -------------------------------------------------------------------------------- 1 | /* Exported types */ 2 | typedef struct 3 | { 4 | ngx_flag_t enabled; 5 | ngx_str_t cookie_name_prefix; 6 | ngx_str_t encryption_key; 7 | ngx_array_t *trusted_web_origins; 8 | ngx_flag_t cors_enabled; 9 | ngx_flag_t allow_tokens; 10 | ngx_str_t cors_allow_methods; 11 | ngx_str_t cors_allow_headers; 12 | ngx_str_t cors_expose_headers; 13 | ngx_int_t cors_max_age; 14 | } oauth_proxy_configuration_t; 15 | 16 | /* Exported functions */ 17 | oauth_proxy_configuration_t* oauth_proxy_module_get_location_configuration(ngx_http_request_t *request); 18 | ngx_int_t oauth_proxy_configuration_initialize_location(ngx_conf_t *main_config, oauth_proxy_configuration_t *child_config); 19 | ngx_int_t oauth_proxy_handler_main(ngx_http_request_t *request); 20 | ngx_int_t oauth_proxy_decryption_decrypt_cookie(ngx_http_request_t *request, ngx_str_t *plain_text, const ngx_str_t* ciphertext, const ngx_str_t* encryption_key_hex); 21 | int oauth_proxy_encoding_bytes_from_hex(u_char *bytes, const u_char *hex, size_t hex_len); 22 | int oauth_proxy_encoding_base64_url_decode(u_char *bufplain, const u_char *bufcoded); 23 | void oauth_proxy_utils_get_csrf_header_name(u_char *csrf_header_name, const oauth_proxy_configuration_t *config); 24 | ngx_str_t *oauth_proxy_utils_get_header_in(ngx_http_request_t *request, u_char *name, size_t len); 25 | ngx_int_t oauth_proxy_utils_get_cookie(ngx_http_request_t *request, ngx_str_t* cookie_value, const ngx_str_t* cookie_name_prefix, const u_char *cookie_suffix); 26 | ngx_int_t oauth_proxy_utils_add_header_out(ngx_http_request_t *request, const char *name, ngx_str_t *value); 27 | ngx_int_t oauth_proxy_utils_add_integer_header_out(ngx_http_request_t *request, const char *name, ngx_int_t value); 28 | -------------------------------------------------------------------------------- /testing/localhost/nginx.conf: -------------------------------------------------------------------------------- 1 | ########################################################################### 2 | # An NGINX configuration to test local deployment on a development computer 3 | ########################################################################### 4 | 5 | worker_processes 1; 6 | error_log /dev/stdout info; 7 | 8 | # During development we turn the daemon off so that we can view logs interactively 9 | daemon off; 10 | 11 | # During development we mostly use a static module, for best debugging and so that automated tests work 12 | # load_module modules/ngx_curity_http_oauth_proxy_module.so; 13 | 14 | events { worker_connections 1024; } 15 | 16 | http { 17 | sendfile on; 18 | 19 | server { 20 | listen 8080; 21 | access_log /dev/stdout; 22 | 23 | location / { 24 | root /usr/share/nginx/html; 25 | index index.html index.htm; 26 | } 27 | 28 | location /api { 29 | 30 | # First run the module 31 | oauth_proxy on; 32 | oauth_proxy_cookie_name_prefix "example"; 33 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 34 | oauth_proxy_trusted_web_origin "https://www.example.com"; 35 | oauth_proxy_cors_enabled on; 36 | 37 | # Then forward to the below API 38 | proxy_pass "http://localhost:8080/api-internal"; 39 | } 40 | 41 | location /api-internal { 42 | 43 | # MIME types must be set like this 44 | default_type application/json; 45 | 46 | # On success, echo back headers 47 | add_header "authorization" $http_authorization; 48 | add_header "cookie" $http_cookie; 49 | add_header "x-example-csrf" $http_x_example_csrf; 50 | 51 | # Return a JSON response 52 | return 200 '{"message": "API was called successfully with ${http_authorization}"}'; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /testing/integration/nginx.conf.template: -------------------------------------------------------------------------------- 1 | ################################################################################### 2 | # An NGINX configuration to test Docker deployment basics on a development computer 3 | ################################################################################### 4 | 5 | worker_processes 1; 6 | error_log /dev/stdout info; 7 | 8 | # This ensures that valgrind can run the main Docker process rather than terminating immediately 9 | daemon off; 10 | 11 | # When testing Linux deployment we deploy a dynamic module 12 | load_module modules/ngx_curity_http_oauth_proxy_module.so; 13 | 14 | events { worker_connections 1024; } 15 | 16 | http { 17 | sendfile on; 18 | 19 | server { 20 | listen 8081; 21 | access_log /dev/stdout; 22 | 23 | location / { 24 | root /usr/share/nginx/html; 25 | index index.html index.htm; 26 | } 27 | 28 | location /api { 29 | 30 | # First run the module 31 | oauth_proxy on; 32 | oauth_proxy_cookie_name_prefix "example"; 33 | oauth_proxy_encryption_key "ENCRYPTION_KEY"; 34 | oauth_proxy_trusted_web_origin "https://www.example.com"; 35 | oauth_proxy_cors_enabled on; 36 | oauth_proxy_allow_tokens on; 37 | 38 | # Then forward to the below API 39 | proxy_pass "http://localhost:8081/api-internal"; 40 | } 41 | 42 | location /api-internal { 43 | 44 | # MIME types must be set like this 45 | default_type application/json; 46 | 47 | # On success, echo back headers 48 | add_header "authorization" $http_authorization; 49 | add_header "cookie" $http_cookie; 50 | add_header "x-example-csrf" $http_x_example_csrf; 51 | 52 | # Return a JSON response 53 | return 200 '{"message": "API was called successfully with ${http_authorization}"}'; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /configure: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | SRC_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) 6 | 7 | NGINX_VERSION=${NGINX_VERSION:-1.27.4} 8 | BUILD_INFO_FILE="$SRC_DIR/.build.info" 9 | test -f "$BUILD_INFO_FILE" && . "$BUILD_INFO_FILE" 10 | 11 | declare -a CONFIG_OPTS=($CONFIG_OPTS --with-compat --with-cc-opt="-Wall -Wextra") 12 | 13 | if [[ -z "$NGINX_SRC_DIR" ]]; then 14 | read -t 60 -e -p "Path to NGINX (leave blank to download version $NGINX_VERSION): " NGINX_SRC_DIR || : 15 | 16 | NGINX_SRC_DIR=${NGINX_SRC_DIR/\~/$HOME} 17 | 18 | if [[ -z "$NGINX_SRC_DIR" ]]; then 19 | NGINX_SRC_DIR="$SRC_DIR/nginx-$NGINX_VERSION" 20 | 21 | # Double check that the directory doesn't already exist before downloading NGINX anew 22 | if [[ ! -d "$NGINX_SRC_DIR" ]]; then 23 | if [[ ! -r nginx-${NGINX_VERSION}.tar.gz ]]; then 24 | if [ -z "$DOWNLOAD_PROGRAM" ]; then 25 | if hash curl &>/dev/null; then 26 | DOWNLOAD_PROGRAM="curl -O" 27 | elif hash wget &>/dev/null; then 28 | DOWNLOAD_PROGRAM="wget" 29 | else 30 | echo "Couldn't find curl or wget, please install either of these programs." 31 | exit 1 32 | fi 33 | fi 34 | $DOWNLOAD_PROGRAM https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz 35 | fi 36 | 37 | tar -xzf nginx-${NGINX_VERSION}.tar.gz 38 | fi 39 | fi 40 | fi 41 | 42 | if [[ -z "${NGINX_DEBUG+xxx}" ]]; then 43 | read -t 10 -p "Do you want to enable NGINX debug features (not recommended for production usage) [y/N]: " NGINX_DEBUG || : 44 | fi 45 | 46 | if [[ "$NGINX_DEBUG" =~ ^([yY][eE][sS]|[yY])+$ ]]; then 47 | CONFIG_OPTS+=(--with-debug --with-cc-opt="-O0 -g3") 48 | else 49 | CONFIG_OPTS+=(--with-cc-opt="-DNDEBUG") 50 | fi 51 | 52 | if [[ -z "$DYNAMIC_MODULE" ]]; then 53 | read -t 10 -p "Do you want to create a dynamic module (required for use with NGINX+) [Y/n]: " DYNAMIC_MODULE || : 54 | fi 55 | 56 | if [[ "$DYNAMIC_MODULE" =~ ^([yY][eE][sS]|[yY])+$ ]] || [[ -z "$DYNAMIC_MODULE" ]]; then 57 | CONFIG_OPTS+=(--add-dynamic-module=$SRC_DIR) 58 | DYNAMIC_MODULE=Y 59 | else 60 | CONFIG_OPTS+=(--add-module=$SRC_DIR) 61 | fi 62 | 63 | BUILD_INFO=("NGINX_SRC_DIR=$NGINX_SRC_DIR" "NGINX_VERSION=$NGINX_VERSION" "NGINX_DEBUG=$NGINX_DEBUG" "DYNAMIC_MODULE=$DYNAMIC_MODULE") 64 | printf '%s\n' "${BUILD_INFO[@]}" >$BUILD_INFO_FILE 65 | cd $NGINX_SRC_DIR && ./configure "${CONFIG_OPTS[@]}" $* 66 | -------------------------------------------------------------------------------- /src/oauth_proxy_configuration.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "oauth_proxy.h" 6 | 7 | /* Forward declarations */ 8 | static ngx_int_t apply_configuration_defaults(ngx_conf_t *main_config, oauth_proxy_configuration_t *config); 9 | static ngx_int_t validate_configuration(ngx_conf_t *main_config, const oauth_proxy_configuration_t *module_location_config); 10 | 11 | /* 12 | * Default and validate the configuration for a location when NGINX starts up 13 | */ 14 | ngx_int_t oauth_proxy_configuration_initialize_location(ngx_conf_t *main_config, oauth_proxy_configuration_t *child_config) 15 | { 16 | if (apply_configuration_defaults(main_config, child_config) != NGX_OK) 17 | { 18 | return NGX_ERROR; 19 | } 20 | 21 | if (validate_configuration(main_config, child_config) != NGX_OK) 22 | { 23 | return NGX_ERROR; 24 | } 25 | 26 | return NGX_OK; 27 | } 28 | 29 | /* 30 | * Set default options that are not provided in the nginx.conf file 31 | */ 32 | static ngx_int_t apply_configuration_defaults(ngx_conf_t *main_config, oauth_proxy_configuration_t *config) 33 | { 34 | const char *default_methods = "OPTIONS,HEAD,GET,POST,PUT,PATCH,DELETE"; 35 | ngx_int_t default_max_age = 86400; 36 | 37 | if (config->cors_enabled) 38 | { 39 | if (config->cors_allow_methods.len == 0) 40 | { 41 | config->cors_allow_methods.data = (u_char *)default_methods; 42 | config->cors_allow_methods.len = ngx_strlen(default_methods); 43 | } 44 | 45 | if (config->cors_max_age == 0) 46 | { 47 | config->cors_max_age = default_max_age; 48 | } 49 | } 50 | 51 | return NGX_OK; 52 | } 53 | 54 | /* 55 | * Validate the cookie prefix to prevent deeper problems later 56 | */ 57 | static ngx_int_t validate_configuration(ngx_conf_t *main_config, const oauth_proxy_configuration_t *module_location_config) 58 | { 59 | size_t max_cookie_name_size = 64; 60 | 61 | if (module_location_config != NULL && module_location_config->enabled) 62 | { 63 | if (module_location_config->cookie_name_prefix.len == 0) 64 | { 65 | ngx_conf_log_error(NGX_LOG_WARN, main_config, 0, "The cookie_name_prefix configuration directive was not provided"); 66 | return NGX_ERROR; 67 | } 68 | 69 | if (module_location_config->cookie_name_prefix.len > max_cookie_name_size) 70 | { 71 | ngx_conf_log_error(NGX_LOG_WARN, main_config, 0, "The cookie_name_prefix configuration directive has a maximum length of %d characters", max_cookie_name_size); 72 | return NGX_ERROR; 73 | } 74 | 75 | if (module_location_config->encryption_key.len == 0) 76 | { 77 | ngx_conf_log_error(NGX_LOG_WARN, main_config, 0, "The encryption_key configuration directive was not provided"); 78 | return NGX_ERROR; 79 | } 80 | 81 | if (module_location_config->encryption_key.len != 64) 82 | { 83 | ngx_conf_log_error(NGX_LOG_WARN, main_config, 0, "The encryption_key configuration directive must contain 64 hex characters"); 84 | return NGX_ERROR; 85 | } 86 | 87 | if (module_location_config->trusted_web_origins == NULL || module_location_config->trusted_web_origins->nelts == 0) 88 | { 89 | ngx_conf_log_error(NGX_LOG_WARN, main_config, 0, "The trusted_web_origin configuration directive was not provided for any web origins"); 90 | return NGX_ERROR; 91 | } 92 | } 93 | 94 | return NGX_OK; 95 | } -------------------------------------------------------------------------------- /testing/integration/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ################################################################################### 4 | # Build and deploy one of the supported Linux distributions with the shared module 5 | ################################################################################### 6 | 7 | cd "$(dirname "${BASH_SOURCE[0]}")" 8 | 9 | # 10 | # Control deployment via environment variables 11 | # 12 | if [ "$DISTRO" == '' ]; then 13 | DISTRO='alpine' 14 | fi 15 | if [ "$NGINX_VERSION" == '' ]; then 16 | NGINX_VERSION='1.27.4' 17 | fi 18 | echo "Deploying for $DISTRO with NGINX version $NGINX_VERSION ..." 19 | 20 | # 21 | # Generate a cookie encryption key for the deployment 22 | # 23 | export ENCRYPTION_KEY=$(openssl rand 32 | xxd -p -c 64) 24 | echo -n $ENCRYPTION_KEY > encryption.key 25 | 26 | # 27 | # Validate input to ensure that we have a supported Linux distribution 28 | # 29 | case $DISTRO in 30 | 31 | 'alpine') 32 | MODULE_FILE="alpine.ngx_curity_http_oauth_proxy_module_$NGINX_VERSION.so" 33 | MODULE_FOLDER='/usr/lib/nginx/modules' 34 | NGINX_PATH='/usr/sbin/nginx' 35 | CONF_PATH='/etc/nginx/nginx.conf' 36 | ;; 37 | 38 | 'debian11') 39 | MODULE_FILE="debian.bullseye.ngx_curity_http_oauth_proxy_module_$NGINX_VERSION.so" 40 | MODULE_FOLDER='/usr/lib/nginx/modules' 41 | NGINX_PATH='/usr/sbin/nginx' 42 | CONF_PATH='/etc/nginx/nginx.conf' 43 | ;; 44 | 45 | 'debian12') 46 | MODULE_FILE="debian.bookworm.ngx_curity_http_oauth_proxy_module_$NGINX_VERSION.so" 47 | MODULE_FOLDER='/usr/lib/nginx/modules' 48 | NGINX_PATH='/usr/sbin/nginx' 49 | CONF_PATH='/etc/nginx/nginx.conf' 50 | ;; 51 | 52 | 'ubuntu20') 53 | MODULE_FILE="ubuntu.20.04.ngx_curity_http_oauth_proxy_module_$NGINX_VERSION.so" 54 | MODULE_FOLDER='/usr/lib/nginx/modules' 55 | NGINX_PATH='/usr/sbin/nginx' 56 | CONF_PATH='/etc/nginx/nginx.conf' 57 | ;; 58 | 59 | 'ubuntu22') 60 | MODULE_FILE="ubuntu.22.04.ngx_curity_http_oauth_proxy_module_$NGINX_VERSION.so" 61 | MODULE_FOLDER='/usr/lib/nginx/modules' 62 | NGINX_PATH='/usr/sbin/nginx' 63 | CONF_PATH='/etc/nginx/nginx.conf' 64 | ;; 65 | 66 | 'ubuntu24') 67 | MODULE_FILE="ubuntu.24.04.ngx_curity_http_oauth_proxy_module_$NGINX_VERSION.so" 68 | MODULE_FOLDER='/usr/lib/nginx/modules' 69 | NGINX_PATH='/usr/sbin/nginx' 70 | CONF_PATH='/etc/nginx/nginx.conf' 71 | ;; 72 | 73 | 'amazon2') 74 | MODULE_FILE="amzn2.ngx_curity_http_oauth_proxy_module_$NGINX_VERSION.so" 75 | MODULE_FOLDER='/etc/nginx/modules' 76 | NGINX_PATH='/usr/sbin/nginx' 77 | CONF_PATH='/etc/nginx/nginx.conf' 78 | ;; 79 | 80 | 'amazon2023') 81 | MODULE_FILE="amzn2023.ngx_curity_http_oauth_proxy_module_$NGINX_VERSION.so" 82 | MODULE_FOLDER='/etc/nginx/modules' 83 | NGINX_PATH='/usr/sbin/nginx' 84 | CONF_PATH='/etc/nginx/nginx.conf' 85 | ;; 86 | 87 | 'centosstream9') 88 | MODULE_FILE="centos.stream.9.ngx_curity_http_oauth_proxy_module_$NGINX_VERSION.so" 89 | MODULE_FOLDER='/etc/nginx/modules' 90 | NGINX_PATH='/usr/sbin/nginx' 91 | CONF_PATH='/etc/nginx/nginx.conf' 92 | ;; 93 | 94 | esac 95 | 96 | # 97 | # Check for a valid distro 98 | # 99 | if [ "$MODULE_FILE" == '' ]; then 100 | >&2 echo 'Please enter a supported Linux distribution as a command line argument' 101 | exit 1 102 | fi 103 | 104 | # 105 | # Check that the image has been built 106 | # 107 | if [ ! -f "../../build/${MODULE_FILE}" ]; then 108 | >&2 echo "The OAuth Proxy plugin for $DISTRO version $NGINX_VERSION has not been built" 109 | exit 1 110 | fi 111 | 112 | # 113 | # Build the Docker image 114 | # 115 | echo 'Building the NGINX and valgrind Docker image ...' 116 | docker build --no-cache -f "$DISTRO/Dockerfile" --build-arg NGINX_VERSION="$NGINX_VERSION" -t "nginx_$DISTRO:$NGINX_VERSION" . 117 | if [ $? -ne 0 ]; then 118 | >&2 echo "Problem encountered building the NGINX $DISTRO docker image" 119 | exit 1 120 | fi 121 | 122 | # 123 | # Supply a runtime 32 byte AES256 cookie encryption key 124 | # 125 | ENCRYPTION_KEY=$(openssl rand 32 | xxd -p -c 64) 126 | echo -n $ENCRYPTION_KEY > encryption.key 127 | 128 | # 129 | # Update the runtime configuration file 130 | # 131 | NGINX_CONF_DATA=$(cat ./nginx.conf.template) 132 | NGINX_CONF_DATA=$(sed "s/ENCRYPTION_KEY/$ENCRYPTION_KEY/g" <<< "$NGINX_CONF_DATA") 133 | echo "$NGINX_CONF_DATA" > ./nginx.conf 134 | 135 | # 136 | # Deploy the Docker container for the distro 137 | # 138 | echo 'Deploying the NGINX and valgrind Docker image ...' 139 | export DISTRO 140 | export NGINX_VERSION 141 | export MODULE_FILE 142 | export MODULE_FOLDER 143 | export NGINX_PATH 144 | export CONF_PATH 145 | docker-compose up --force-recreate --remove-orphans 146 | -------------------------------------------------------------------------------- /src/oauth_proxy_encoding.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Curity AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* 18 | * Adapted from the Apache open source repo: 19 | * https://svn.apache.org/repos/asf/apr/apr/trunk/encoding/apr_base64.c 20 | */ 21 | 22 | /* Licensed to the Apache Software Foundation (ASF) under one or more 23 | * contributor license agreements. See the NOTICES file distributed with 24 | * this work for additional information regarding copyright ownership. 25 | * The ASF licenses this file to You under the Apache License, Version 2.0 26 | * (the "License"); you may not use this file except in compliance with 27 | * the License. You may obtain a copy of the License at 28 | * 29 | * http://www.apache.org/licenses/LICENSE-2.0 30 | * 31 | * Unless required by applicable law or agreed to in writing, software 32 | * distributed under the License is distributed on an "AS IS" BASIS, 33 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 34 | * See the License for the specific language governing permissions and 35 | * limitations under the License. 36 | */ 37 | 38 | #include 39 | 40 | /* Decoding ASCII table with valid entries for base64url 41 | 42 | In base64url: 43 | - Ascii 43 (+) is replaced with Ascii 45 (-) 44 | - Ascii 47 (/) is replaced with Ascii 95 (_) 45 | 46 | Therefore: 47 | - The Apache value from zero based position 43 was copied to byte 45, then byte 43 was set to 64 48 | - The Apache value from zero based position 47 was copied to byte 95, then byte 47 was set to 64 49 | */ 50 | static const u_char pr2six[256] = 51 | { 52 | 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 53 | 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 54 | 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 62, 64, 64, 55 | 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 64, 64, 64, 64, 64, 64, 56 | 64, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 57 | 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 64, 64, 64, 64, 63, 58 | 64, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 59 | 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 64, 64, 64, 64, 64, 60 | 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 61 | 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 62 | 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 63 | 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64 | 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 65 | 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 66 | 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 67 | 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64 68 | }; 69 | 70 | /* 71 | * Decode bytes from base64url 72 | */ 73 | int oauth_proxy_encoding_base64_url_decode(u_char *bufplain, const u_char *bufcoded) 74 | { 75 | int nbytesdecoded = 0; 76 | const u_char *bufin = NULL; 77 | u_char *bufout = NULL; 78 | int nprbytes = 0; 79 | 80 | bufin = bufcoded; 81 | while (pr2six[*(bufin++)] <= 63) 82 | ; 83 | 84 | nprbytes = (bufin - bufcoded) - 1; 85 | nbytesdecoded = (((int)nprbytes + 3) / 4) * 3; 86 | 87 | bufout = bufplain; 88 | bufin = bufcoded; 89 | 90 | while (nprbytes > 4) 91 | { 92 | *(bufout++) = (u_char) (pr2six[*bufin] << 2 | pr2six[bufin[1]] >> 4); 93 | *(bufout++) = (u_char) (pr2six[bufin[1]] << 4 | pr2six[bufin[2]] >> 2); 94 | *(bufout++) = (u_char) (pr2six[bufin[2]] << 6 | pr2six[bufin[3]]); 95 | bufin += 4; 96 | nprbytes -= 4; 97 | } 98 | 99 | if (nprbytes > 1) 100 | { 101 | *(bufout++) = (u_char) (pr2six[*bufin] << 2 | pr2six[bufin[1]] >> 4); 102 | } 103 | if (nprbytes > 2) 104 | { 105 | *(bufout++) = (u_char) (pr2six[bufin[1]] << 4 | pr2six[bufin[2]] >> 2); 106 | } 107 | if (nprbytes > 3) 108 | { 109 | *(bufout++) = (u_char) (pr2six[bufin[2]] << 6 | pr2six[bufin[3]]); 110 | } 111 | 112 | nbytesdecoded -= (4 - nprbytes) & 3; 113 | bufplain[nbytesdecoded] = '\0'; 114 | return nbytesdecoded; 115 | } 116 | 117 | /* 118 | * Convert each pair of hex characters to a byte value 119 | */ 120 | int oauth_proxy_encoding_bytes_from_hex(u_char *bytes, const u_char *hex, size_t hex_len) 121 | { 122 | size_t i = 0; 123 | char c = 0; 124 | u_char d = 0; 125 | 126 | if (hex_len %2 != 0) 127 | { 128 | return -1; 129 | } 130 | 131 | for (i = 0; i < hex_len; i++) 132 | { 133 | c = hex[i]; 134 | if (c >= '0' && c <= '9') 135 | { 136 | d = (c - '0'); 137 | } 138 | else if (c >= 'A' && c <= 'F') 139 | { 140 | d = (10 + (c - 'A')); 141 | } 142 | else if (c >= 'a' && c <= 'f') 143 | { 144 | d = (10 + (c - 'a')); 145 | } 146 | else 147 | { 148 | /* Invalid character */ 149 | return 1; 150 | } 151 | 152 | if (i % 2 == 0) 153 | { 154 | /* Low order byte is set first */ 155 | bytes[i / 2] = 16 * d; 156 | } 157 | else 158 | { 159 | /* High order byte is then added to the low order byte */ 160 | bytes[i / 2] += d; 161 | } 162 | } 163 | 164 | return 0; 165 | } 166 | -------------------------------------------------------------------------------- /src/oauth_proxy_utils.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "oauth_proxy.h" 7 | 8 | /* Forward declarations */ 9 | static ngx_int_t oauth_proxy_utils_integer_to_headerstring(ngx_http_request_t *request, ngx_str_t *output, ngx_int_t input); 10 | 11 | /* 12 | * Get the CSRF header name into the supplied buffer 13 | */ 14 | void oauth_proxy_utils_get_csrf_header_name(u_char *csrf_header_name, const oauth_proxy_configuration_t *config) 15 | { 16 | const char *literal_prefix = "x-"; 17 | const char *literal_suffix = "-csrf"; 18 | size_t prefix_length = ngx_strlen(literal_prefix); 19 | size_t suffix_length = ngx_strlen(literal_suffix); 20 | 21 | ngx_memcpy(csrf_header_name, literal_prefix, prefix_length); 22 | ngx_memcpy(csrf_header_name + prefix_length, config->cookie_name_prefix.data, config->cookie_name_prefix.len); 23 | ngx_memcpy(csrf_header_name + prefix_length + config->cookie_name_prefix.len, literal_suffix, suffix_length); 24 | csrf_header_name[prefix_length + config->cookie_name_prefix.len + suffix_length] = 0; 25 | } 26 | 27 | /* 28 | * Find a header that is not in the standard headers_in structure 29 | * https://www.nginx.com/resources/wiki/start/topics/examples/headers_management/ 30 | */ 31 | ngx_str_t *oauth_proxy_utils_get_header_in(ngx_http_request_t *request, u_char *name, size_t len) 32 | { 33 | ngx_list_part_t *part = NULL; 34 | ngx_table_elt_t *h = NULL; 35 | ngx_uint_t i = 0; 36 | 37 | /* Get the first part of the list. There is usual only one part */ 38 | part = &request->headers_in.headers.part; 39 | h = part->elts; 40 | 41 | /* Headers list array may consist of more than one part, so loop through all of it */ 42 | for (i = 0; ; i++) { 43 | 44 | if (i >= part->nelts) { 45 | if (part->next == NULL) { 46 | /* The last part, search is done */ 47 | break; 48 | } 49 | 50 | part = part->next; 51 | h = part->elts; 52 | i = 0; 53 | } 54 | 55 | /* Just compare the lengths and then the names case insensitively */ 56 | if (len != h[i].key.len || ngx_strcasecmp(name, h[i].key.data) != 0) { 57 | continue; 58 | } 59 | 60 | /* Stop the search at the first matched header */ 61 | return &h[i].value; 62 | } 63 | 64 | return NULL; 65 | } 66 | 67 | /* 68 | * Get a cookie and deal with string manipulation 69 | * Use this include technique from the below link to handle version specific differences 70 | * https://github.com/openresty/headers-more-nginx-module/blob/master/src/ngx_http_headers_more_headers_in.c 71 | */ 72 | ngx_int_t oauth_proxy_utils_get_cookie(ngx_http_request_t *request, ngx_str_t* cookie_value, const ngx_str_t* cookie_name_prefix, const u_char *cookie_suffix) 73 | { 74 | u_char cookie_name[128]; 75 | size_t suffix_len = 0; 76 | ngx_str_t cookie_name_str; 77 | 78 | #if defined(nginx_version) && nginx_version >= 1023000 79 | ngx_table_elt_t *cookie_headers = NULL; 80 | #endif 81 | 82 | suffix_len = ngx_strlen(cookie_suffix); 83 | ngx_memcpy(cookie_name, cookie_name_prefix->data, cookie_name_prefix->len); 84 | ngx_memcpy(cookie_name + cookie_name_prefix->len, cookie_suffix, suffix_len); 85 | cookie_name[cookie_name_prefix->len + suffix_len] = 0; 86 | 87 | cookie_name_str.data = cookie_name; 88 | cookie_name_str.len = ngx_strlen(cookie_name); 89 | 90 | #if defined(nginx_version) && nginx_version >= 1023000 91 | 92 | // The API to deal with multi header lines changed for NGINX 1.23.0 93 | cookie_headers = ngx_http_parse_multi_header_lines(request, request->headers_in.cookie, &cookie_name_str, cookie_value); 94 | if (cookie_headers == NULL) { 95 | return NGX_DECLINED; 96 | } 97 | 98 | return NGX_OK; 99 | #else 100 | 101 | // Versions before 1.23.0 used this syntax and returned NGX_DECLINED if not found 102 | return ngx_http_parse_multi_header_lines(&request->headers_in.cookies, &cookie_name_str, cookie_value); 103 | #endif 104 | } 105 | 106 | /* 107 | * Add a single outgoing header 108 | */ 109 | ngx_int_t oauth_proxy_utils_add_header_out(ngx_http_request_t *request, const char *name, ngx_str_t *value) 110 | { 111 | ngx_table_elt_t *header_element = NULL; 112 | 113 | header_element = ngx_list_push(&request->headers_out.headers); 114 | if (header_element == NULL) 115 | { 116 | return NGX_ERROR; 117 | } 118 | 119 | header_element->key.data = (u_char *)name; 120 | header_element->key.len = ngx_strlen(name); 121 | header_element->value.data = value->data; 122 | header_element->value.len = value->len; 123 | header_element->hash = 1; 124 | return NGX_OK; 125 | } 126 | 127 | /* 128 | * Deal with conversions and then add an outgoing header for an integer 129 | */ 130 | ngx_int_t oauth_proxy_utils_add_integer_header_out(ngx_http_request_t *request, const char *name, ngx_int_t value) 131 | { 132 | ngx_str_t buffer; 133 | 134 | if (oauth_proxy_utils_integer_to_headerstring(request, &buffer, value) != NGX_OK) 135 | { 136 | return NGX_ERROR; 137 | } 138 | 139 | return oauth_proxy_utils_add_header_out(request, name, &buffer); 140 | } 141 | 142 | /* 143 | * Set a header string, which must point to permanent heap memory 144 | */ 145 | static ngx_int_t oauth_proxy_utils_integer_to_headerstring(ngx_http_request_t *request, ngx_str_t *output, ngx_int_t input) 146 | { 147 | u_char *buffer; 148 | u_char *result = NULL; 149 | size_t buffer_size = 128; 150 | size_t size = 0; 151 | 152 | buffer = ngx_pcalloc(request->pool, buffer_size); 153 | if (buffer == NULL) 154 | { 155 | ngx_log_error(NGX_LOG_WARN, request->connection->log, 0, "OAuth proxy failed to allocate integer to string memory"); 156 | return NGX_ERROR; 157 | } 158 | 159 | result = ngx_snprintf(buffer, buffer_size - 1, "%d", input); 160 | size = result - buffer; 161 | buffer[size] = 0; 162 | 163 | output->data = buffer; 164 | output->len = size; 165 | return NGX_OK; 166 | } 167 | -------------------------------------------------------------------------------- /src/oauth_proxy_decryption.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Curity AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include "oauth_proxy.h" 23 | 24 | /* For encryption related constants to be used in array sizes, use #defines as valid C */ 25 | #define VERSION_SIZE 1 26 | #define GCM_IV_SIZE 12 27 | #define GCM_TAG_SIZE 16 28 | #define AES_KEY_SIZE_BYTES 32 29 | #define CURRENT_VERSION 1 30 | 31 | /* 32 | * Performs AES256-GCM authenticated decryption of secure cookies, using the hex encryption key from configuration 33 | * https://wiki.openssl.org/index.php/EVP_Authenticated_Encryption_and_Decryption 34 | */ 35 | ngx_int_t oauth_proxy_decryption_decrypt_cookie(ngx_http_request_t *request, ngx_str_t *plaintext, const ngx_str_t *ciphertext, const ngx_str_t *encryption_key_hex) 36 | { 37 | EVP_CIPHER_CTX *ctx = NULL; 38 | u_char encryption_key_bytes[AES_KEY_SIZE_BYTES]; 39 | u_char *ciphertext_bytes = NULL; 40 | u_char *plaintext_bytes = NULL; 41 | u_char iv_bytes[GCM_IV_SIZE]; 42 | u_char tag_bytes[GCM_TAG_SIZE]; 43 | int decoded_size = 0; 44 | int ciphertext_byte_size = 0; 45 | int plaintext_len = 0; 46 | int offset = 0; 47 | int len = 0; 48 | int evp_result = 0; 49 | ngx_int_t ret_code = NGX_OK; 50 | 51 | ctx = EVP_CIPHER_CTX_new(); 52 | if (ctx == NULL) 53 | { 54 | ngx_log_error(NGX_LOG_WARN, request->connection->log, 0, "Unable to create the decryption cipher"); 55 | ret_code = NGX_HTTP_INTERNAL_SERVER_ERROR; 56 | } 57 | 58 | if (ret_code == NGX_OK) 59 | { 60 | ret_code = oauth_proxy_encoding_bytes_from_hex(encryption_key_bytes, encryption_key_hex->data, encryption_key_hex->len); 61 | if (ret_code != 0) 62 | { 63 | ngx_log_error(NGX_LOG_WARN, request->connection->log, 0, "The configured encryption key is not valid hex"); 64 | ret_code = NGX_HTTP_UNAUTHORIZED; 65 | } 66 | } 67 | 68 | /* The cookie ciphertext size could represent a large JWT, so allocate memory dynamically 69 | In base64url the plaintext is always smaller than the ciphertext, but here we just ensure sufficient size */ 70 | if (ret_code == NGX_OK) 71 | { 72 | ciphertext_bytes = ngx_pcalloc(request->pool, ciphertext->len + 1); 73 | if (ciphertext_bytes == NULL) 74 | { 75 | ngx_log_error(NGX_LOG_WARN, request->connection->log, 0, "Problem encountered allocating memory for ciphertext bytes"); 76 | ret_code = NGX_HTTP_INTERNAL_SERVER_ERROR; 77 | } 78 | } 79 | 80 | /* Decode and get the exact encrypted byte sizes */ 81 | if (ret_code == NGX_OK) 82 | { 83 | decoded_size = oauth_proxy_encoding_base64_url_decode(ciphertext_bytes, ciphertext->data); 84 | ciphertext_byte_size = decoded_size - (VERSION_SIZE + GCM_IV_SIZE + GCM_TAG_SIZE); 85 | if (ciphertext_byte_size <= 0) 86 | { 87 | ngx_log_error(NGX_LOG_WARN, request->connection->log, 0, "Invalid data length after decoding from base64"); 88 | ret_code = NGX_HTTP_UNAUTHORIZED; 89 | } 90 | else 91 | { 92 | if (ciphertext_bytes[0] != CURRENT_VERSION) 93 | { 94 | ngx_log_error(NGX_LOG_WARN, request->connection->log, 0, "The received cookie has an invalid format"); 95 | ret_code = NGX_HTTP_UNAUTHORIZED; 96 | } 97 | } 98 | } 99 | 100 | if (ret_code == NGX_OK) 101 | { 102 | plaintext_bytes = ngx_pcalloc(request->pool, ciphertext_byte_size + 1); 103 | if (plaintext_bytes == NULL) 104 | { 105 | ngx_log_error(NGX_LOG_WARN, request->connection->log, 0, "Problem encountered allocating memory for plaintext bytes"); 106 | ret_code = NGX_HTTP_INTERNAL_SERVER_ERROR; 107 | } 108 | } 109 | 110 | if (ret_code == NGX_OK) 111 | { 112 | offset = VERSION_SIZE; 113 | memcpy(iv_bytes, ciphertext_bytes + offset, GCM_IV_SIZE); 114 | 115 | offset = decoded_size - GCM_TAG_SIZE; 116 | memcpy(tag_bytes, ciphertext_bytes + offset, GCM_TAG_SIZE); 117 | } 118 | 119 | if (ret_code == NGX_OK) 120 | { 121 | /* With this algorithm, this method will read precisely 32 bytes from the 4th parameter and 12 bytes from the 5th */ 122 | evp_result = EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, encryption_key_bytes, iv_bytes); 123 | if (evp_result == 0) 124 | { 125 | ngx_log_error(NGX_LOG_WARN, request->connection->log, 0, "Unable to initialize the decryption context, error number: %d", evp_result); 126 | ret_code = NGX_HTTP_UNAUTHORIZED; 127 | } 128 | } 129 | 130 | if (ret_code == NGX_OK) 131 | { 132 | offset = VERSION_SIZE + GCM_IV_SIZE; 133 | evp_result = EVP_DecryptUpdate(ctx, plaintext_bytes, &len, ciphertext_bytes + offset, ciphertext_byte_size); 134 | if (evp_result == 0) 135 | { 136 | ngx_log_error(NGX_LOG_WARN, request->connection->log, 0, "Problem encountered processing ciphertext, error number: %d", evp_result); 137 | ret_code = NGX_HTTP_UNAUTHORIZED; 138 | } 139 | else 140 | { 141 | plaintext_len = len; 142 | } 143 | } 144 | 145 | if (ret_code == NGX_OK) 146 | { 147 | /* With this algorithm, this method will read precisely 16 bytes from the 4th parameter */ 148 | evp_result = EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, GCM_TAG_SIZE, tag_bytes); 149 | if (evp_result == 0) 150 | { 151 | ngx_log_error(NGX_LOG_WARN, request->connection->log, 0, "Problem encountered setting the message authentication code, error number: %d", evp_result); 152 | ret_code = NGX_HTTP_UNAUTHORIZED; 153 | } 154 | } 155 | 156 | if (ret_code == NGX_OK) 157 | { 158 | evp_result = EVP_DecryptFinal_ex(ctx, plaintext_bytes + plaintext_len, &len); 159 | if (evp_result <= 0) 160 | { 161 | ngx_log_error(NGX_LOG_WARN, request->connection->log, 0, "Problem encountered decrypting data, error number: %d", evp_result); 162 | ret_code = NGX_HTTP_UNAUTHORIZED; 163 | } 164 | else 165 | { 166 | plaintext_len += len; 167 | plaintext_bytes[plaintext_len] = 0; 168 | } 169 | } 170 | 171 | if (ret_code == NGX_OK) 172 | { 173 | plaintext->data = plaintext_bytes; 174 | plaintext->len = plaintext_len; 175 | } 176 | 177 | if (ctx != NULL) 178 | { 179 | EVP_CIPHER_CTX_free(ctx); 180 | } 181 | 182 | return ret_code; 183 | } 184 | -------------------------------------------------------------------------------- /src/oauth_proxy_module.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Curity AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include "oauth_proxy.h" 22 | 23 | /* Forward declarations */ 24 | static void *create_location_configuration(ngx_conf_t *config); 25 | static char *merge_location_configuration(ngx_conf_t *main_config, void *parent, void *child); 26 | static ngx_int_t post_configuration(ngx_conf_t *config); 27 | 28 | /* Configuration directives */ 29 | static ngx_command_t oauth_proxy_module_directives[] = 30 | { 31 | { 32 | ngx_string("oauth_proxy"), 33 | NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_FLAG, 34 | ngx_conf_set_flag_slot, 35 | NGX_HTTP_LOC_CONF_OFFSET, 36 | offsetof(oauth_proxy_configuration_t, enabled), 37 | NULL 38 | }, 39 | { 40 | ngx_string("oauth_proxy_cookie_name_prefix"), 41 | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, 42 | ngx_conf_set_str_slot, 43 | NGX_HTTP_LOC_CONF_OFFSET, 44 | offsetof(oauth_proxy_configuration_t, cookie_name_prefix), 45 | NULL 46 | }, 47 | { 48 | ngx_string("oauth_proxy_encryption_key"), 49 | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, 50 | ngx_conf_set_str_slot, 51 | NGX_HTTP_LOC_CONF_OFFSET, 52 | offsetof(oauth_proxy_configuration_t, encryption_key), 53 | NULL 54 | }, 55 | { 56 | ngx_string("oauth_proxy_trusted_web_origin"), 57 | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, 58 | ngx_conf_set_str_array_slot, 59 | NGX_HTTP_LOC_CONF_OFFSET, 60 | offsetof(oauth_proxy_configuration_t, trusted_web_origins), 61 | NULL 62 | }, 63 | { 64 | ngx_string("oauth_proxy_cors_enabled"), 65 | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, 66 | ngx_conf_set_flag_slot, 67 | NGX_HTTP_LOC_CONF_OFFSET, 68 | offsetof(oauth_proxy_configuration_t, cors_enabled), 69 | NULL 70 | }, 71 | { 72 | ngx_string("oauth_proxy_allow_tokens"), 73 | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, 74 | ngx_conf_set_flag_slot, 75 | NGX_HTTP_LOC_CONF_OFFSET, 76 | offsetof(oauth_proxy_configuration_t, allow_tokens), 77 | NULL 78 | }, 79 | { 80 | ngx_string("oauth_proxy_cors_allow_methods"), 81 | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, 82 | ngx_conf_set_str_slot, 83 | NGX_HTTP_LOC_CONF_OFFSET, 84 | offsetof(oauth_proxy_configuration_t, cors_allow_methods), 85 | NULL 86 | }, 87 | { 88 | ngx_string("oauth_proxy_cors_allow_headers"), 89 | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, 90 | ngx_conf_set_str_slot, 91 | NGX_HTTP_LOC_CONF_OFFSET, 92 | offsetof(oauth_proxy_configuration_t, cors_allow_headers), 93 | NULL 94 | }, 95 | { 96 | ngx_string("oauth_proxy_cors_expose_headers"), 97 | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, 98 | ngx_conf_set_str_slot, 99 | NGX_HTTP_LOC_CONF_OFFSET, 100 | offsetof(oauth_proxy_configuration_t, cors_expose_headers), 101 | NULL 102 | }, 103 | { 104 | ngx_string("oauth_proxy_cors_max_age"), 105 | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, 106 | ngx_conf_set_num_slot, 107 | NGX_HTTP_LOC_CONF_OFFSET, 108 | offsetof(oauth_proxy_configuration_t, cors_max_age), 109 | NULL 110 | }, 111 | ngx_null_command /* command termination */ 112 | }; 113 | 114 | /* NGINX integration */ 115 | static ngx_http_module_t oauth_proxy_module_context = 116 | { 117 | NULL, /* pre-configuration */ 118 | post_configuration, 119 | 120 | NULL, /* create main configuration */ 121 | NULL, /* init main configuration */ 122 | 123 | NULL, /* create server configuration */ 124 | NULL, /* merge server configuration */ 125 | 126 | create_location_configuration, 127 | merge_location_configuration 128 | }; 129 | 130 | /* This module is exported and must be non static to avoid linker errors */ 131 | ngx_module_t ngx_curity_http_oauth_proxy_module = 132 | { 133 | NGX_MODULE_V1, 134 | &oauth_proxy_module_context, 135 | oauth_proxy_module_directives, 136 | NGX_HTTP_MODULE, /* module type */ 137 | NULL, /* init master */ 138 | NULL, /* init module */ 139 | NULL, /* init process */ 140 | NULL, /* init thread */ 141 | NULL, /* exit thread */ 142 | NULL, /* exit process */ 143 | NULL, /* exit master */ 144 | NGX_MODULE_V1_PADDING 145 | }; 146 | 147 | /* 148 | * An export to return configuration to the handler 149 | */ 150 | oauth_proxy_configuration_t* oauth_proxy_module_get_location_configuration(ngx_http_request_t *request) 151 | { 152 | return ngx_http_get_module_loc_conf(request, ngx_curity_http_oauth_proxy_module); 153 | } 154 | 155 | /* 156 | * Called when NGINX starts up and finds a location that uses the plumodulegin 157 | */ 158 | static void *create_location_configuration(ngx_conf_t *main_config) 159 | { 160 | oauth_proxy_configuration_t *location_config = ngx_pcalloc(main_config->pool, sizeof(oauth_proxy_configuration_t)); 161 | if (location_config == NULL) 162 | { 163 | return NGX_CONF_ERROR; 164 | } 165 | 166 | location_config->enabled = NGX_CONF_UNSET_UINT; 167 | location_config->trusted_web_origins = NGX_CONF_UNSET_PTR; 168 | location_config->cors_enabled = NGX_CONF_UNSET_UINT; 169 | location_config->allow_tokens = NGX_CONF_UNSET_UINT; 170 | location_config->cors_max_age = NGX_CONF_UNSET_UINT; 171 | return location_config; 172 | } 173 | 174 | /* 175 | * Called when NGINX starts up and finds a parent location that uses the module 176 | */ 177 | static char *merge_location_configuration(ngx_conf_t *main_config, void *parent, void *child) 178 | { 179 | oauth_proxy_configuration_t *parent_config = parent, *child_config = child; 180 | 181 | ngx_conf_merge_off_value(child_config->enabled, parent_config->enabled, 0); 182 | ngx_conf_merge_str_value(child_config->cookie_name_prefix, parent_config->cookie_name_prefix, ""); 183 | ngx_conf_merge_str_value(child_config->encryption_key, parent_config->encryption_key, ""); 184 | ngx_conf_merge_ptr_value(child_config->trusted_web_origins, parent_config->trusted_web_origins, NULL); 185 | ngx_conf_merge_off_value(child_config->cors_enabled, parent_config->cors_enabled, 0); 186 | ngx_conf_merge_off_value(child_config->allow_tokens, parent_config->allow_tokens, 0); 187 | ngx_conf_merge_str_value(child_config->cors_allow_methods, parent_config->cors_allow_methods, ""); 188 | ngx_conf_merge_str_value(child_config->cors_allow_headers, parent_config->cors_allow_headers, ""); 189 | ngx_conf_merge_str_value(child_config->cors_expose_headers, parent_config->cors_expose_headers, ""); 190 | ngx_conf_merge_off_value(child_config->cors_max_age, parent_config->cors_max_age, 0); 191 | 192 | if (oauth_proxy_configuration_initialize_location(main_config, child_config) != NGX_OK) 193 | { 194 | return NGX_CONF_ERROR; 195 | } 196 | 197 | return NGX_CONF_OK; 198 | } 199 | 200 | /* 201 | * Set up the handler after configuration has been processed 202 | */ 203 | static ngx_int_t post_configuration(ngx_conf_t *config) 204 | { 205 | ngx_http_core_main_conf_t *main_config = ngx_http_conf_get_module_main_conf(config, ngx_http_core_module); 206 | ngx_http_handler_pt *h = ngx_array_push(&main_config->phases[NGX_HTTP_ACCESS_PHASE].handlers); 207 | 208 | if (h == NULL) 209 | { 210 | return NGX_ERROR; 211 | } 212 | 213 | *h = oauth_proxy_handler_main; 214 | return NGX_OK; 215 | } 216 | -------------------------------------------------------------------------------- /testing/t/http_options.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | ########################################################################## 4 | # Runs OPTIONS tests to verify security behavior from a client's viewpoint 5 | ########################################################################## 6 | 7 | use strict; 8 | use warnings; 9 | use Test::Nginx::Socket 'no_plan'; 10 | 11 | SKIP: { 12 | run_tests(); 13 | } 14 | 15 | __DATA__ 16 | 17 | === TEST HTTP_OPTIONS_1: OPTIONS without CORS returns no headers 18 | ######################################################################### 19 | # Ensure that CORS headers can be handled by an API when CORS is disabled 20 | ######################################################################### 21 | 22 | --- config 23 | location /t { 24 | oauth_proxy on; 25 | oauth_proxy_cookie_name_prefix "example"; 26 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 27 | oauth_proxy_trusted_web_origin "https://www.example.com"; 28 | oauth_proxy_cors_enabled off; 29 | 30 | proxy_pass http://localhost:1984/target; 31 | } 32 | location /target { 33 | add_header 'access-control-allow-origin' '*'; 34 | return 200; 35 | } 36 | 37 | --- request 38 | OPTIONS /t 39 | 40 | --- more_headers 41 | origin: https://www.example.com 42 | 43 | --- error_code: 200 44 | 45 | --- response_headers 46 | access-control-allow-origin: * 47 | 48 | === TEST HTTP_OPTIONS_2: OPTIONS with CORS and untrusted origin returns no CORS headers 49 | ################################################################### 50 | # Ensure that CORS headers do not grant access to untrusted origins 51 | ################################################################### 52 | 53 | --- config 54 | location /t { 55 | oauth_proxy on; 56 | oauth_proxy_cookie_name_prefix "example"; 57 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 58 | oauth_proxy_trusted_web_origin "https://www.example.com"; 59 | oauth_proxy_cors_enabled on; 60 | } 61 | 62 | --- request 63 | OPTIONS /t 64 | 65 | --- more_headers 66 | origin: https://www.malicious-site.com 67 | 68 | --- error_code: 204 69 | 70 | --- response_headers 71 | access-control-allow-origin: 72 | access-control-allow-credentials: 73 | access-control-allow-methods: 74 | access-control-allow-headers: 75 | access-control-expose-headers: 76 | access-control-max-age: 77 | vary: 78 | 79 | === TEST HTTP_OPTIONS_3: OPTIONS with CORS and valid origin returns expected default headers 80 | ###################################################################### 81 | # Ensure that CORS headers are correctly returned for a trusted origin 82 | ###################################################################### 83 | 84 | --- config 85 | location /t { 86 | oauth_proxy on; 87 | oauth_proxy_cookie_name_prefix "example"; 88 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 89 | oauth_proxy_trusted_web_origin "https://www.other.com"; 90 | oauth_proxy_trusted_web_origin "https://www.example.com"; 91 | oauth_proxy_cors_enabled on; 92 | } 93 | 94 | --- request 95 | OPTIONS /t 96 | 97 | --- more_headers 98 | origin: https://www.example.com 99 | 100 | --- error_code: 204 101 | 102 | --- response_headers 103 | access-control-allow-origin: https://www.example.com 104 | access-control-allow-credentials: true 105 | access-control-allow-methods: OPTIONS,HEAD,GET,POST,PUT,PATCH,DELETE 106 | access-control-allow-headers: 107 | access-control-expose-headers: 108 | access-control-max-age: 86400 109 | vary: origin,access-control-request-headers 110 | 111 | === TEST HTTP_OPTIONS_4: OPTIONS with runtime headers returns those requested by the SPA 112 | ########################################################################################################### 113 | # Ensure that CORS runtime headers are correctly allowed by the OAuth proxy, when default settings are used 114 | ########################################################################################################### 115 | 116 | --- config 117 | location /t { 118 | oauth_proxy on; 119 | oauth_proxy_cookie_name_prefix "example"; 120 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 121 | oauth_proxy_trusted_web_origin "https://www.example.com"; 122 | oauth_proxy_cors_enabled on; 123 | } 124 | 125 | --- request 126 | OPTIONS /t 127 | 128 | --- more_headers eval 129 | my $data; 130 | $data .= "origin: https://www.example.com\n"; 131 | $data .= "access-control-request-headers: x-example-csrf, first, second\n"; 132 | $data; 133 | 134 | --- error_code: 204 135 | 136 | --- response_headers 137 | access-control-allow-origin: https://www.example.com 138 | access-control-allow-credentials: true 139 | access-control-allow-methods: OPTIONS,HEAD,GET,POST,PUT,PATCH,DELETE 140 | access-control-allow-headers: x-example-csrf, first, second 141 | access-control-expose-headers: 142 | access-control-max-age: 86400 143 | vary: origin,access-control-request-headers 144 | 145 | === TEST HTTP_OPTIONS_5: OPTIONS with custom allowed methods returns expected headers 146 | ##################################################################### 147 | # Ensure that custom CORS allow methods can be returned if configured 148 | ##################################################################### 149 | 150 | --- config 151 | location /t { 152 | oauth_proxy on; 153 | oauth_proxy_cookie_name_prefix "example"; 154 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 155 | oauth_proxy_trusted_web_origin "https://www.example.com"; 156 | oauth_proxy_cors_enabled on; 157 | oauth_proxy_cors_allow_methods "GET, POST"; 158 | } 159 | 160 | --- request 161 | OPTIONS /t 162 | 163 | --- more_headers 164 | origin: https://www.example.com 165 | 166 | --- error_code: 204 167 | 168 | --- response_headers 169 | access-control-allow-origin: https://www.example.com 170 | access-control-allow-credentials: true 171 | access-control-allow-methods: GET, POST 172 | access-control-allow-headers: 173 | access-control-expose-headers: 174 | access-control-max-age: 86400 175 | vary: origin,access-control-request-headers 176 | 177 | === TEST HTTP_OPTIONS_6: OPTIONS with custom allowed headers returns expected headers 178 | ##################################################################### 179 | # Ensure that custom CORS allow headers can be returned if configured 180 | ##################################################################### 181 | 182 | --- config 183 | location /t { 184 | oauth_proxy on; 185 | oauth_proxy_cookie_name_prefix "example"; 186 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 187 | oauth_proxy_trusted_web_origin "https://www.example.com"; 188 | oauth_proxy_cors_enabled on; 189 | oauth_proxy_cors_allow_headers "x-example-csrf,other"; 190 | } 191 | 192 | --- request 193 | OPTIONS /t 194 | 195 | --- more_headers 196 | origin: https://www.example.com 197 | 198 | --- error_code: 204 199 | 200 | --- response_headers 201 | access-control-allow-origin: https://www.example.com 202 | access-control-allow-credentials: true 203 | access-control-allow-methods: OPTIONS,HEAD,GET,POST,PUT,PATCH,DELETE 204 | access-control-allow-headers: x-example-csrf,other 205 | access-control-expose-headers: 206 | access-control-max-age: 86400 207 | vary: origin 208 | 209 | === TEST HTTP_OPTIONS_7: OPTIONS with custom expose headers returns expected headers 210 | ###################################################################### 211 | # Ensure that custom CORS expose headers can be returned if configured 212 | ###################################################################### 213 | 214 | --- config 215 | location /t { 216 | oauth_proxy on; 217 | oauth_proxy_cookie_name_prefix "example"; 218 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 219 | oauth_proxy_trusted_web_origin "https://www.example.com"; 220 | oauth_proxy_cors_enabled on; 221 | oauth_proxy_cors_expose_headers "first, second"; 222 | } 223 | 224 | --- request 225 | OPTIONS /t 226 | 227 | --- more_headers 228 | origin: https://www.example.com 229 | 230 | --- error_code: 204 231 | 232 | --- response_headers 233 | access-control-allow-origin: https://www.example.com 234 | access-control-allow-credentials: true 235 | access-control-allow-methods: OPTIONS,HEAD,GET,POST,PUT,PATCH,DELETE 236 | access-control-allow-headers: 237 | access-control-expose-headers: first, second 238 | access-control-max-age: 86400 239 | vary: origin,access-control-request-headers 240 | 241 | === TEST HTTP_OPTIONS_8: OPTIONS with custom max age returns expected headers 242 | ############################################################### 243 | # Ensure that custom CORS max age can be returned if configured 244 | ############################################################### 245 | 246 | --- config 247 | location /t { 248 | oauth_proxy on; 249 | oauth_proxy_cookie_name_prefix "example"; 250 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 251 | oauth_proxy_trusted_web_origin "https://www.example.com"; 252 | oauth_proxy_cors_enabled on; 253 | oauth_proxy_cors_max_age 600; 254 | } 255 | 256 | --- request 257 | OPTIONS /t 258 | 259 | -- ONLY 260 | 261 | --- more_headers 262 | origin: https://www.example.com 263 | 264 | --- error_code: 204 265 | 266 | --- response_headers 267 | access-control-allow-origin: https://www.example.com 268 | access-control-allow-credentials: true 269 | access-control-allow-methods: OPTIONS,HEAD,GET,POST,PUT,PATCH,DELETE 270 | access-control-allow-headers: 271 | access-control-expose-headers: 272 | access-control-max-age: 600 273 | vary: origin,access-control-request-headers -------------------------------------------------------------------------------- /testing/t/config.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | ######################################################################## 4 | # Runs configuration tests to verify that only correct input is accepted 5 | ######################################################################## 6 | 7 | use strict; 8 | use warnings; 9 | use Test::Nginx::Socket 'no_plan'; 10 | run_tests(); 11 | 12 | __DATA__ 13 | 14 | === TEST CONFIG_1: NGINX starts OK when the module is deactivated 15 | ################################################################################ 16 | # Routing to the target does not require config settings or run the module logic 17 | ################################################################################ 18 | 19 | --- config 20 | location /t { 21 | oauth_proxy off; 22 | proxy_pass http://localhost:1984/target; 23 | } 24 | location /target { 25 | return 200; 26 | } 27 | 28 | --- request 29 | GET /t 30 | 31 | --- error_code: 200 32 | 33 | === TEST CONFIG_2: A deployment with empty configuration is correctly detected and logged 34 | #################################################################################### 35 | # Verify that null configuration is handled in a controlled manner and fails to load 36 | #################################################################################### 37 | 38 | --- config 39 | location /t { 40 | oauth_proxy on; 41 | } 42 | 43 | --- must_die 44 | 45 | --- request 46 | GET /t 47 | 48 | --- error_code: 500 49 | 50 | --- error_log 51 | The cookie_name_prefix configuration directive was not provided 52 | 53 | --- response_body_like chomp 54 | {"code":"server_error","message":"Problem encountered processing the request"} 55 | 56 | === TEST CONFIG_3: NGINX quits when no encryption key is configured for a location 57 | ################################################################## 58 | # The module correctly validates required parameters for each path 59 | ################################################################## 60 | 61 | --- config 62 | location /first { 63 | oauth_proxy on; 64 | oauth_proxy_cookie_name_prefix "example"; 65 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 66 | oauth_proxy_trusted_web_origin "https://www.example.com"; 67 | oauth_proxy_cors_enabled on; 68 | } 69 | location /second { 70 | oauth_proxy on; 71 | oauth_proxy_cookie_name_prefix "example"; 72 | oauth_proxy_trusted_web_origin "https://www.example.com"; 73 | oauth_proxy_cors_enabled on; 74 | } 75 | 76 | --- must_die 77 | 78 | --- request 79 | GET /t 80 | 81 | --- error_log 82 | The encryption_key configuration directive was not provided 83 | 84 | === TEST CONFIG_4: NGINX quits when the cookie prefix configured is abnormally long 85 | #################################################################################################### 86 | # A 64 character limit is used for the prefix to allow stack allocation based on a known buffer size 87 | #################################################################################################### 88 | 89 | --- config 90 | location /t { 91 | oauth_proxy on; 92 | oauth_proxy_cookie_name_prefix "abcdefghijklmnopqrstuvwxyz-0123456789-abcdefghijklmnopqrstuvwxyz-0123456789"; 93 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 94 | oauth_proxy_trusted_web_origin "https://www.example.com"; 95 | oauth_proxy_cors_enabled on; 96 | } 97 | 98 | --- must_die 99 | 100 | --- error_log 101 | The cookie_name_prefix configuration directive has a maximum length of 64 characters 102 | 103 | === TEST CONFIG_5: NGINX quits when an invalid length 256 bit encryption key is configured 104 | ################################################################ 105 | # This ensures that input has an expected length before using it 106 | ################################################################ 107 | 108 | --- config 109 | location /t { 110 | oauth_proxy on; 111 | oauth_proxy_cookie_name_prefix "example"; 112 | oauth_proxy_encryption_key "4e4636356d6"; 113 | oauth_proxy_trusted_web_origin "https://www.example.com"; 114 | oauth_proxy_cors_enabled on; 115 | } 116 | 117 | --- must_die 118 | 119 | --- error_log 120 | The encryption_key configuration directive must contain 64 hex characters 121 | 122 | === TEST CONFIG_6: NGINX quits when no trusted web origins are configured 123 | ################################################################################ 124 | # The module is only used for SPAs so it makes sense to always have at least one 125 | ################################################################################ 126 | 127 | --- config 128 | location /t { 129 | oauth_proxy on; 130 | oauth_proxy_cookie_name_prefix "example"; 131 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 132 | oauth_proxy_cors_enabled on; 133 | } 134 | 135 | --- must_die 136 | 137 | --- error_log 138 | The trusted_web_origin configuration directive was not provided for any web origins 139 | 140 | === TEST CONFIG_7: NGINX starts correctly with a valid configuration 141 | ######################################################################################### 142 | # Verifies the most standard happy case, to ensure that the SPA can get data from the API 143 | ######################################################################################### 144 | 145 | --- config 146 | location /t { 147 | oauth_proxy on; 148 | oauth_proxy_cookie_name_prefix "example"; 149 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 150 | oauth_proxy_trusted_web_origin "http://www.example.com"; 151 | oauth_proxy_cors_enabled on; 152 | return 200; 153 | } 154 | location /target { 155 | return 200; 156 | } 157 | 158 | --- request 159 | GET /t 160 | 161 | --- error_code: 200 162 | 163 | === TEST CONFIG_8: NGINX starts correctly with a valid configuration and multiple web origins 164 | ################################################################################### 165 | # For cases where a single API potentially serves multiple SPAs that share a cookie 166 | ################################################################################### 167 | 168 | --- config 169 | location /t { 170 | oauth_proxy on; 171 | oauth_proxy_cookie_name_prefix "example"; 172 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 173 | oauth_proxy_trusted_web_origin "https://webapp1.example.com"; 174 | oauth_proxy_trusted_web_origin "https://webapp2.example.com"; 175 | oauth_proxy_cors_enabled on; 176 | return 200; 177 | } 178 | 179 | --- request 180 | GET /t 181 | 182 | --- error_code: 200 183 | 184 | === TEST CONFIG_9: NGINX starts correctly with two valid root paths 185 | ############################################################################### 186 | # For cases where multiple routes serve multiple SPAs in the same reverse proxy 187 | ############################################################################### 188 | 189 | --- config 190 | location /api1 { 191 | oauth_proxy on; 192 | oauth_proxy_cookie_name_prefix "example"; 193 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 194 | oauth_proxy_trusted_web_origin "https://www.domain1.com"; 195 | oauth_proxy_cors_enabled on; 196 | return 500; 197 | } 198 | location /api2 { 199 | oauth_proxy on; 200 | oauth_proxy_cookie_name_prefix "example"; 201 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 202 | oauth_proxy_trusted_web_origin "https://www.domain2.com"; 203 | oauth_proxy_cors_enabled on; 204 | return 200; 205 | } 206 | 207 | --- request 208 | GET /api2 209 | 210 | --- error_code: 200 211 | 212 | === TEST CONFIG_10: NGINX quits when one of two configurations is invalid 213 | ############################################################### 214 | # Verifies that multiple configurations are correctly validated 215 | ############################################################### 216 | 217 | --- config 218 | location /api1 { 219 | oauth_proxy on; 220 | oauth_proxy_cookie_name_prefix "example"; 221 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 222 | oauth_proxy_trusted_web_origin "https://www.domain1.com"; 223 | oauth_proxy_cors_enabled on; 224 | return 200; 225 | } 226 | location /api2 { 227 | oauth_proxy on; 228 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 229 | oauth_proxy_trusted_web_origin "https://www.domain2.com"; 230 | oauth_proxy_cors_enabled on; 231 | return 200; 232 | } 233 | 234 | --- must_die 235 | 236 | --- error_log 237 | The cookie_name_prefix configuration directive was not provided 238 | 239 | === TEST CONFIG_11: NGINX starts correctly with inherited settings for a child path 240 | ######################################################################### 241 | # Verifies that the module runs for child paths when enabled for a parent 242 | ######################################################################### 243 | 244 | --- config 245 | location /root { 246 | oauth_proxy on; 247 | oauth_proxy_cookie_name_prefix "example"; 248 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 249 | oauth_proxy_trusted_web_origin "https://www.example.com"; 250 | oauth_proxy_cors_enabled on; 251 | 252 | location /root/api { 253 | proxy_pass http://localhost:1984/target; 254 | } 255 | } 256 | location /target { 257 | return 200; 258 | } 259 | 260 | --- request 261 | GET /root/api 262 | 263 | --- error_code: 401 264 | 265 | --- error_log 266 | The request did not have an origin header 267 | -------------------------------------------------------------------------------- /testing/t/decryption.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | ################################################################ 4 | # Runs tests related to decryption and handling error conditions 5 | ################################################################ 6 | 7 | use strict; 8 | use warnings; 9 | use Test::Nginx::Socket 'no_plan'; 10 | 11 | SKIP: { 12 | our $at_opaque = "42665300-efe8-419d-be52-07b53e208f46"; 13 | our $at_opaque_cookie = "AUixxnN28w2MjVK7sMZ3GqErPlw15NwIng-V8amEv5eu43Wr1nzhif1hU2QpKbw_L55GVxD0Kz4gKVG539ywk6g"; 14 | 15 | our $at_jwt = "eyJraWQiOiI2NDQyOTUwMTYiLCJ4NXQiOiJ3S0JrZHB6VmZuaUpTSWIwZ0pWc0VfcU1PN1UiLCJhbGciOiJSUzI1NiJ9.eyJqdGkiOiI4MjVlNGM5MC0xOTc3LTQ4NGYtOWY1MS0yNWFlOGZiMzU0NmYiLCJkZWxlZ2F0aW9uSWQiOiI1NTJiZDhiNy0xOGIwLTQzMTQtODkyZC04OTc1OThkODA5ZGYiLCJleHAiOjE2NDE4MTEwMjEsIm5iZiI6MTY0MTgxMDcyMSwic2NvcGUiOiJwcm9maWxlIiwiaXNzIjoiaHR0cHM6Ly9sb2dpbi1nYXJjaGVyLmV1Lm5ncm9rLmlvL29hdXRoL3YyL29hdXRoLWFub255bW91cyIsInN1YiI6IjRiNmExZDAwNzAyZThkNjA1YzZlOWIwMDQ3Nzk1ZDBmMmM2NjZjZmNlM2UwNDgzNTY5OGJlMzFlYTY1NWM0Y2IiLCJhdWQiOiJqd3QtY2xpZW50IiwiaWF0IjoxNjQxODEwNzIxLCJwdXJwb3NlIjoiYWNjZXNzX3Rva2VuIn0.iSQItk-eScsLifxgFTyF0UQN30couAiczPam4lDTCAHUKjznOPBQJNmbc5tGcog1RcQyvjpAk2wJtObenD0A9Bjk-BQbnhrD0M7In_J1CQaGMmPC6X22lWOmpkJniKudpMqh018xqwec22HmCYWsoUAJQfUfurJa0iin_YkaU-RQAB5rC-JNzY6E4yOrHe6mXiu8iC7c_BNRxcJQ1mAc7WipLzPa047TOL87EXkumwpWeDqH23VB8tXvQHejRYvELDfCxm9CKFvF2QStUFsBB42okkW5HtbmWusR9lqtepXbgM6rRdmbN9kds_tVny394Y6sIU53qx7hyytV88u8_w"; 16 | our $at_jwt_cookie = "AWqpyPNra0Cfa01-Uay9CVY9OjzLMQ8bbOursLK2j5uBRTJPuWUJOMbfhGt2htTCFR9UHN9MKjdfjFwyQPPlD49iFsoq7J8M5Jbi5TwPSvBqdjWAPQHWQiyGBD5BPwY8xQiPN8TY1T6KFQ_eA1cU47l88B-L4TTGkoiI2ESYZwFO9W_8NSEkPy4n3MPmJREHtOKNnxSTEfbWfJmM8sQ3JwfEtmKdpNO3GN_Rr_6HBQ2CYOQaI8wfIrxGRP6FeEgPNwOh2b3Hxj6voFeJUN6vslhyYh7Lw3mxW8FoUOM91lgcEdyDL50ITQnDepegEklBwYjqQUGjCOPf3AyQBXg8AAefd5Z6BGFz4OarIDaMmbLraptvh2LYNhGgil_vdkqHZ5PFVu1ugxjaytA-kjuh8jq_C4vlm4TwSnS34KWjl-Z7_otgRzegFMLOPPuq2BPyfIrfmP8gOyc7t42YSaOyh7ulSbLwqjej4qYT-JWmgQQ7J5D19rx_UiXdrQ2MTCHdrGbymZ3rDJ6Ed3yvY2jlbVbqSlK3WEJh9lKuL4xZOWWAzM6bI31iwcgDDgo8o84xzCgIEHoXyaNK32Om3liHWIydduUsRjQBELdsScHM-CR5F2XpLpWDMR3XcY4Jll2n6-FNrCE0p3czG_PiNJ075StaQz1kAm-Q_L-sfHpHNGQtPWUcIrFO8WK5ibriIo1kMhUgPDOCTQuhTEDNur4T-GmNjlqzqBodiyQs_OhWoBmbggbpjRTv08d3wvngIHjrJDnV7tSSk28fIdC8FIfQiXK0P4HchhGvKzRQ-2AnC5zK6B6eRiULrKcSsFhk_6wFVgHVPb5tgXiaZnlwJonL__53qr3HZkcrGallKCG2Rbu1Sx0_zRYSoL_TEfuzhzZ3-1LQDPe0kDoYoJlUJeSj8iiHyv9DQa5rjq_5eYdL1yxK9riNTQe1ZddssiV86lQp3z805k3r_wG46Gl8AM97Jo0Q0kVtoGbjmmPW0C4g9xWbGVHnSnMetAnPTvnvMQK30lfzBROVGHASe6IJAVNj_7PMdL46o6fU6VrYfjT8meq3PnbExxJwdzQ0S4KqZbNTDA4YpOqjs3246_M0AAQQrMTkCD2dcEvHnwVWSBCtG9ZBZWO20Bgbw0ti3zVZSRxOR4QTmfNhVAiCiRpPI6jaG_1lojJygVXGcHMkc3t7kTMV0fXKXj74eI5R1e3wikXiu4KXxaPG5xeo9F9W6Muvd5N452w3s5Mz"; 17 | 18 | our $csrf_token = "pQguFsD6hFjnyYjaeC5KyijcWS6AvkJHiUmY7dLUsuTKsLAITLiJHVqsCdQpaGYO"; 19 | our $csrf_cookie = "AcdY11SVolhDSduFnfe-83_26jWo8zA4K4x-kT2WtjTLal6PAg6GFjnB3CZqWbDHhIfYYTm_ubeDi92bJjc4CTeZXIEFGhZr3jvyXnaHDW-ZlD6Z_KgcRgcViUWa"; 20 | 21 | run_tests(); 22 | } 23 | 24 | __DATA__ 25 | 26 | === TEST DECRYPTION_1: A request with a cookie too small to be a valid encrypted payload returns 401 27 | #################################################################################### 28 | # Verify that obviously invalid data is rejected and does not cause the code to fail 29 | #################################################################################### 30 | 31 | --- config 32 | location /t { 33 | oauth_proxy on; 34 | oauth_proxy_cookie_name_prefix "example"; 35 | oauth_proxy_encryption_key "7b99279ab87533d3c238db874a842a91ee26a76027f3c03c317504963d2c9926"; 36 | oauth_proxy_trusted_web_origin "https://www.example.com"; 37 | oauth_proxy_cors_enabled on; 38 | } 39 | 40 | --- request 41 | GET /t 42 | 43 | --- more_headers 44 | origin: https://www.example.com 45 | cookie: example-at=x 46 | 47 | --- error_code: 401 48 | 49 | --- error_log 50 | Invalid data length after decoding from base64 51 | 52 | === TEST DECRYPTION_2: A POST with 2 valid cookies for an opaque access token succeeds 53 | ########################################################################## 54 | # Verify that decryption when using opaque access tokens works as expected 55 | ########################################################################## 56 | 57 | --- config 58 | location /t { 59 | oauth_proxy on; 60 | oauth_proxy_cookie_name_prefix "example"; 61 | oauth_proxy_encryption_key "7b99279ab87533d3c238db874a842a91ee26a76027f3c03c317504963d2c9926"; 62 | oauth_proxy_trusted_web_origin "https://www.example.com"; 63 | oauth_proxy_cors_enabled on; 64 | 65 | proxy_pass http://localhost:1984/target; 66 | } 67 | location /target { 68 | add_header 'authorization' $http_authorization; 69 | return 200; 70 | } 71 | 72 | --- request 73 | POST /t 74 | 75 | --- more_headers eval 76 | my $data; 77 | $data .= "origin: https://www.example.com\n"; 78 | $data .= "x-example-csrf: " . $main::csrf_token . "\n"; 79 | $data .= "cookie: example-at=" . $main::at_opaque_cookie . "; example-csrf=" . $main::csrf_cookie . "\n"; 80 | $data; 81 | 82 | --- error_code: 200 83 | 84 | --- response_headers eval 85 | "authorization: Bearer " . $main::at_opaque 86 | 87 | === TEST DECRYPTION_3: A POST with 2 valid cookies for a JWT access token succeeds 88 | ############################################################################## 89 | # Verify that decryption when using larger JWT access tokens works as expected 90 | ############################################################################## 91 | 92 | --- config 93 | location /t { 94 | oauth_proxy on; 95 | oauth_proxy_cookie_name_prefix "example"; 96 | oauth_proxy_encryption_key "7b99279ab87533d3c238db874a842a91ee26a76027f3c03c317504963d2c9926"; 97 | oauth_proxy_trusted_web_origin "https://www.example.com"; 98 | oauth_proxy_cors_enabled on; 99 | 100 | proxy_pass http://localhost:1984/target; 101 | } 102 | location /target { 103 | add_header 'authorization' $http_authorization; 104 | return 200; 105 | } 106 | 107 | --- request 108 | POST /t 109 | 110 | --- more_headers eval 111 | my $data; 112 | $data .= "origin: https://www.example.com\n"; 113 | $data .= "x-example-csrf: " . $main::csrf_token . "\n"; 114 | $data .= "cookie: example-at=" . $main::at_jwt_cookie . "; example-csrf=" . $main::csrf_cookie . "\n"; 115 | $data; 116 | 117 | --- error_code: 200 118 | 119 | --- response_headers eval 120 | "authorization: Bearer " . $main::at_jwt 121 | 122 | === TEST DECRYPTION_4: Handling a request encrypted with a malicious key fails with a 401 123 | ######################################################################################## 124 | # Verify that a request from an attacker sending their own encrypted JWT is not accepted 125 | ######################################################################################## 126 | 127 | --- config 128 | location /t { 129 | oauth_proxy on; 130 | oauth_proxy_cookie_name_prefix "example"; 131 | oauth_proxy_encryption_key "7b99279ab87533d3c238db874a842a91ee26a76027f3c03c317504963d2c9926"; 132 | oauth_proxy_trusted_web_origin "https://www.example.com"; 133 | oauth_proxy_cors_enabled on; 134 | } 135 | 136 | --- request 137 | GET /t 138 | 139 | --- more_headers eval 140 | my $data; 141 | my $encrypted_at = "AcYBf995tTBVsLtQLvOuLUZXHm2c-XqP8t7SKmhBiQtzy5CAw4h_RF6rXyg6kHrvhb8x4WaLQC6h3mw6a3O3Q9A"; 142 | $data .= "origin: https://www.example.com\n"; 143 | $data .= "cookie: example-at=" . $encrypted_at . "\n"; 144 | $data; 145 | 146 | --- error_code: 401 147 | 148 | --- error_log 149 | Problem encountered decrypting data 150 | 151 | === TEST DECRYPTION_5: GET with a tampered IV in the encrypted payload is rejected 152 | ########################################################################################################## 153 | # Verify that a request with an altered initialization vector is rejected when replacing the 5th character 154 | ########################################################################################################## 155 | 156 | --- config 157 | location /t { 158 | oauth_proxy on; 159 | oauth_proxy_cookie_name_prefix "example"; 160 | oauth_proxy_encryption_key "7b99279ab87533d3c238db874a842a91ee26a76027f3c03c317504963d2c9926"; 161 | oauth_proxy_trusted_web_origin "https://www.example.com"; 162 | oauth_proxy_cors_enabled on; 163 | } 164 | 165 | --- request 166 | GET /t 167 | 168 | --- more_headers eval 169 | my $replaced_char = substr($main::at_opaque_cookie, 4, 1); 170 | if ($replaced_char eq "a") { $replaced_char = "b"; } else { $replaced_char = "a"; } 171 | my $tampered_cookie = substr($main::at_opaque_cookie, 0, 4) . $replaced_char . substr($main::at_opaque_cookie, 5, length($main::at_opaque_cookie) - 5); 172 | 173 | my $data; 174 | $data .= "origin: https://www.example.com\n"; 175 | $data .= "cookie: example-at=" . $tampered_cookie . "\n"; 176 | $data; 177 | 178 | --- error_code: 401 179 | 180 | --- error_log 181 | Problem encountered decrypting data 182 | 183 | === TEST DECRYPTION_6: GET with tampered ciphertext in the encrypted payload is rejected 184 | ################################################################################################ 185 | # Verify that a request with an altered ciphertext is rejected when replacing the 45th character 186 | ################################################################################################ 187 | 188 | --- config 189 | location /t { 190 | oauth_proxy on; 191 | oauth_proxy_cookie_name_prefix "example"; 192 | oauth_proxy_encryption_key "7b99279ab87533d3c238db874a842a91ee26a76027f3c03c317504963d2c9926"; 193 | oauth_proxy_trusted_web_origin "https://www.example.com"; 194 | oauth_proxy_cors_enabled on; 195 | } 196 | 197 | --- request 198 | GET /t 199 | 200 | --- more_headers eval 201 | my $replaced_char = substr($main::at_opaque_cookie, 44, 1); 202 | if ($replaced_char eq "a") { $replaced_char = "b"; } else { $replaced_char = "a"; } 203 | my $tampered_cookie = substr($main::at_opaque_cookie, 0, 44) . $replaced_char . substr($main::at_opaque_cookie, 45, length($main::at_opaque_cookie) - 45); 204 | 205 | my $data; 206 | $data .= "origin: https://www.example.com\n"; 207 | $data .= "cookie: example-at=" . $tampered_cookie . "\n"; 208 | $data; 209 | 210 | --- error_code: 401 211 | 212 | --- error_log 213 | Problem encountered decrypting data 214 | 215 | === TEST DECRYPTION_7: GET with tampered MAC in the encrypted payload is rejected 216 | ######################################################################################################################### 217 | # Verify that a request with an altered message authenticaton code is rejected when replacing the 5th from last character 218 | ######################################################################################################################### 219 | 220 | --- config 221 | location /t { 222 | oauth_proxy on; 223 | oauth_proxy_cookie_name_prefix "example"; 224 | oauth_proxy_encryption_key "7b99279ab87533d3c238db874a842a91ee26a76027f3c03c317504963d2c9926"; 225 | oauth_proxy_trusted_web_origin "https://www.example.com"; 226 | oauth_proxy_cors_enabled on; 227 | } 228 | 229 | --- request 230 | GET /t 231 | 232 | --- more_headers eval 233 | my $replaced_char = substr($main::at_opaque_cookie, length($main::at_opaque_cookie) - 5, 1); 234 | if ($replaced_char eq "a") { $replaced_char = "b"; } else { $replaced_char = "a"; } 235 | my $tampered_cookie = substr($main::at_opaque_cookie, 0, length($main::at_opaque_cookie) - 5) . $replaced_char . substr($main::at_opaque_cookie, length($main::at_opaque_cookie) - 4, 4); 236 | 237 | my $data; 238 | $data .= "origin: https://www.example.com\n"; 239 | $data .= "cookie: example-at=" . $tampered_cookie . "\n"; 240 | $data; 241 | 242 | --- error_code: 401 243 | 244 | --- error_log 245 | Problem encountered decrypting data -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 Curity AB 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /testing/t/http_get.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | ####################################################################### 4 | # Runs HTTP tests to verify security behavior from a client's viewpoint 5 | ####################################################################### 6 | 7 | use strict; 8 | use warnings; 9 | use Test::Nginx::Socket 'no_plan'; 10 | 11 | SKIP: { 12 | our $at_opaque = "42665300-efe8-419d-be52-07b53e208f46"; 13 | our $at_opaque_cookie = "AcYBf995tTBVsLtQLvOuLUZXHm2c-XqP8t7SKmhBiQtzy5CAw4h_RF6rXyg6kHrvhb8x4WaLQC6h3mw6a3O3Q9A"; 14 | run_tests(); 15 | } 16 | 17 | __DATA__ 18 | 19 | === TEST HTTP_GET_1: GET without an origin header returns 401 20 | ################################################################################################# 21 | # SPA clients are expected to always send the origin header, as supported by all modern browsers 22 | ################################################################################################# 23 | 24 | --- config 25 | --- config 26 | location /t { 27 | oauth_proxy on; 28 | oauth_proxy_cookie_name_prefix "example"; 29 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 30 | oauth_proxy_trusted_web_origin "https://www.example.com"; 31 | oauth_proxy_cors_enabled on; 32 | } 33 | 34 | --- request 35 | GET /t 36 | 37 | --- error_code: 401 38 | 39 | --- error_log 40 | The request did not have an origin header 41 | 42 | --- response_headers 43 | content-type: application/json 44 | 45 | === TEST HTTP_GET_2: GET with an untrusted origin header returns 401 46 | ############################################################################################### 47 | # Only trusted SPA clients should be able to get data from the browser due to CORS restrictions 48 | ############################################################################################### 49 | 50 | --- config 51 | location /t { 52 | oauth_proxy on; 53 | oauth_proxy_cookie_name_prefix "example"; 54 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 55 | oauth_proxy_trusted_web_origin "https://www.example.com"; 56 | oauth_proxy_cors_enabled on; 57 | } 58 | 59 | --- request 60 | GET /t 61 | 62 | --- more_headers 63 | origin: https://www.malicious-site.com 64 | 65 | --- error_code: 401 66 | 67 | --- error_log 68 | The request was from an untrusted web origin 69 | 70 | --- response_headers 71 | content-type: application/json 72 | 73 | === TEST HTTP_GET_3: GET without a cookie or token credential returns 401 74 | ########################################################################################## 75 | # Verify that a 401 is received when there is no message credential at all sent to the API 76 | ########################################################################################## 77 | 78 | --- config 79 | location /t { 80 | oauth_proxy on; 81 | oauth_proxy_cookie_name_prefix "example"; 82 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 83 | oauth_proxy_trusted_web_origin "https://www.example.com"; 84 | oauth_proxy_cors_enabled on; 85 | } 86 | 87 | --- request 88 | GET /t 89 | 90 | --- more_headers 91 | origin: https://www.example.com 92 | 93 | --- error_code: 401 94 | 95 | --- error_log 96 | No AT cookie was found in the incoming request 97 | 98 | --- response_headers 99 | content-type: application/json 100 | 101 | === TEST HTTP_GET_4: GET with an invalid cookie returns 401 102 | ##################################################################################### 103 | # Verify that a 401 is received when there is an obviously invalid message credential 104 | ##################################################################################### 105 | 106 | --- config 107 | location /t { 108 | oauth_proxy on; 109 | oauth_proxy_cookie_name_prefix "example"; 110 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 111 | oauth_proxy_trusted_web_origin "https://www.example.com"; 112 | oauth_proxy_cors_enabled on; 113 | } 114 | 115 | --- request 116 | GET /t 117 | 118 | --- more_headers eval 119 | my $data; 120 | $data .= "origin: https://www.example.com\n"; 121 | $data .= "cookie: example-at=xxx"; 122 | $data; 123 | 124 | --- error_code: 401 125 | 126 | --- error_log 127 | Invalid data length after decoding from base64 128 | 129 | --- response_headers 130 | content-type: application/json 131 | 132 | === TEST HTTP_GET_5: GET returns correct CORS response headers with module errors 133 | ################################################################################# 134 | # Verify that when a 401 is received the SPA can read details due to CORS headers 135 | ################################################################################# 136 | 137 | --- config 138 | location /t { 139 | oauth_proxy on; 140 | oauth_proxy_cookie_name_prefix "example"; 141 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 142 | oauth_proxy_trusted_web_origin "https://www.example.com"; 143 | oauth_proxy_cors_enabled on; 144 | } 145 | 146 | --- request 147 | GET /t 148 | 149 | --- more_headers eval 150 | my $data; 151 | $data .= "origin: https://www.example.com\n"; 152 | $data .= "cookie: example-at="; 153 | $data; 154 | 155 | --- error_code: 401 156 | 157 | --- response_headers 158 | content-type: application/json 159 | access-control-allow-origin: https://www.example.com 160 | access-control-allow-credentials: true 161 | 162 | === TEST HTTP_GET_6: GET with a valid cookie returns 200 and an Authorization header 163 | ################################################################## 164 | # Ensure that GET requests work as expected with the correct input 165 | ################################################################## 166 | 167 | --- config 168 | location /t { 169 | oauth_proxy on; 170 | oauth_proxy_cookie_name_prefix "example"; 171 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 172 | oauth_proxy_trusted_web_origin "https://www.example.com"; 173 | oauth_proxy_cors_enabled on; 174 | 175 | proxy_pass http://localhost:1984/target; 176 | } 177 | location /target { 178 | add_header 'authorization' $http_authorization; 179 | return 200; 180 | } 181 | 182 | --- request 183 | GET /t 184 | 185 | --- more_headers eval 186 | my $data; 187 | $data .= "origin: https://www.example.com\n"; 188 | $data .= "cookie: example-at=" . $main::at_opaque_cookie . "\n"; 189 | $data; 190 | 191 | --- error_code: 200 192 | 193 | --- response_headers eval 194 | "authorization: Bearer " . $main::at_opaque; 195 | 196 | === TEST HTTP_GET_7: GET with a valid request and CORS enabled returns the correct CORS response headers 197 | ####################################################################### 198 | # Ensure that CORS headers are returned correctly for success responses 199 | ####################################################################### 200 | --- config 201 | location /t { 202 | oauth_proxy on; 203 | oauth_proxy_cookie_name_prefix "mycompany-myproduct"; 204 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 205 | oauth_proxy_trusted_web_origin "https://www.example.com"; 206 | oauth_proxy_cors_enabled on; 207 | 208 | proxy_pass http://localhost:1984/target; 209 | } 210 | location /target { 211 | return 200; 212 | } 213 | 214 | --- request 215 | GET /t 216 | 217 | --- more_headers eval 218 | my $data; 219 | $data .= "origin: https://www.example.com\n"; 220 | $data .= "cookie: mycompany-myproduct-at=" . $main::at_opaque_cookie . "\n"; 221 | $data; 222 | 223 | --- error_code: 200 224 | 225 | --- response_headers 226 | access-control-allow-origin: https://www.example.com 227 | access-control-allow-credentials: true 228 | vary: origin 229 | 230 | === TEST HTTP_GET_8: GET with a valid request and CORS disabled does not return CORS response headers 231 | ########################################################################### 232 | # Ensure that CORS headers can be handled by an API and not the OAuth proxy 233 | ########################################################################### 234 | --- config 235 | location /t { 236 | 237 | oauth_proxy on; 238 | oauth_proxy_cookie_name_prefix "example"; 239 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 240 | oauth_proxy_trusted_web_origin "http://www.example.com"; 241 | oauth_proxy_cors_enabled off; 242 | 243 | proxy_pass http://localhost:1984/target; 244 | } 245 | location /target { 246 | return 200; 247 | } 248 | 249 | --- request 250 | GET /t 251 | 252 | --- more_headers eval 253 | my $data; 254 | $data .= "origin: http://www.example.com\n"; 255 | $data .= "cookie: example-at=" . $main::at_opaque_cookie . "\n"; 256 | $data; 257 | 258 | --- error_code: 200 259 | 260 | --- response_headers 261 | access-control-allow-origin: 262 | access-control-allow-credentials: 263 | vary: 264 | 265 | === TEST HTTP_GET_9: GET with a bearer token is allowed when enabled 266 | ########################################################################################################## 267 | # Verify that mobile and SPA clients can use the same routes, where the first sends access tokens directly 268 | ########################################################################################################## 269 | --- config 270 | location /t { 271 | oauth_proxy on; 272 | oauth_proxy_cookie_name_prefix "example"; 273 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 274 | oauth_proxy_trusted_web_origin "https://www.example.com"; 275 | oauth_proxy_cors_enabled on; 276 | oauth_proxy_allow_tokens on; 277 | 278 | proxy_pass http://localhost:1984/target; 279 | } 280 | location /target { 281 | add_header 'authorization' $http_authorization; 282 | return 200; 283 | } 284 | 285 | --- request 286 | GET /t 287 | 288 | --- more_headers 289 | authorization: bearer xxx 290 | 291 | --- error_code: 200 292 | 293 | === TEST HTTP_GET_10: GET with a bearer token is denied when not enabled 294 | ####################################################################################################### 295 | # Verify that if a company wants to force mobile and SPA clients to use different routes they can do so 296 | ####################################################################################################### 297 | 298 | --- config 299 | location /t { 300 | oauth_proxy on; 301 | oauth_proxy_cookie_name_prefix "example"; 302 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 303 | oauth_proxy_trusted_web_origin "https://www.example.com"; 304 | oauth_proxy_cors_enabled on; 305 | oauth_proxy_allow_tokens off; 306 | } 307 | 308 | --- request 309 | GET /t 310 | 311 | --- more_headers 312 | origin: https://www.example.com 313 | authorization: bearer xxx 314 | 315 | --- error_code: 401 316 | 317 | --- error_log 318 | No AT cookie was found in the incoming request 319 | 320 | --- response_body_like chomp 321 | {"code":"unauthorized","message":"Access denied due to missing or invalid credentials"} 322 | 323 | === TEST HTTP_GET_11: Same origin GET with a valid cookie returns 200 and an Authorization header 324 | ######################################################################################################### 325 | # Ensure that GET requests work as expected when same domain hosting is used and no origin header is sent 326 | ######################################################################################################### 327 | 328 | --- config 329 | location /t { 330 | oauth_proxy on; 331 | oauth_proxy_cookie_name_prefix "example"; 332 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 333 | oauth_proxy_trusted_web_origin "https://www.example.com"; 334 | oauth_proxy_cors_enabled off; 335 | 336 | proxy_pass http://localhost:1984/target; 337 | } 338 | location /target { 339 | add_header 'authorization' $http_authorization; 340 | return 200; 341 | } 342 | 343 | --- request 344 | GET /t 345 | 346 | --- more_headers eval 347 | my $data; 348 | $data .= "cookie: example-at=" . $main::at_opaque_cookie . "\n"; 349 | $data; 350 | 351 | --- error_code: 200 352 | 353 | --- response_headers eval 354 | "authorization: Bearer " . $main::at_opaque; -------------------------------------------------------------------------------- /testing/integration/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #################################################################################### 4 | #Run some integration tests against the deployed NGINX system with the custom module 5 | #################################################################################### 6 | 7 | API_URL='http://localhost:8081/api' 8 | WEB_ORIGIN='https://www.example.com' 9 | ACCESS_TOKEN='42665300-efe8-419d-be52-07b53e208f46' 10 | CSRF_TOKEN='njowdfew098723rhjl' 11 | RESPONSE_FILE=response.txt 12 | 13 | # 14 | # Ensure that we are in the folder containing this script 15 | # 16 | cd "$(dirname "${BASH_SOURCE[0]}")" 17 | ENCRYPT_UTIL=$(pwd)/encrypt.js 18 | 19 | # 20 | # Get encrypted values for testing 21 | # 22 | ENCRYPTED_ACCESS_TOKEN=$(node $ENCRYPT_UTIL "$ACCESS_TOKEN") 23 | ENCRYPTED_CSRF_TOKEN=$(node $ENCRYPT_UTIL "$CSRF_TOKEN") 24 | 25 | # 26 | # Get a header value from the HTTP response file 27 | # 28 | function getHeaderValue(){ 29 | local _HEADER_NAME=$1 30 | local _HEADER_VALUE=$(cat $RESPONSE_FILE | grep -i "^$_HEADER_NAME" | sed -r "s/^$_HEADER_NAME: (.*)$/\1/i") 31 | local _HEADER_VALUE=${_HEADER_VALUE%$'\r'} 32 | echo $_HEADER_VALUE 33 | } 34 | 35 | # 36 | # Verify that browser pre-flight OPTIONS requests from a malicious site are denied CORS access 37 | # 38 | echo '1. Testing OPTIONS request for an untrusted web origin ...' 39 | HTTP_STATUS=$(curl -i -s -X OPTIONS "$API_URL" \ 40 | -H "origin: https://malicious-site.com" \ 41 | -o $RESPONSE_FILE -w '%{http_code}') 42 | if [ "$HTTP_STATUS" != '204' ]; then 43 | >&2 echo "*** OPTIONS request failed, status: $HTTP_STATUS" 44 | exit 1 45 | fi 46 | 47 | ORIGIN=$(getHeaderValue 'access-control-allow-origin') 48 | if [ "$ORIGIN" != '' ]; then 49 | >&2 echo '*** The CORS access-control-allow-origin response header was granted incorrectly' 50 | exit 1 51 | fi 52 | 53 | CREDENTIALS=$(getHeaderValue 'access-control-allow-credentials') 54 | if [ "$CREDENTIALS" != '' ]; then 55 | >&2 echo '*** The CORS access-control-allow-credentials response header was granted incorrectly' 56 | exit 1 57 | fi 58 | echo '1. OPTIONS request successfully denied access to an untrusted web origin' 59 | 60 | # 61 | # Verify that browser pre-flight requests from a valid origin succeed and return the correct headers 62 | # 63 | echo '2. Testing OPTIONS request for a valid web origin ...' 64 | HTTP_STATUS=$(curl -i -s -X OPTIONS "$API_URL" \ 65 | -H "origin: $WEB_ORIGIN" \ 66 | -H "access-control-request-headers: x-example-csrf" \ 67 | -o $RESPONSE_FILE -w '%{http_code}') 68 | if [ "$HTTP_STATUS" != '204' ]; then 69 | >&2 echo "*** OPTIONS request failed, status: $HTTP_STATUS" 70 | exit 1 71 | fi 72 | 73 | ORIGIN=$(getHeaderValue 'access-control-allow-origin') 74 | if [ "$ORIGIN" != "$WEB_ORIGIN" ]; then 75 | >&2 echo '*** The CORS access-control-allow-origin response header was not set correctly' 76 | exit 1 77 | fi 78 | 79 | CREDENTIALS=$(getHeaderValue 'access-control-allow-credentials') 80 | if [ "$CREDENTIALS" != 'true' ]; then 81 | >&2 echo '*** The CORS access-control-allow-credentials response header was not set correctly' 82 | exit 1 83 | fi 84 | 85 | VARY=$(getHeaderValue 'vary') 86 | if [ "$VARY" != 'origin,access-control-request-headers' ]; then 87 | >&2 echo '*** The CORS vary response header was not set correctly' 88 | exit 1 89 | fi 90 | 91 | METHODS=$(getHeaderValue 'access-control-allow-methods') 92 | if [ "$METHODS" != 'OPTIONS,HEAD,GET,POST,PUT,PATCH,DELETE' ]; then 93 | >&2 echo '*** The CORS access-control-allow-methods response header was not set correctly' 94 | exit 1 95 | fi 96 | 97 | HEADERS=$(getHeaderValue 'access-control-allow-headers') 98 | if [ "$HEADERS" != 'x-example-csrf' ]; then 99 | >&2 echo '*** The CORS access-control-allow-headers response header was not set correctly' 100 | exit 1 101 | fi 102 | 103 | MAXAGE=$(getHeaderValue 'access-control-max-age') 104 | if [ "$MAXAGE" != '86400' ]; then 105 | >&2 echo '*** The CORS access-control-max-age response header was not set correctly' 106 | exit 1 107 | fi 108 | echo '2. OPTIONS request returned all correct CORS headers for a valid web origin' 109 | 110 | # 111 | # Verify that the main browser request from a malicious site is denied CORS access 112 | # 113 | echo '3. Testing GET request for an untrusted web origin ...' 114 | HTTP_STATUS=$(curl -i -s -X GET "$API_URL" \ 115 | -H "origin: https://malicious-site.com" \ 116 | -H "cookie: example-at=$ENCRYPTED_ACCESS_TOKEN" \ 117 | -o $RESPONSE_FILE -w '%{http_code}') 118 | if [ "$HTTP_STATUS" != '401' ]; then 119 | >&2 echo "*** GET request failed, status: $HTTP_STATUS" 120 | exit 1 121 | fi 122 | ORIGIN=$(getHeaderValue 'access-control-allow-origin') 123 | if [ "$ORIGIN" != '' ]; then 124 | >&2 echo '*** The CORS access-control-allow-origin response header was granted incorrectly' 125 | exit 1 126 | fi 127 | 128 | CREDENTIALS=$(getHeaderValue 'access-control-allow-credentials') 129 | if [ "$CREDENTIALS" != '' ]; then 130 | >&2 echo '*** The CORS access-control-allow-credentials response header was granted incorrectly' 131 | exit 1 132 | fi 133 | echo '3. GET request successfully denied access to an untrusted web origin' 134 | 135 | # 136 | # Verify that the main browser request from a valid origin succeeds and returns the correct headers 137 | # 138 | echo '4. Testing GET request for a valid web origin ...' 139 | HTTP_STATUS=$(curl -i -s -X GET "$API_URL" \ 140 | -H "origin: $WEB_ORIGIN" \ 141 | -H "cookie: example-at=$ENCRYPTED_ACCESS_TOKEN" \ 142 | -o $RESPONSE_FILE -w '%{http_code}') 143 | if [ "$HTTP_STATUS" != '200' ]; then 144 | >&2 echo "*** GET request failed, status: $HTTP_STATUS" 145 | exit 1 146 | fi 147 | 148 | ORIGIN=$(getHeaderValue 'access-control-allow-origin') 149 | if [ "$ORIGIN" != "$WEB_ORIGIN" ]; then 150 | >&2 echo '*** The CORS access-control-allow-origin response header was not set correctly' 151 | exit 1 152 | fi 153 | 154 | CREDENTIALS=$(getHeaderValue 'access-control-allow-credentials') 155 | if [ "$CREDENTIALS" != 'true' ]; then 156 | >&2 echo '*** The CORS access-control-allow-credentials response header was not set correctly' 157 | exit 1 158 | fi 159 | 160 | VARY=$(getHeaderValue 'vary') 161 | if [ "$VARY" != 'origin' ]; then 162 | >&2 echo '*** The CORS vary response header was not set correctly' 163 | exit 1 164 | fi 165 | echo '4. GET request returned all correct CORS headers for a valid web origin' 166 | 167 | # 168 | # Verify that SPA clients can read error responses from the module, by sending no credential but the correct origin 169 | # 170 | echo '5. Testing CORS headers for error responses to the SPA ...' 171 | HTTP_STATUS=$(curl -i -s -X POST "$API_URL" \ 172 | -H "origin: $WEB_ORIGIN" \ 173 | -o $RESPONSE_FILE -w '%{http_code}') 174 | if [ "$HTTP_STATUS" != '401' ]; then 175 | >&2 echo '*** Request with no credential did not result in the expected error' 176 | exit 1 177 | fi 178 | ORIGIN=$(getHeaderValue 'Access-Control-Allow-Origin') 179 | if [ "$ORIGIN" != "$WEB_ORIGIN" ]; then 180 | >&2 echo '*** CORS headers do not allow the SPA to read the error response' 181 | exit 1 182 | fi 183 | echo '5. CORS error responses returned to the SPA have the correct CORS headers' 184 | 185 | # 186 | # Verify that access is denied for GET requests without a token or cookie 187 | # 188 | echo '6. Testing POST with no credential ...' 189 | HTTP_STATUS=$(curl -i -s -X POST "$API_URL" \ 190 | -o $RESPONSE_FILE -w '%{http_code}') 191 | if [ "$HTTP_STATUS" != '401' ]; then 192 | >&2 echo '*** POST with no credential did not result in the expected error' 193 | exit 1 194 | fi 195 | echo '6. POST with no credential failed with the expected error' 196 | JSON=$(tail -n 1 $RESPONSE_FILE) 197 | echo $JSON | jq 198 | 199 | # 200 | # Verify that an access token sent from a mobile client is passed through to the API 201 | # 202 | echo '7. Testing POST from mobile client with an access token ...' 203 | HTTP_STATUS=$(curl -i -s -X POST "$API_URL" \ 204 | -H "Authorization: Bearer $ACCESS_TOKEN" \ 205 | -o $RESPONSE_FILE -w '%{http_code}') 206 | if [ "$HTTP_STATUS" != '200' ]; then 207 | >&2 echo "*** POST from mobile client failed, status: $HTTP_STATUS" 208 | exit 1 209 | fi 210 | echo '7. POST from mobile client was successfully routed to the API' 211 | JSON=$(tail -n 1 $RESPONSE_FILE) 212 | echo $JSON | jq 213 | 214 | # 215 | # Verify that a cookie sent on a GET request is correctly decrypted 216 | # 217 | echo '8. Testing GET with a valid encrypted cookie ...' 218 | HTTP_STATUS=$(curl -i -s -X GET "$API_URL" \ 219 | -H "origin: $WEB_ORIGIN" \ 220 | -H "cookie: example-at=$ENCRYPTED_ACCESS_TOKEN" \ 221 | -o $RESPONSE_FILE -w '%{http_code}') 222 | if [ "$HTTP_STATUS" != '200' ]; then 223 | >&2 echo "*** GET with a valid encrypted cookie failed, status: $HTTP_STATUS" 224 | exit 1 225 | fi 226 | echo '8. GET with a valid encrypted cookie was successfully routed to the API' 227 | JSON=$(tail -n 1 $RESPONSE_FILE) 228 | echo $JSON | jq 229 | 230 | # 231 | # Verify that data changing commands require a CSRF cookie 232 | # 233 | echo '9. Testing POST with missing CSRF cookie ...' 234 | HTTP_STATUS=$(curl -i -s -X POST "$API_URL" \ 235 | -H "origin: $WEB_ORIGIN" \ 236 | -H "cookie: example-at=$ENCRYPTED_ACCESS_TOKEN" \ 237 | -o $RESPONSE_FILE -w '%{http_code}') 238 | if [ "$HTTP_STATUS" != '401' ]; then 239 | >&2 echo '*** POST with a missing CSRF cookie did not result in the expected error' 240 | exit 1 241 | fi 242 | echo '9. POST with a missing CSRF cookie was successfully rejected' 243 | JSON=$(tail -n 1 $RESPONSE_FILE) 244 | echo $JSON | jq 245 | 246 | # 247 | # Verify that data changing commands require a CSRF header 248 | # 249 | echo '10. Testing POST with missing CSRF header ...' 250 | HTTP_STATUS=$(curl -i -s -X POST "$API_URL" \ 251 | -H "origin: $WEB_ORIGIN" \ 252 | -H "cookie: example-at=$ENCRYPTED_ACCESS_TOKEN" \ 253 | -H "cookie: example-csrf=$ENCRYPTED_CSRF_TOKEN" \ 254 | -o $RESPONSE_FILE -w '%{http_code}') 255 | if [ "$HTTP_STATUS" != '401' ]; then 256 | >&2 echo '*** POST with a missing CSRF header did not result in the expected error' 257 | exit 1 258 | fi 259 | echo '10. POST with a missing CSRF header was successfully rejected' 260 | JSON=$(tail -n 1 $RESPONSE_FILE) 261 | echo $JSON | jq 262 | 263 | # 264 | # Verify that double submit cookie checks work if the cookie and value do not match 265 | # 266 | echo '11. Testing POST with incorrect CSRF header ...' 267 | HTTP_STATUS=$(curl -i -s -X POST "$API_URL" \ 268 | -H "origin: $WEB_ORIGIN" \ 269 | -H "cookie: example-at=$ENCRYPTED_ACCESS_TOKEN" \ 270 | -H "cookie: example-csrf=$ENCRYPTED_CSRF_TOKEN" \ 271 | -H "x-example-csrf: x$CSRF_TOKEN" \ 272 | -o $RESPONSE_FILE -w '%{http_code}') 273 | if [ "$HTTP_STATUS" != '401' ]; then 274 | >&2 echo '*** POST with an incorrect CSRF header did not result in the expected error' 275 | exit 1 276 | fi 277 | echo '11. POST with an incorrect CSRF header was successfully rejected' 278 | JSON=$(tail -n 1 $RESPONSE_FILE) 279 | echo $JSON | jq 280 | 281 | # 282 | # Verify that double submit cookie checks succeed with the correct data 283 | # 284 | echo '12. Testing POST with correct CSRF cookie and header ...' 285 | HTTP_STATUS=$(curl -i -s -X POST "$API_URL" \ 286 | -H "origin: $WEB_ORIGIN" \ 287 | -H "cookie: example-at=$ENCRYPTED_ACCESS_TOKEN" \ 288 | -H "cookie: example-csrf=$ENCRYPTED_CSRF_TOKEN" \ 289 | -H "x-example-csrf: $CSRF_TOKEN" \ 290 | -o $RESPONSE_FILE -w '%{http_code}') 291 | if [ "$HTTP_STATUS" != '200' ]; then 292 | >&2 echo '*** POST with correct CSRF cookie and header did not succeed' 293 | exit 1 294 | fi 295 | echo '12. POST with correct CSRF cookie and header was successfully routed to the API' 296 | JSON=$(tail -n 1 $RESPONSE_FILE) 297 | echo $JSON | jq 298 | 299 | # 300 | # Verify that malformed cookies are correctly rejected 301 | # 302 | echo '13. Testing GET with malformed access token cookie ...' 303 | HTTP_STATUS=$(curl -i -s -X GET "$API_URL" \ 304 | -H "origin: $WEB_ORIGIN" \ 305 | -H "cookie: example-at=" \ 306 | -o $RESPONSE_FILE -w '%{http_code}') 307 | if [ "$HTTP_STATUS" != '401' ]; then 308 | >&2 echo '*** GET with malformed access token cookie did not result in the expected error' 309 | exit 1 310 | fi 311 | echo '13. GET with malformed access token cookie was successfully rejected' 312 | JSON=$(tail -n 1 $RESPONSE_FILE) 313 | echo $JSON | jq 314 | 315 | # 316 | # Output valgrind results once finished 317 | # 318 | echo 'Retrieving valgrind memory results ...' 319 | DOCKER_CONTAINER_ID=$(docker container ls | grep "nginx_${DISTRO}" | awk '{print $1}') 320 | echo $DOCKER_CONTAINER_ID 321 | docker cp "$DOCKER_CONTAINER_ID:/valgrind-results.txt" . 322 | cat valgrind-results.txt -------------------------------------------------------------------------------- /testing/t/http_post.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | ####################################################################### 4 | # Runs HTTP tests to verify security behavior from a client's viewpoint 5 | ####################################################################### 6 | 7 | use strict; 8 | use warnings; 9 | use Test::Nginx::Socket 'no_plan'; 10 | 11 | SKIP: { 12 | our $at_opaque = "42665300-efe8-419d-be52-07b53e208f46"; 13 | our $at_opaque_cookie = "AcYBf995tTBVsLtQLvOuLUZXHm2c-XqP8t7SKmhBiQtzy5CAw4h_RF6rXyg6kHrvhb8x4WaLQC6h3mw6a3O3Q9A"; 14 | 15 | our $csrf_token = "pQguFsD6hFjnyYjaeC5KyijcWS6AvkJHiUmY7dLUsuTKsLAITLiJHVqsCdQpaGYO"; 16 | our $csrf_cookie = "AfctuC2zuBeZoQHfbopmpQyOADYU6Tp9raMEA-2EhWp4I3HtoiAtoP-H2U_PIrF7O0ZQ0nwE7VmWcl3BAY6bGlv4_EGqToyh4lOqynkSlBByxixJY-kA3bIFufJl"; 17 | 18 | run_tests(); 19 | } 20 | 21 | __DATA__ 22 | 23 | === TEST HTTP_POST_1: POST with no CSRF cookie returns 401 24 | ############################################################################### 25 | # Data changing commands require CSRF details in line with OWASP best practices 26 | ############################################################################### 27 | 28 | --- config 29 | location /t { 30 | oauth_proxy on; 31 | oauth_proxy_cookie_name_prefix "example"; 32 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 33 | oauth_proxy_trusted_web_origin "https://www.example.com"; 34 | oauth_proxy_cors_enabled on; 35 | } 36 | 37 | --- request 38 | POST /t 39 | 40 | --- more_headers eval 41 | my $data; 42 | $data .= "origin: https://www.example.com\n"; 43 | $data .= "cookie: example-at=" . $main::at_opaque_cookie . "\n"; 44 | $data; 45 | 46 | --- error_code: 401 47 | 48 | --- error_log 49 | No CSRF cookie was found in the incoming request 50 | 51 | --- response_headers 52 | content-type: application/json 53 | 54 | === TEST HTTP_POST_2: POST with no CSRF request header returns 401 55 | ############################################################ 56 | # A request header should be sent along with the CSRF cookie 57 | ############################################################ 58 | 59 | --- config 60 | location /t { 61 | oauth_proxy on; 62 | oauth_proxy_cookie_name_prefix "example"; 63 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 64 | oauth_proxy_trusted_web_origin "https://www.example.com"; 65 | oauth_proxy_cors_enabled on; 66 | } 67 | 68 | --- request 69 | POST /t 70 | 71 | --- more_headers eval 72 | my $data; 73 | $data .= "origin: https://www.example.com\n"; 74 | $data .= "cookie: example-at=" . $main::at_opaque_cookie . "; example-csrf=" . $main::csrf_cookie . "\n"; 75 | $data; 76 | 77 | --- error_code: 401 78 | 79 | --- error_log 80 | A data changing request did not have a CSRF header 81 | 82 | --- response_headers 83 | content-type: application/json 84 | 85 | === TEST HTTP_POST_3: POST with mismatched CSRF request header and cookie returns 401 86 | ################################################################## 87 | # The request header value must match that in the encrypted cookie 88 | ################################################################## 89 | 90 | --- config 91 | location /t { 92 | oauth_proxy on; 93 | oauth_proxy_cookie_name_prefix "example"; 94 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 95 | oauth_proxy_trusted_web_origin "https://www.example.com"; 96 | oauth_proxy_cors_enabled on; 97 | } 98 | 99 | --- request 100 | POST /t 101 | 102 | --- more_headers eval 103 | my $data; 104 | $data .= "origin: https://www.example.com\n"; 105 | $data .= "x-example-csrf: x" . $main::csrf_token . "\n"; 106 | $data .= "cookie: example-at=" . $main::at_opaque_cookie . "; example-csrf=" . $main::csrf_cookie . "\n"; 107 | $data; 108 | 109 | --- error_code: 401 110 | 111 | --- error_log 112 | The CSRF request header did not match the value in the encrypted CSRF cookie 113 | 114 | --- response_headers 115 | content-type: application/json 116 | 117 | === TEST HTTP_POST_4: POST with 2 valid cookies and a CSRF token returns 200 118 | ############################################################# 119 | # Verify that the happy path works for data changing commands 120 | ############################################################# 121 | 122 | --- config 123 | location /t { 124 | oauth_proxy on; 125 | oauth_proxy_cookie_name_prefix "example"; 126 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 127 | oauth_proxy_trusted_web_origin "https://www.example.com"; 128 | oauth_proxy_cors_enabled on; 129 | 130 | proxy_pass http://localhost:1984/target; 131 | } 132 | location /target { 133 | add_header 'authorization' $http_authorization; 134 | return 200; 135 | } 136 | 137 | --- request 138 | POST /t 139 | 140 | --- more_headers eval 141 | my $data; 142 | $data .= "origin: https://www.example.com\n"; 143 | $data .= "x-example-csrf: " . $main::csrf_token . "\n"; 144 | $data .= "cookie: example-at=" . $main::at_opaque_cookie . "; example-csrf=" . $main::csrf_cookie . "\n"; 145 | $data; 146 | 147 | --- error_code: 200 148 | 149 | --- response_headers eval 150 | "authorization: Bearer " . $main::at_opaque 151 | 152 | === TEST HTTP_POST_5: POST with 2 locations and same details works as expected 153 | ###################################################################### 154 | # Verify that the happy path works for multiple configuration sections 155 | ###################################################################### 156 | 157 | --- config 158 | location /api1 { 159 | oauth_proxy on; 160 | oauth_proxy_cookie_name_prefix "example"; 161 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 162 | oauth_proxy_trusted_web_origin "https://www.example.com"; 163 | oauth_proxy_cors_enabled on; 164 | 165 | proxy_pass http://localhost:1984/target; 166 | } 167 | location /api2 { 168 | oauth_proxy on; 169 | oauth_proxy_cookie_name_prefix "example"; 170 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 171 | oauth_proxy_trusted_web_origin "https://www.example.com"; 172 | oauth_proxy_cors_enabled on; 173 | 174 | proxy_pass http://localhost:1984/target; 175 | } 176 | location /target { 177 | add_header 'authorization' $http_authorization; 178 | return 200; 179 | } 180 | 181 | --- request 182 | POST /api2 183 | 184 | --- more_headers eval 185 | my $data; 186 | $data .= "origin: https://www.example.com\n"; 187 | $data .= "x-example-csrf: " . $main::csrf_token . "\n"; 188 | $data .= "cookie: example-at=" . $main::at_opaque_cookie . "; example-csrf=" . $main::csrf_cookie . "\n"; 189 | $data; 190 | 191 | --- error_code: 200 192 | 193 | --- response_headers eval 194 | "authorization: Bearer " . $main::at_opaque 195 | 196 | === TEST HTTP_POST_6: POST with 2 locations and different details works as expected 197 | ######################################################################################## 198 | # Verify that the happy path works for data changing commands with independent locations 199 | ######################################################################################## 200 | 201 | --- config 202 | location /api1 { 203 | oauth_proxy on; 204 | oauth_proxy_cookie_name_prefix "example1"; 205 | oauth_proxy_encryption_key "7b99279ab87533d3c238db874a842a91ee26a76027f3c03c317504963d2c9926"; 206 | oauth_proxy_trusted_web_origin "https://www.example1.com"; 207 | oauth_proxy_cors_enabled on; 208 | 209 | proxy_pass http://localhost:1984/target; 210 | } 211 | location /api2 { 212 | oauth_proxy on; 213 | oauth_proxy_cookie_name_prefix "example2"; 214 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 215 | oauth_proxy_trusted_web_origin "https://www.example2.com"; 216 | oauth_proxy_cors_enabled on; 217 | 218 | proxy_pass http://localhost:1984/target; 219 | } 220 | location /target { 221 | add_header 'authorization' $http_authorization; 222 | return 200; 223 | } 224 | 225 | --- request 226 | POST /api2 227 | 228 | --- more_headers eval 229 | my $data; 230 | $data .= "origin: https://www.example2.com\n"; 231 | $data .= "x-example2-csrf: " . $main::csrf_token . "\n"; 232 | $data .= "cookie: example2-at=" . $main::at_opaque_cookie . "; example2-csrf=" . $main::csrf_cookie . "\n"; 233 | $data; 234 | 235 | --- error_code: 200 236 | 237 | --- response_headers eval 238 | "authorization: Bearer " . $main::at_opaque 239 | 240 | === TEST HTTP_POST_7: POST with parent child locations uses inherited properties as expected 241 | ######################################################################################## 242 | # Verify that the happy path works for data changing commands with independent locations 243 | ######################################################################################## 244 | 245 | --- config 246 | location /api { 247 | oauth_proxy on; 248 | oauth_proxy_cookie_name_prefix "example"; 249 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 250 | oauth_proxy_trusted_web_origin "https://www.example.com"; 251 | oauth_proxy_cors_enabled on; 252 | 253 | location /api/products { 254 | proxy_pass http://localhost:1984/target; 255 | } 256 | } 257 | location /target { 258 | add_header 'authorization' $http_authorization; 259 | return 200; 260 | } 261 | 262 | --- request 263 | POST /api/products 264 | 265 | --- more_headers eval 266 | my $data; 267 | $data .= "origin: https://www.example.com\n"; 268 | $data .= "x-example-csrf: " . $main::csrf_token . "\n"; 269 | $data .= "cookie: example-at=" . $main::at_opaque_cookie . "; example-csrf=" . $main::csrf_cookie . "\n"; 270 | $data; 271 | 272 | --- error_code: 200 273 | 274 | --- response_headers eval 275 | "authorization: Bearer " . $main::at_opaque 276 | 277 | === TEST HTTP_POST_8: POST with HTTP/2 and multiple cookie headers 278 | ################################################################################################# 279 | # When running with HTTP/2 the cookie header can be sent multiple times so verify that this works 280 | ################################################################################################# 281 | 282 | --- config 283 | location /api { 284 | oauth_proxy on; 285 | oauth_proxy_cookie_name_prefix "example"; 286 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 287 | oauth_proxy_trusted_web_origin "https://www.example.com"; 288 | oauth_proxy_cors_enabled on; 289 | 290 | location /api/products { 291 | proxy_pass http://localhost:1984/target; 292 | } 293 | } 294 | location /target { 295 | add_header 'authorization' $http_authorization; 296 | return 200; 297 | } 298 | 299 | --- request 300 | POST /api/products 301 | 302 | --- more_headers eval 303 | my $data; 304 | $data .= "origin: https://www.example.com\n"; 305 | $data .= "x-example-csrf: " . $main::csrf_token . "\n"; 306 | $data .= "cookie: example-at=" . $main::at_opaque_cookie . "\n"; 307 | $data .= "cookie: example-csrf=" . $main::csrf_cookie . "\n"; 308 | $data; 309 | 310 | --- error_code: 200 311 | 312 | --- response_headers eval 313 | "authorization: Bearer " . $main::at_opaque 314 | 315 | === TEST HTTP_POST_9: POST with a cookie prefix on the maximum length boundary 316 | ########################################################### 317 | # This ensures no overflows if a long cookie prefix is used 318 | ########################################################### 319 | 320 | --- config 321 | location /api { 322 | oauth_proxy on; 323 | oauth_proxy_cookie_name_prefix "myveryveryverylongcompanyname-myveryveryveryverylongproductname"; 324 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 325 | oauth_proxy_trusted_web_origin "https://www.example.com"; 326 | oauth_proxy_cors_enabled on; 327 | 328 | location /api/products { 329 | proxy_pass http://localhost:1984/target; 330 | } 331 | } 332 | location /target { 333 | add_header 'authorization' $http_authorization; 334 | return 200; 335 | } 336 | 337 | --- request 338 | POST /api/products 339 | 340 | --- more_headers eval 341 | my $data; 342 | $data .= "origin: https://www.example.com\n"; 343 | $data .= "x-myveryveryverylongcompanyname-myveryveryveryverylongproductname-csrf: " . $main::csrf_token . "\n"; 344 | $data .= "cookie: myveryveryverylongcompanyname-myveryveryveryverylongproductname-at=" . $main::at_opaque_cookie . "; myveryveryverylongcompanyname-myveryveryveryverylongproductname-csrf=" . $main::csrf_cookie . "\n"; 345 | $data; 346 | 347 | --- error_code: 200 348 | 349 | --- response_headers eval 350 | "authorization: Bearer " . $main::at_opaque -------------------------------------------------------------------------------- /src/oauth_proxy_handler.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Curity AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include "oauth_proxy.h" 23 | 24 | /* Forward declarations of implementation functions */ 25 | static ngx_flag_t is_data_changing_command(ngx_http_request_t *request); 26 | static ngx_str_t *get_header(ngx_http_request_t *request, const char *name); 27 | static ngx_int_t verify_web_origin(const oauth_proxy_configuration_t *config, const ngx_str_t *web_origin); 28 | static ngx_int_t apply_csrf_checks(ngx_http_request_t *request, const oauth_proxy_configuration_t *config, const ngx_str_t *web_origin); 29 | static ngx_int_t add_authorization_header(ngx_http_request_t *request, const ngx_str_t* token_value); 30 | static ngx_int_t write_options_response(ngx_http_request_t *request, oauth_proxy_configuration_t *module_location_config); 31 | static ngx_int_t write_error_response(ngx_http_request_t *request, ngx_int_t status, oauth_proxy_configuration_t *config); 32 | static ngx_int_t add_cors_response_headers(ngx_http_request_t *request, oauth_proxy_configuration_t *config, u_char is_error); 33 | 34 | /* 35 | * The main exported handler method, called for each incoming API request 36 | */ 37 | ngx_int_t oauth_proxy_handler_main(ngx_http_request_t *request) 38 | { 39 | oauth_proxy_configuration_t *module_location_config = NULL; 40 | ngx_str_t *authorization_header = NULL; 41 | ngx_str_t *web_origin = NULL; 42 | ngx_str_t at_cookie_encrypted_hex; 43 | ngx_str_t access_token; 44 | ngx_int_t ret_code = NGX_OK; 45 | 46 | /* Return immediately for locations where the module is not used */ 47 | module_location_config = oauth_proxy_module_get_location_configuration(request); 48 | if (!module_location_config->enabled) 49 | { 50 | return NGX_DECLINED; 51 | } 52 | 53 | if (request->method == NGX_HTTP_OPTIONS) 54 | { 55 | if (module_location_config->cors_enabled) 56 | { 57 | /* When CORS is enabled, avoid needing to handling pre-flight OPTIONS requests in the API */ 58 | return write_options_response(request, module_location_config); 59 | } 60 | 61 | /* If CORS is disabled, return immediately and the request will be routed to the target API */ 62 | return NGX_OK; 63 | } 64 | 65 | /* Pass the request through if it has an Authorization header, eg from a mobile client that uses the same route as an SPA */ 66 | if (module_location_config->allow_tokens) 67 | { 68 | authorization_header = get_header(request, "authorization"); 69 | if (authorization_header != NULL) 70 | { 71 | return NGX_OK; 72 | } 73 | } 74 | 75 | /* Verify the web origin, which is sent by all modern browsers */ 76 | if (module_location_config->cors_enabled || is_data_changing_command(request)) 77 | { 78 | web_origin = get_header(request, "origin"); 79 | if (web_origin == NULL) 80 | { 81 | ret_code = NGX_HTTP_UNAUTHORIZED; 82 | ngx_log_error(NGX_LOG_WARN, request->connection->log, 0, "The request did not have an origin header"); 83 | return write_error_response(request, ret_code, module_location_config); 84 | } 85 | 86 | ret_code = verify_web_origin(module_location_config, web_origin); 87 | if (ret_code != NGX_OK) 88 | { 89 | ret_code = NGX_HTTP_UNAUTHORIZED; 90 | ngx_log_error(NGX_LOG_WARN, request->connection->log, 0, "The request was from an untrusted web origin"); 91 | return write_error_response(request, ret_code, module_location_config); 92 | } 93 | } 94 | 95 | /* For data changing commands, apply double submit cookie checks in line with OWASP best practices */ 96 | if (is_data_changing_command(request)) 97 | { 98 | ret_code = apply_csrf_checks(request, module_location_config, web_origin); 99 | if (ret_code != NGX_OK) 100 | { 101 | return write_error_response(request, ret_code, module_location_config); 102 | } 103 | } 104 | 105 | /* This returns 0 when there is a single cookie header (HTTP 1.1) or > 0 when there are multiple cookie headers (HTTP 2.0) */ 106 | ret_code = oauth_proxy_utils_get_cookie(request, &at_cookie_encrypted_hex, &module_location_config->cookie_name_prefix, (u_char *)"-at"); 107 | if (ret_code == NGX_DECLINED) 108 | { 109 | ret_code = NGX_HTTP_UNAUTHORIZED; 110 | ngx_log_error(NGX_LOG_WARN, request->connection->log, 0, "No AT cookie was found in the incoming request"); 111 | return write_error_response(request, ret_code, module_location_config); 112 | } 113 | 114 | /* Try to decrypt the cookie to get the access token */ 115 | ret_code = oauth_proxy_decryption_decrypt_cookie(request, &access_token, &at_cookie_encrypted_hex, &module_location_config->encryption_key); 116 | if (ret_code != NGX_OK) 117 | { 118 | return write_error_response(request, ret_code, module_location_config); 119 | } 120 | 121 | /* Update the authorization header in the headers in, to forward to the API via proxy_pass */ 122 | ret_code = add_authorization_header(request, &access_token); 123 | if (ret_code != NGX_OK) 124 | { 125 | return write_error_response(request, ret_code, module_location_config); 126 | } 127 | 128 | /* Finally update CORS headers, which must be done for both the pre-flight request and also the main API request */ 129 | if (module_location_config->cors_enabled) 130 | { 131 | add_cors_response_headers(request, module_location_config, 0); 132 | } 133 | 134 | return NGX_OK; 135 | } 136 | 137 | /* 138 | * CORS and CSRF checks work differently for this type of command 139 | */ 140 | static ngx_flag_t is_data_changing_command(ngx_http_request_t *request) 141 | { 142 | return request->method == NGX_HTTP_POST || 143 | request->method == NGX_HTTP_PUT || 144 | request->method == NGX_HTTP_PATCH || 145 | request->method == NGX_HTTP_DELETE ? 1 : 0; 146 | } 147 | 148 | /* 149 | * Return the authorization header if it exists 150 | */ 151 | static ngx_str_t *get_header(ngx_http_request_t *request, const char *name) 152 | { 153 | return oauth_proxy_utils_get_header_in(request, (u_char *)name, ngx_strlen(name)); 154 | } 155 | 156 | /* 157 | * Ensure that incoming requests have the origin header that all modern browsers send 158 | */ 159 | static ngx_int_t verify_web_origin(const oauth_proxy_configuration_t *config, const ngx_str_t *web_origin) 160 | { 161 | ngx_str_t *trusted_web_origins = NULL; 162 | ngx_str_t trusted_web_origin; 163 | ngx_uint_t i = 0; 164 | 165 | trusted_web_origins = config->trusted_web_origins->elts; 166 | for (i = 0; i < config->trusted_web_origins->nelts; i++) 167 | { 168 | trusted_web_origin = trusted_web_origins[i]; 169 | 170 | if (web_origin->len == trusted_web_origin.len && 171 | ngx_strncasecmp(web_origin->data, trusted_web_origin.data, web_origin->len) == 0) 172 | { 173 | return NGX_OK; 174 | } 175 | } 176 | 177 | return NGX_ERROR; 178 | } 179 | 180 | /* 181 | * For data changing commands we make extra CSRF checks in line with OWASP best practices 182 | */ 183 | static ngx_int_t apply_csrf_checks(ngx_http_request_t *request, const oauth_proxy_configuration_t *config, const ngx_str_t *web_origin) 184 | { 185 | ngx_str_t csrf_cookie_encrypted_hex; 186 | u_char csrf_header_name[128]; 187 | ngx_str_t *csrf_header_value = NULL; 188 | ngx_str_t csrf_token; 189 | ngx_int_t ret_code = NGX_OK; 190 | 191 | /* This returns 0 when there is a single cookie header or > 0 when there are multiple cookie headers */ 192 | ret_code = oauth_proxy_utils_get_cookie(request, &csrf_cookie_encrypted_hex, &config->cookie_name_prefix, (u_char *)"-csrf"); 193 | if (ret_code == NGX_DECLINED) 194 | { 195 | ngx_log_error(NGX_LOG_WARN, request->connection->log, 0, "No CSRF cookie was found in the incoming request"); 196 | return NGX_HTTP_UNAUTHORIZED; 197 | } 198 | 199 | oauth_proxy_utils_get_csrf_header_name(csrf_header_name, config); 200 | csrf_header_value = oauth_proxy_utils_get_header_in(request, csrf_header_name, ngx_strlen(csrf_header_name)); 201 | if (csrf_header_value == NULL) 202 | { 203 | ngx_log_error(NGX_LOG_WARN, request->connection->log, 0, "A data changing request did not have a CSRF header"); 204 | return NGX_HTTP_UNAUTHORIZED; 205 | } 206 | 207 | ret_code = oauth_proxy_decryption_decrypt_cookie(request, &csrf_token, &csrf_cookie_encrypted_hex, &config->encryption_key); 208 | if (ret_code != NGX_OK) 209 | { 210 | return ret_code; 211 | } 212 | 213 | if (ngx_strcmp(csrf_token.data, csrf_header_value->data) != 0) 214 | { 215 | ngx_log_error(NGX_LOG_WARN, request->connection->log, 0, "The CSRF request header did not match the value in the encrypted CSRF cookie"); 216 | return NGX_HTTP_UNAUTHORIZED; 217 | } 218 | 219 | return NGX_OK; 220 | } 221 | 222 | /* 223 | * Set the authorization header and deal with string manipulation 224 | */ 225 | static ngx_int_t add_authorization_header(ngx_http_request_t *request, const ngx_str_t* token_value) 226 | { 227 | ngx_table_elt_t *authorization_header = NULL; 228 | u_char *header_value = NULL; 229 | size_t header_value_len = 0; 230 | 231 | authorization_header = ngx_list_push(&request->headers_in.headers); 232 | if (authorization_header == NULL) 233 | { 234 | ngx_log_error(NGX_LOG_WARN, request->connection->log, 0, "OAuth proxy failed to allocate memory for the authorization header"); 235 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 236 | } 237 | 238 | /* The header size is unknown and could represent a large JWT, so allocate memory dynamically */ 239 | header_value_len = ngx_strlen("Bearer ") + token_value->len; 240 | header_value = ngx_pcalloc(request->pool, header_value_len + 1); 241 | if (header_value == NULL) 242 | { 243 | ngx_log_error(NGX_LOG_WARN, request->connection->log, 0, "OAuth proxy failed to allocate memory for the authorization header value"); 244 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 245 | } 246 | 247 | ngx_str_set(&authorization_header->key, "authorization"); 248 | ngx_snprintf(header_value, header_value_len, "Bearer %V", token_value); 249 | header_value[header_value_len] = 0; 250 | 251 | authorization_header->value.data = header_value; 252 | authorization_header->value.len = header_value_len; 253 | authorization_header->hash = 1; 254 | request->headers_in.authorization = authorization_header; 255 | 256 | return NGX_OK; 257 | } 258 | 259 | /* 260 | * Write an empty CORS response 261 | */ 262 | static ngx_int_t write_options_response(ngx_http_request_t *request, oauth_proxy_configuration_t *module_location_config) 263 | { 264 | add_cors_response_headers(request, module_location_config, 0); 265 | return NGX_HTTP_NO_CONTENT; 266 | } 267 | 268 | /* 269 | * Add the error response and write CORS headers so that Javascript can read it 270 | * http://nginx.org/en/docs/dev/development_guide.html#http_response_body 271 | */ 272 | static ngx_int_t write_error_response(ngx_http_request_t *request, ngx_int_t status, oauth_proxy_configuration_t *module_location_config) 273 | { 274 | ngx_int_t rc; 275 | ngx_str_t code; 276 | ngx_str_t message; 277 | u_char json_error_data[256]; 278 | ngx_chain_t output; 279 | ngx_buf_t *body = NULL; 280 | const char *error_format = NULL; 281 | size_t error_len = 0; 282 | 283 | add_cors_response_headers(request, module_location_config, 1); 284 | if (request->method == NGX_HTTP_HEAD) 285 | { 286 | return status; 287 | } 288 | 289 | body = ngx_calloc_buf(request->pool); 290 | if (body == NULL) 291 | { 292 | ngx_log_error(NGX_LOG_WARN, request->connection->log, 0, "OAuth proxy failed to allocate memory for error body"); 293 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 294 | } 295 | else 296 | { 297 | if (status == NGX_HTTP_INTERNAL_SERVER_ERROR) 298 | { 299 | ngx_str_set(&code, "server_error"); 300 | ngx_str_set(&message, "Problem encountered processing the request"); 301 | } 302 | else 303 | { 304 | ngx_str_set(&code, "unauthorized"); 305 | ngx_str_set(&message, "Access denied due to missing or invalid credentials"); 306 | } 307 | 308 | /* The string length calculation replaces the two '%V' markers with their actual values */ 309 | error_format = "{\"code\":\"%V\",\"message\":\"%V\"}"; 310 | error_len = ngx_strlen(error_format) + code.len + message.len - 4; 311 | ngx_snprintf(json_error_data, sizeof(json_error_data) - 1, error_format, &code, &message); 312 | json_error_data[error_len] = 0; 313 | 314 | request->headers_out.status = status; 315 | request->headers_out.content_length_n = error_len; 316 | ngx_str_set(&request->headers_out.content_type, "application/json"); 317 | 318 | rc = ngx_http_send_header(request); 319 | if (rc == NGX_ERROR || rc > NGX_OK || request->header_only) { 320 | return rc; 321 | } 322 | 323 | body->pos = json_error_data; 324 | body->last = json_error_data + error_len; 325 | body->memory = 1; 326 | body->last_buf = 1; 327 | body->last_in_chain = 1; 328 | output.buf = body; 329 | output.next = NULL; 330 | 331 | /* Return an error result, which also requires finalize_request to be called, to prevent a 'header already sent' warning in logs 332 | https://forum.nginx.org/read.php?29,280514,280521#msg-280521 */ 333 | rc = ngx_http_output_filter(request, &output); 334 | ngx_http_finalize_request(request, rc); 335 | return NGX_DONE; 336 | } 337 | } 338 | 339 | 340 | /* 341 | * When there is a valid web origin, add CORS headers so that Javascript can read the response 342 | */ 343 | static ngx_int_t add_cors_response_headers(ngx_http_request_t *request, oauth_proxy_configuration_t *config, u_char is_error) 344 | { 345 | ngx_str_t *web_origin = NULL; 346 | ngx_str_t *allow_headers = NULL; 347 | ngx_str_t allow_credentials_str; 348 | ngx_str_t vary_str; 349 | const char *literal_request_headers = "access-control-request-headers"; 350 | 351 | web_origin = get_header(request, "origin"); 352 | if (web_origin != NULL && verify_web_origin(config, web_origin) == NGX_OK) 353 | { 354 | /* These are always needed in order for the SPA to be able to read error responses from the module */ 355 | if (config->cors_enabled || is_error != 0) 356 | { 357 | if (oauth_proxy_utils_add_header_out(request, "access-control-allow-origin", web_origin) != NGX_OK) 358 | { 359 | ngx_log_error(NGX_LOG_WARN, request->connection->log, 0, "OAuth proxy failed to add CORS allow_origin response header"); 360 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 361 | } 362 | 363 | ngx_str_set(&allow_credentials_str, "true"); 364 | if (oauth_proxy_utils_add_header_out(request, "access-control-allow-credentials", &allow_credentials_str) != NGX_OK) 365 | { 366 | ngx_log_error(NGX_LOG_WARN, request->connection->log, 0, "OAuth proxy failed to add CORS allow_credentials response header"); 367 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 368 | } 369 | } 370 | 371 | if (config->cors_enabled) 372 | { 373 | /* Write headers only needed in responses to pre-flight requests */ 374 | ngx_str_set(&vary_str, "origin"); 375 | if (request->method == NGX_HTTP_OPTIONS) 376 | { 377 | if (config->cors_allow_methods.len > 0) 378 | { 379 | if (oauth_proxy_utils_add_header_out(request, "access-control-allow-methods", &config->cors_allow_methods) != NGX_OK) 380 | { 381 | ngx_log_error(NGX_LOG_WARN, request->connection->log, 0, "OAuth proxy failed to add CORS allow_methods response header"); 382 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 383 | } 384 | } 385 | 386 | /* If no headers are set explicitly then return any headers the browser requests at runtime 387 | This ensures that the API gateway does not need reconfiguration whenever a new header is sent */ 388 | allow_headers = &config->cors_allow_headers; 389 | if (allow_headers->len == 0) 390 | { 391 | allow_headers = oauth_proxy_utils_get_header_in(request, (u_char *)literal_request_headers, ngx_strlen(literal_request_headers)); 392 | ngx_str_set(&vary_str, "origin,access-control-request-headers"); 393 | } 394 | 395 | if (allow_headers != NULL) 396 | { 397 | if (oauth_proxy_utils_add_header_out(request, "access-control-allow-headers", allow_headers) != NGX_OK) 398 | { 399 | ngx_log_error(NGX_LOG_WARN, request->connection->log, 0, "OAuth proxy failed to add CORS allow_headers response header"); 400 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 401 | } 402 | } 403 | 404 | if (config->cors_max_age > 0) 405 | { 406 | if (oauth_proxy_utils_add_integer_header_out(request, "access-control-max-age", config->cors_max_age) != NGX_OK) 407 | { 408 | ngx_log_error(NGX_LOG_WARN, request->connection->log, 0, "OAuth proxy failed to add CORS max_age header"); 409 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 410 | } 411 | } 412 | } 413 | 414 | /* These headers are needed in both pre-flight requests and the main request */ 415 | if (config->cors_expose_headers.len > 0) 416 | { 417 | if (oauth_proxy_utils_add_header_out(request, "access-control-expose-headers", &config->cors_expose_headers) != NGX_OK) 418 | { 419 | ngx_log_error(NGX_LOG_WARN, request->connection->log, 0, "OAuth proxy failed to add CORS expose_headers response header"); 420 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 421 | } 422 | } 423 | 424 | if (oauth_proxy_utils_add_header_out(request, "vary", &vary_str) != NGX_OK) 425 | { 426 | ngx_log_error(NGX_LOG_WARN, request->connection->log, 0, "OAuth proxy failed to add CORS vary response header"); 427 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 428 | } 429 | } 430 | } 431 | 432 | return NGX_OK; 433 | } 434 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OAuth Proxy NGINX Module 2 | 3 | [![Quality](https://img.shields.io/badge/quality-test-yellow)](https://curity.io/resources/code-examples/status/) 4 | [![Availability](https://img.shields.io/badge/availability-binary-blue)](https://curity.io/resources/code-examples/status/) 5 | 6 | An NGINX module that decrypts secure cookies in API calls from Single Page Applications.\ 7 | This is the OAuth Proxy component of the [Token Handler Pattern](https://curity.io/resources/learn/the-token-handler-pattern/). 8 | 9 | ![Token Handler Pattern](token-handler-pattern.png) 10 | 11 | ## High Level Usage 12 | 13 | The OAuth proxy is a forwarder placed in front of your business APIs, to deal with cookie authorization.\ 14 | A typical flow for an SPA calling an API would work like this: 15 | 16 | ![Security Handling](security-handling.png) 17 | 18 | - The SPA sends an AES256 encrypted HTTP Only cookie containing an opaque access token 19 | - The OAuth Proxy module decrypts the cookie to get the opaque access token 20 | - The opaque access token is then forwarded in the HTTP Authorization Header 21 | - The [Phantom Token Module](https://github.com/curityio/nginx_phantom_token_module) then swaps the opaque token for a JWT access token 22 | - The incoming HTTP Authorization Header is then updated with the JWT access token 23 | - The API must then verify the JWT in a zero trust manner, on every request 24 | 25 | ## Required Configuration Directives 26 | 27 | All of the directives are required for locations where the module is enabled.\ 28 | NGINX will fail to load if the configuration for any locations fail validation: 29 | 30 | #### oauth_proxy 31 | 32 | > **Syntax**: **`oauth_proxy`** `on` | `off` 33 | > 34 | > **Context**: `location` 35 | 36 | The module is disabled by default but can be enabled for paths you choose. 37 | 38 | #### oauth_proxy_cookie_name_prefix 39 | 40 | > **Syntax**: **`oauth_proxy_cookie_name_prefix`** `string` 41 | > 42 | > **Context**: `location` 43 | 44 | The prefix used in the SPA's cookie name, typically representing a company or product name.\ 45 | The value supplied must not be empty, and `example` would lead to full cookie names such as `example-at`. 46 | 47 | #### oauth_proxy_encryption_key 48 | 49 | > **Syntax**: **`oauth_proxy_encryption_key`** `string` 50 | > 51 | > **Context**: `location` 52 | 53 | This must be a 32 byte encryption key expressed as 64 hex characters.\ 54 | It is used to decrypt AES256 encrypted secure cookies.\ 55 | The key is initially generated with a tool such as `openssl`, as explained in Curity tutorials. 56 | 57 | #### oauth_proxy_trusted_web_origins 58 | 59 | > **Syntax**: **`oauth_proxy_trusted_web_origins`** `string[]` 60 | > 61 | > **Context**: `location` 62 | 63 | A whitelist of at least one web origin from which the module will accept requests.\ 64 | Multiple origins could be used in special cases where cookies are shared across subdomains. 65 | 66 | #### oauth_proxy_cors_enabled 67 | 68 | > **Syntax**: **`oauth_proxy_cors_enabled`** `boolean` 69 | > 70 | > **Context**: `location` 71 | 72 | When enabled, the OAuth proxy returns CORS response headers on behalf of the API.\ 73 | When an origin header is received that is in the trusted_web_origins whitelist, response headers are written.\ 74 | The [access-control-allow-origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin) header is returned, so that the SPA can call the API.\ 75 | The [access-control-allow-credentials](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials) header is returned, so that the SPA can send secured cookies to the API.\ 76 | Default values are provided for other CORS headers that the SPA needs. 77 | 78 | ## Optional Configuration Directives 79 | 80 | #### oauth_proxy_allow_tokens 81 | 82 | > **Syntax**: **`oauth_proxy_allow_tokens`** `on` | `off` 83 | > 84 | > **Default**: *off* 85 | > 86 | > **Context**: `location` 87 | 88 | If set to true, then requests that already have a bearer token are passed straight through to APIs.\ 89 | This can be useful when web and mobile clients share the same API routes. 90 | 91 | #### oauth_proxy_cors_allow_methods 92 | 93 | > **Syntax**: **`oauth_proxy_cors_allow_methods`** `string` 94 | > 95 | > **Default**: *'OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE'* 96 | > 97 | > **Context**: `location` 98 | 99 | When CORS is enabled, these values are returned in the [access-control-allow-methods](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods) response header.\ 100 | A '*' wildcard value should not be configured here, since it will not work with credentialed requests. 101 | 102 | #### oauth_proxy_cors_allow_headers 103 | 104 | > **Syntax**: **`oauth_proxy_cors_allow_headers`** `string` 105 | > 106 | > **Default**: *''* 107 | > 108 | > **Context**: `location` 109 | 110 | When CORS is enabled, the module returns these values in the [access-control-allow-headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers) response header.\ 111 | If no values are configured then at runtime any headers the SPA sends are allowed.\ 112 | This is managed by returning the contents of the [access-control-request-headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Headers) field.\ 113 | If setting values explicitly, ensure that the token handler CSRF request header is included, eg `x-example-csrf`.\ 114 | A '*' wildcard value should not be configured here, since it will not work with credentialed requests. 115 | 116 | #### oauth_proxy_cors_expose_headers 117 | 118 | > **Syntax**: **`oauth_proxy_cors_expose_headers`** `string` 119 | > 120 | > **Default**: *''* 121 | > 122 | > **Context**: `location` 123 | 124 | When CORS is enabled, the module returns these values in the [access-contol-expose-headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers) response header.\ 125 | A '*' wildcard value should not be configured here, since it will not work with credentialed requests. 126 | 127 | #### oauth_proxy_cors_max_age 128 | 129 | > **Syntax**: **`oauth_proxy_cors_max_age`** `number` 130 | > 131 | > **Default**: *86400* 132 | > 133 | > **Context**: `location` 134 | 135 | When CORS is enabled, the module returns this value in the [access-contol-max-age](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age) response header.\ 136 | This option prevents excessive pre-flight OPTIONS requests, to improve the efficiency of API calls. 137 | 138 | ## Example Configurations 139 | 140 | #### Loading the Module 141 | 142 | In deployed systems the module is loaded using the [load_module](http://nginx.org/en/docs/ngx_core_module.html#load_module) directive.\ 143 | This needs to be done in the _main_ part of the NGINX configuration: 144 | 145 | ```nginx 146 | load_module modules/ngx_curity_http_oauth_proxy_module.so; 147 | ``` 148 | 149 | #### Basic Configuration 150 | 151 | The following location decrypts cookies, then forwards an access token to the downstream API: 152 | 153 | ```nginx 154 | location /products { 155 | 156 | oauth_proxy on; 157 | oauth_proxy_cookie_name_prefix "example"; 158 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 159 | oauth_proxy_trusted_web_origin "https://www.example.com"; 160 | oauth_proxy_cors_enabled on; 161 | 162 | proxy_pass "https://productsapi.example.com"; 163 | } 164 | ``` 165 | 166 | #### Inherited Configuration 167 | 168 | Parent and child locations can be used, in which case children inherit the parent settings: 169 | 170 | ```nginx 171 | location /api { 172 | 173 | oauth_proxy on; 174 | oauth_proxy_cookie_name_prefix "example"; 175 | oauth_proxy_encryption_key "4e4636356d65563e4c73233847503e3b21436e6f7629724950526f4b5e2e4e50"; 176 | oauth_proxy_trusted_web_origin "https://www.example.com"; 177 | oauth_proxy_cors_enabled on; 178 | 179 | location /api/products { 180 | proxy_pass "https://productsapi.example.com"; 181 | } 182 | 183 | location /api/offers { 184 | proxy_pass "https://offersapi.example.com"; 185 | } 186 | } 187 | ``` 188 | 189 | ## Cookie Details 190 | 191 | The module expects to receives two cookies, which use a custom prefix with fixed suffixes.\ 192 | Cookies are encrypted using AES256-GCM, and received in a base64 URL encoded format. 193 | 194 | | Example Cookie Name | Fixed Suffix | Contains | 195 | | ------------------- | ------------ | -------- | 196 | | example-at | -at | An encrypted cookie containing an opaque or JWT access token | 197 | | example-csrf | -csrf | A CSRF cookie verified during data changing requests | 198 | 199 | ## Security Behavior 200 | 201 | The module handles cookies according to [OWASP Cross Site Request Forgery Best Practices](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html): 202 | 203 | #### Options Requests 204 | 205 | The module first handles pre-flight OPTIONS requests and writes CORS response headers: 206 | 207 | ```text 208 | access-control-allow-origin: https://www.example.com 209 | access-control-allow-credentials: true 210 | access-control-allow-cors_allow_methods: OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE 211 | access-control-allow-cors_allow_headers: x-example-csrf 212 | access-control-max-age: 86400 213 | vary: origin,access-control-request-headers 214 | ``` 215 | 216 | #### Web Origin Checks 217 | 218 | On the main request the module first reads the `Origin HTTP Header`, sent by all modern browsers.\ 219 | If this does not contain a trusted value the request is immediately rejected with a 401 response. 220 | 221 | #### Cross Site Request Forgery Checks 222 | 223 | The process is as follows, though the exact identifiers depend on the configured cookie prefix: 224 | 225 | - After a user login the browser receives an `example-csrf` cookie from the main Token Handler API. 226 | - When the SPA loads it receives a `csrf-token`, which stays the same for the authenticated session. 227 | - This is sent as an `x-example-csrf` request header on POST, PUT, PATCH, DELETE commands. 228 | - The cookie and header value must have the same value or the module returns a 401 error response. 229 | 230 | #### Access Token Handling 231 | 232 | Once other checks have completed, the module processes the access token cookie.\ 233 | The `-at` cookie is decrypted, after which the token is forwarded to the downstream API: 234 | 235 | ```text 236 | Authorization Bearer 42665300-efe8-419d-be52-07b53e208f46 237 | ``` 238 | 239 | With opaque reference tokens the encrypted cookies do not exceed NGINX default header sizes.\ 240 | If large JWTs are instead used, then these NGINX properties may need to use larger than default values: 241 | 242 | - proxy_buffers 243 | - proxy_buffer_size 244 | - large_client_header_buffers 245 | 246 | #### Decryption 247 | 248 | AES256-GCM uses authenticated encryption, so invalid cookies are rejected with a 401 response: 249 | 250 | - Cookies encrypted with a different encryption key 251 | - Cookies where any part of the payload has been tampered with 252 | 253 | #### Error Responses 254 | 255 | Error responses contain a JSON body and CORS headers so that the SPA can read the details: 256 | 257 | ```text 258 | { 259 | "code": "unauthorized", 260 | "message": "Access denied due to missing or invalid credentials" 261 | } 262 | 263 | access-control-allow-origin: https://www.example.com 264 | access-control-allow-credentials: true 265 | ``` 266 | 267 | The code in the [Example SPA](https://github.com/curityio/spa-using-token-handler) shows how to handle error responses.\ 268 | The HTTP status code is usually sufficient, and the error code can inform the SPA of specific causes. 269 | 270 | ## Compatibility 271 | 272 | This module has been tested for the Linux NGINX distributions from the [Deployment Resources](/resources/deployment). It requires the [NGINX HTTP SSL module](http://nginx.org/en/docs/http/ngx_http_ssl_module.html) to be enabled, so that OpenSSL libraries are available. 273 | 274 | The binary releases align with the 24 month supported release cycle of [NGINX Plus](https://docs.nginx.com/nginx/releases/) to keep the NGINX secure environment up to date. The module's code has also run to a production level with many previous NGINX releases. 275 | 276 | ### Pre-Built Releases 277 | 278 | Pre-built binaries of this module are provided for the following versions of NGINX.\ 279 | Download the .so file for your platform and deploy it to the `/usr/lib/nginx/modules` folder of your NGINX servers. 280 | 281 | | | NGINX 1.27.4 / NGINX Plus R34 | NGINX 1.27.2 / NGINX Plus R33 | NGINX 1.25.5 / NGINX Plus R32 | NGINX 1.25.3 / NGINX Plus R31 | NGINX 1.25.1 / NGINX Plus R30 | 282 | | -----------------------------------|:-----------------------------:|:-----------------------------:|:-----------------------------:|:------------------------------:|:-----------------------------:| 283 | | Alpine | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/alpine.ngx_curity_http_oauth_proxy_module_1.27.4.so) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/alpine.ngx_curity_http_oauth_proxy_module_1.27.2.so) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/alpine.ngx_curity_http_oauth_proxy_module_1.25.5.so) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/alpine.ngx_curity_http_oauth_proxy_module_1.25.3.so) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/alpine.ngx_curity_http_oauth_proxy_module_1.25.1.so) | 284 | | Debian 11.0 (Bullseye) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/debian.bullseye.ngx_curity_http_oauth_proxy_module_1.27.4.so) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/debian.bullseye.ngx_curity_http_oauth_proxy_module_1.27.2.so) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/debian.bullseye.ngx_curity_http_oauth_proxy_module_1.25.5.so) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/debian.bullseye.ngx_curity_http_oauth_proxy_module_1.25.3.so) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/debian.bullseye.ngx_curity_http_oauth_proxy_module_1.25.1.so) | 285 | | Debian 12.0 (Bookworm) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/debian.bookworm.ngx_curity_http_oauth_proxy_module_1.27.4.so) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/debian.bookworm.ngx_curity_http_oauth_proxy_module_1.27.2.so) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/debian.bookworm.ngx_curity_http_oauth_proxy_module_1.25.5.so) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/debian.bookworm.ngx_curity_http_oauth_proxy_module_1.25.3.so) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/debian.bookworm.ngx_curity_http_oauth_proxy_module_1.25.1.so) | 286 | | Ubuntu 20.04 LTS (Focal Fossa) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/ubuntu.20.04.ngx_curity_http_oauth_proxy_module_1.27.4.so) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/ubuntu.20.04.ngx_curity_http_oauth_proxy_module_1.27.2.so) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/ubuntu.20.04.ngx_curity_http_oauth_proxy_module_1.25.5.so) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/ubuntu.20.04.ngx_curity_http_oauth_proxy_module_1.25.3.so) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/ubuntu.20.04.ngx_curity_http_oauth_proxy_module_1.25.1.so) | 287 | | Ubuntu 22.04 LTS (Jammy Jellyfish) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/ubuntu.22.04.ngx_curity_http_oauth_proxy_module_1.27.4.so) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/ubuntu.22.04.ngx_curity_http_oauth_proxy_module_1.27.2.so) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/ubuntu.22.04.ngx_curity_http_oauth_proxy_module_1.25.5.so) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/ubuntu.22.04.ngx_curity_http_oauth_proxy_module_1.25.3.so) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/ubuntu.22.04.ngx_curity_http_oauth_proxy_module_1.25.1.so) | 288 | | Ubuntu 24.04 LTS (Noble Numbat) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/ubuntu.24.04.ngx_curity_http_oauth_proxy_module_1.27.4.so) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/ubuntu.24.04.ngx_curity_http_oauth_proxy_module_1.27.2.so) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/ubuntu.24.04.ngx_curity_http_oauth_proxy_module_1.25.5.so) | X | X | 289 | | Amazon Linux 2 | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/amzn2.ngx_curity_http_oauth_proxy_module_1.27.4.so) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/amzn2.ngx_curity_http_oauth_proxy_module_1.27.2.so) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/amzn2.ngx_curity_http_oauth_proxy_module_1.25.5.so) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/amzn2.ngx_curity_http_oauth_proxy_module_1.25.3.so) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/amzn2.ngx_curity_http_oauth_proxy_module_1.25.1.so) | 290 | | Amazon Linux 2023 | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/amzn2023.ngx_curity_http_oauth_proxy_module_1.27.4.so) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/amzn2023.ngx_curity_http_oauth_proxy_module_1.27.2.so) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/amzn2023.ngx_curity_http_oauth_proxy_module_1.25.5.so) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/amzn2023.ngx_curity_http_oauth_proxy_module_1.25.3.so) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/amzn2023.ngx_curity_http_oauth_proxy_module_1.25.1.so) | 291 | | CentOS Stream 9.0+ | x | x | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/centos.stream.9.ngx_curity_http_oauth_proxy_module_1.25.5.so) | [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/centos.stream.9.ngx_curity_http_oauth_proxy_module_1.25.3.so)| [⇓](https://github.com/curityio/nginx_oauth_proxy_module/releases/download/1.6.0/centos.stream.9.ngx_curity_http_oauth_proxy_module_1.25.1.so)| 292 | 293 | ## Building From Source 294 | 295 | To build the latest code against older NGINX versions or Linux distributions, follow the instructions in the [Development Wiki](https://github.com/curityio/nginx_oauth_proxy_module/wiki). 296 | 297 | - [Build the Module](https://github.com/curityio/nginx_oauth_proxy_module/wiki/3.-Builds) 298 | - [Deploy the Module](https://github.com/curityio/nginx_oauth_proxy_module/wiki/4.-Testing-Deployment) 299 | 300 | ## Licensing 301 | 302 | This software is copyright (C) 2022 Curity AB. It is open source software that is licensed under the [Apache 2 license](LICENSE). For commercial support of this module, please contact [Curity sales](mailto:sales@curity.io). 303 | 304 | ## More Information 305 | 306 | Please visit [curity.io](https://curity.io/) for more information about the Curity Identity Server. 307 | 308 | --------------------------------------------------------------------------------