diff --git a/.bin/git/hooks-wrapper b/.bin/git/hooks-wrapper new file mode 100755 index 0000000..33cea38 --- /dev/null +++ b/.bin/git/hooks-wrapper @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +# Runs all executable pre-commit-* hooks and exits after, +# if any of them was not successful. +# +# Based on +# https://github.com/ELLIOTTCABLE/Paws.js/blob/Master/Scripts/git-hooks/chain-hooks.sh +# http://osdir.com/ml/git/2009-01/msg00308.html +# +# assumes your scripts are located at /bin/git/hooks + +exitcodes=() +hookname=`basename $0` +# our special hooks folder +CUSTOM_HOOKS_DIR=$(git rev-parse --show-toplevel)/bin/git/hooks +# find gits native hooks folder +NATIVE_HOOKS_DIR=$(git rev-parse --show-toplevel)/.git/hooks + +# Run each hook, passing through STDIN and storing the exit code. +# We don't want to bail at the first failure, as the user might +# then bypass the hooks without knowing about additional issues. + +for hook in ${CUSTOM_HOOKS_DIR}/$(basename $0)-*; do + test -x "$hook" || continue + + echo "Running custom hook '$hookname' ..." + out=`$hook "$@"` + exitcodes+=($?) + echo "$out" +done + +# check if there was a local hook that was moved previously +if [ -f "${NATIVE_HOOKS_DIR}/$hookname.local" ]; then + echo "Running native hook '$hookname' ..." + out=`${NATIVE_HOOKS_DIR}/$hookname.local "$@"` + exitcodes+=($?) + echo "$out" +fi + +# If any exit code isn't 0, bail. +for i in "${exitcodes[@]}"; do + [ "$i" == 0 ] || exit $i +done \ No newline at end of file diff --git a/.bin/git/hooks/pre-push-build-and-test b/.bin/git/hooks/pre-push-build-and-test new file mode 100755 index 0000000..876ca32 --- /dev/null +++ b/.bin/git/hooks/pre-push-build-and-test @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +REPO_ROOT_DIR=$(git rev-parse --show-toplevel) +CHANGE_COUNT=$(cd ${REPO_ROOT_DIR}; git diff --name-only origin/HEAD..HEAD -- resources/ src/ test/ Dockerfile scripts.sh |wc -l) + +if [[ "0" -ne "${CHANGE_COUNT}" ]]; then + (cd ${REPO_ROOT_DIR}; ./scripts.sh rebuild_nginx rebuild_test test) +else + HOOK_NAME=$(basename $0) + + echo "Skipping hook '${HOOK_NAME}' -- no changes detected which would require tests to be run." +fi diff --git a/.bin/git/init-hooks b/.bin/git/init-hooks new file mode 100755 index 0000000..1513105 --- /dev/null +++ b/.bin/git/init-hooks @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# based on http://stackoverflow.com/a/3464399/1383268 +# assumes that the hooks-wrapper script is located at /bin/git/hooks-wrapper + +HOOK_NAMES="applypatch-msg pre-applypatch post-applypatch pre-commit prepare-commit-msg commit-msg post-commit pre-rebase post-checkout post-merge pre-receive update post-receive post-update pre-auto-gc pre-push" +# find git's native hooks folder +REPO_ROOT_DIR=$(git rev-parse --show-toplevel) +HOOKS_DIR=$(git rev-parse --show-toplevel)/.git/hooks + +for hook in ${HOOK_NAMES}; do + # If the hook already exists, is a file, and is not a symlink + if [ ! -h ${HOOKS_DIR}/${hook} ] && [ -f ${HOOKS_DIR}/${hook} ]; then + mv ${HOOKS_DIR}/${hook} ${HOOKS_DIR}/${hook}.local + fi + # create the symlink, overwriting the file if it exists + # probably the only way this would happen is if you're using an old version of git + # -- back when the sample hooks were not executable, instead of being named ____.sample + ln -s -f ${REPO_ROOT_DIR}/bin/git/hooks-wrapper ${HOOKS_DIR}/${hook} +done \ No newline at end of file diff --git a/.bin/init b/.bin/init new file mode 100755 index 0000000..9c59222 --- /dev/null +++ b/.bin/init @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +source $(dirname $0)/git/init-hooks \ No newline at end of file diff --git a/.env b/.env new file mode 100644 index 0000000..1bb6443 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +COMPOSE_PROJECT_NAME=jwt-nginx-test \ No newline at end of file diff --git a/.github/workflows/make-releases.yml b/.github/workflows/make-releases.yml new file mode 100644 index 0000000..c77edcb --- /dev/null +++ b/.github/workflows/make-releases.yml @@ -0,0 +1,182 @@ +name: Make Releases + +on: + workflow_dispatch: + +jobs: + meta: + name: Get Metadata + runs-on: ubuntu-latest + outputs: + tag: ${{steps.meta.outputs.tag}} + steps: + + - name: Checkout Code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get Metadata + id: meta + run: | + set -eu + tag=$(git describe --tags --abbrev=0) + + echo "tag=${tag}" >> $GITHUB_OUTPUT + + build: + name: "NGINX: ${{ matrix.nginx-version }}; libjwt: ${{ matrix.libjwt-version }}" + needs: meta + strategy: + matrix: + nginx-version: + - 1.20.2 # legacy + - 1.22.1 # legacy + - 1.24.0 # legacy + - 1.26.2 # stable + - 1.26.3 # stable + - 1.27.3 # mainline + - 1.27.4 # mainline + + libjwt-version: + - 1.12.0 + - 1.14.0 + - 1.15.3 + runs-on: ubuntu-latest + steps: + + - name: Checkout Code + uses: actions/checkout@v4 + with: + path: ngx-http-auth-jwt-module + + - name: Get Metadata + id: meta + run: | + set -eu + artifact="ngx-http-auth-jwt-module-${{needs.meta.outputs.tag}}_libjwt-${{matrix.libjwt-version}}_nginx-${{matrix.nginx-version}}" + + echo "artifact=${artifact}" >> $GITHUB_OUTPUT + echo "filename=${artifact}.tgz" >> $GITHUB_OUTPUT + + + # TODO cache the build result so we don't have to do this every time? + - name: Download jansson + uses: actions/checkout@v4 + with: + repository: 'akheron/jansson' + ref: 'v2.14' + path: 'jansson' + + - name: Build jansson + working-directory: ./jansson + run: | + set -e + cmake . -DJANSSON_BUILD_SHARED_LIBS=1 -DJANSSON_BUILD_DOCS=OFF + make + make check + sudo make install + + # TODO cache the build result so we don't have to do this every time? + - name: Download libjwt + uses: actions/checkout@v4 + with: + repository: 'benmcollins/libjwt' + ref: 'v${{matrix.libjwt-version}}' + path: 'libjwt' + + - name: Build libjwt + working-directory: ./libjwt + run: | + set -e + autoreconf -i + ./configure + make all + sudo make install + + - name: Download NGINX + run: | + mkdir nginx + curl -O http://nginx.org/download/nginx-${{matrix.nginx-version}}.tar.gz + tar -xzf nginx-${{matrix.nginx-version}}.tar.gz --strip-components 1 -C nginx + + - name: Configure NGINX + working-directory: ./nginx + run: | + BUILD_FLAGS='' + MAJ=$(echo ${{matrix.nginx-version}} | cut -f1 -d.) + MIN=$(echo ${{matrix.nginx-version}} | cut -f2 -d.) + REV=$(echo ${{matrix.nginx-version}} | cut -f3 -d.) + + if [ "${MAJ}" -gt 1 ] || [ "${MAJ}" -eq 1 -a "${MIN}" -ge 23 ]; then + BUILD_FLAGS="${BUILD_FLAGS} --with-cc-opt='-DNGX_LINKED_LIST_COOKIES=1'" + fi + + ./configure --with-compat --without-http_rewrite_module --add-dynamic-module=../ngx-http-auth-jwt-module ${BUILD_FLAGS} + + - name: Make Modules + working-directory: ./nginx + run: make modules + + - name: Create Release Archive + run: | + cp ./nginx/objs/ngx_http_auth_jwt_module.so ./ + tar czf ${{steps.meta.outputs.filename}} ngx_http_auth_jwt_module.so + + - name: Upload Build Artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: ${{steps.meta.outputs.artifact}} + path: ${{steps.meta.outputs.filename}} + + release: + name: Create/Update Release + needs: + - meta + - build + runs-on: ubuntu-latest + permissions: + contents: write + steps: + + - name: Set-up Variables + id: vars + run: | + echo "date_now=$(date --rfc-3339=seconds)" >> "${GITHUB_OUTPUT}" + + - name: Download Artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Flatten Artifacts + run: | + set -eu + + cd artifacts + + for f in $(find . -type f); do + echo "Staging: ${f}" + mv "${f}" . + done + + find . -type d -mindepth 1 -exec rm -rf "{}" + + + - name: Create/Update Release + uses: ncipollo/release-action@v1 + with: + tag: ${{needs.meta.outputs.tag}} + name: "Pre-release: ${{needs.meta.outputs.tag}}" + body: | + > [!WARNING] + > This is an automatically generated pre-release version of the module, which includes the latest master branch changes. + > Please report any bugs you find. + + - Build Date: `${{ steps.vars.outputs.date_now }}` + - Commit: `${{ github.sha }}` + prerelease: true + allowUpdates: true + removeArtifacts: true + artifactErrorsFailBuild: true + artifacts: artifacts/* diff --git a/.gitignore b/.gitignore index 763f224..14c2591 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,4 @@ .idea -ngx_http_auth_jwt_module.so -libjwt.so.0.6.0 -libjwt.la -libjwt.a -libjansson.so.4.13.0 -libjwt.so.0.7.0 -jansson.pc -libjwt.pc -libjansson.so.4.10.0 -libjwt.so.0.4.0 +.vscode +bin +release \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 31f1342..0000000 --- a/Dockerfile +++ /dev/null @@ -1,104 +0,0 @@ -FROM centos:7 - -LABEL maintainer="TeslaGov" email="developers@teslagov.com" - -ARG NGINX_VERSION=1.16.1 -ARG JANSSON_VERSION=2.10 -ARG LIBJWT_VERSION=1.9.0 - -ENV LD_LIBRARY_PATH=/usr/local/lib -ENV PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:/usr/share/pkgconfig - -RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm && \ - yum -y update && \ - yum -y groupinstall 'Development Tools' && \ - yum -y install pcre-devel pcre zlib-devel openssl-devel wget cmake check-devel check && \ - yum -y install nginx-$NGINX_VERSION - -# for compiling for rh-nginx110 -# yum -y install libxml2 libxslt libxml2-devel libxslt-devel gd gd-devel perl-ExtUtils-Embed - -# for compiling for epel7 -RUN yum -y install libxml2 libxslt libxml2-devel libxslt-devel gd gd-devel perl-ExtUtils-Embed geoip geoip-devel google-perftools google-perftools-devel - -# Jansson requires new cmake -RUN yum -y install cmake3 && \ - alternatives --install /usr/local/bin/cmake cmake /usr/bin/cmake 10 \ ---slave /usr/local/bin/ctest ctest /usr/bin/ctest \ ---slave /usr/local/bin/cpack cpack /usr/bin/cpack \ ---slave /usr/local/bin/ccmake ccmake /usr/bin/ccmake \ ---family cmake && \ - alternatives --install /usr/local/bin/cmake cmake /usr/bin/cmake3 20 \ ---slave /usr/local/bin/ctest ctest /usr/bin/ctest3 \ ---slave /usr/local/bin/cpack cpack /usr/bin/cpack3 \ ---slave /usr/local/bin/ccmake ccmake /usr/bin/ccmake3 \ ---family cmake - -RUN mkdir -p /root/dl -WORKDIR /root/dl - -# build jansson -RUN wget https://github.com/akheron/jansson/archive/v$JANSSON_VERSION.zip && \ - unzip v$JANSSON_VERSION.zip && \ - rm v$JANSSON_VERSION.zip && \ - ln -sf jansson-$JANSSON_VERSION jansson && \ - cd /root/dl/jansson && \ - cmake . -DJANSSON_BUILD_SHARED_LIBS=1 -DJANSSON_BUILD_DOCS=OFF && \ - make && \ - make check && \ - make install - -# build libjwt -RUN wget https://github.com/benmcollins/libjwt/archive/v$LIBJWT_VERSION.zip && \ - unzip v$LIBJWT_VERSION.zip && \ - rm v$LIBJWT_VERSION.zip && \ - ln -sf libjwt-$LIBJWT_VERSION libjwt && \ - cd /root/dl/libjwt && \ - autoreconf -i && \ - ./configure && \ - make all && \ - make install - -ADD . /root/dl/ngx-http-auth-jwt-module - -# after 1.11.5 when compiling for a server that was compiled with --with-compat use this command -# ./configure --with-compat --add-dynamic-module=../ngx-http-auth-jwt-module --with-cc-opt='-std=gnu99' -# cp /root/dl/nginx/objs/ngx_http_auth_jwt_module.so /etc/nginx/modules/. -# build nginx module against nginx sources -# -# 1.10.2 from nginx by default use config flags... I had to add the -std=c99 and could not achieve "binary compatibility" -# ./configure --add-dynamic-module=../ngx-http-auth-jwt-module --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-file-aio --with-threads --with-ipv6 --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_ssl_module --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -m64 -mtune=generic -std=c99' -# -# rh-nginx110 uses these config flags -# ./configure --add-dynamic-module=../ngx-http-auth-jwt-module --prefix=/opt/rh/rh-nginx110/root/usr/share/nginx --sbin-path=/opt/rh/rh-nginx110/root/usr/sbin/nginx --modules-path=/opt/rh/rh-nginx110/root/usr/lib64/nginx/modules --conf-path=/etc/opt/rh/rh-nginx110/nginx/nginx.conf --error-log-path=/var/opt/rh/rh-nginx110/log/nginx/error.log --http-log-path=/var/opt/rh/rh-nginx110/log/nginx/access.log --http-client-body-temp-path=/var/opt/rh/rh-nginx110/lib/nginx/tmp/client_body --http-proxy-temp-path=/var/opt/rh/rh-nginx110/lib/nginx/tmp/proxy --http-fastcgi-temp-path=/var/opt/rh/rh-nginx110/lib/nginx/tmp/fastcgi --http-uwsgi-temp-path=/var/opt/rh/rh-nginx110/lib/nginx/tmp/uwsgi --http-scgi-temp-path=/var/opt/rh/rh-nginx110/lib/nginx/tmp/scgi --pid-path=/var/opt/rh/rh-nginx110/run/nginx/nginx.pid --lock-path=/var/opt/rh/rh-nginx110/lock/subsys/nginx --user=nginx --group=nginx --with-file-aio --with-ipv6 --with-http_ssl_module --with-http_v2_module --with-http_realip_module --with-http_addition_module --with-http_xslt_module=dynamic --with-http_image_filter_module=dynamic --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_random_index_module --with-http_secure_link_module --with-http_degradation_module --with-http_slice_module --with-http_stub_status_module --with-http_perl_module=dynamic --with-mail=dynamic --with-mail_ssl_module --with-pcre --with-pcre-jit --with-stream=dynamic --with-stream_ssl_module --with-debug --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -m64 -mtune=generic -std=c99' --with-ld-opt='-Wl,-z,relro -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -Wl,-E' -# -# epel7 version 1.12.1 uses these config flags -# ./configure --add-dynamic-module=../ngx-http-auth-jwt-module --prefix=/usr/share/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --http-client-body-temp-path=/var/lib/nginx/tmp/client_body --http-proxy-temp-path=/var/lib/nginx/tmp/proxy --http-fastcgi-temp-path=/var/lib/nginx/tmp/fastcgi --http-uwsgi-temp-path=/var/lib/nginx/tmp/uwsgi --http-scgi-temp-path=/var/lib/nginx/tmp/scgi --pid-path=/run/nginx.pid --lock-path=/run/lock/subsys/nginx --user=nginx --group=nginx --with-file-aio --with-ipv6 --with-http_ssl_module --with-http_v2_module --with-http_realip_module --with-http_addition_module --with-http_xslt_module=dynamic --with-http_image_filter_module=dynamic --with-http_geoip_module=dynamic --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_random_index_module --with-http_secure_link_module --with-http_degradation_module --with-http_slice_module --with-http_stub_status_module --with-http_perl_module=dynamic --with-mail=dynamic --with-mail_ssl_module --with-pcre --with-pcre-jit --with-stream=dynamic --with-stream_ssl_module --with-google_perftools_module --with-debug --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -m64 -mtune=generic -std=gnu99' --with-ld-opt='-Wl,-z,relro -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -Wl,-E' -# -# epel7 version 1.16.1 uses these config flags -# ./configure --add-dynamic-module=../ngx-http-auth-jwt-module --prefix=/usr/share/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --http-client-body-temp-path=/var/lib/nginx/tmp/client_body --http-proxy-temp-path=/var/lib/nginx/tmp/proxy --http-fastcgi-temp-path=/var/lib/nginx/tmp/fastcgi --http-uwsgi-temp-path=/var/lib/nginx/tmp/uwsgi --http-scgi-temp-path=/var/lib/nginx/tmp/scgi --pid-path=/run/nginx.pid --lock-path=/run/lock/subsys/nginx --user=nginx --group=nginx --with-file-aio --with-ipv6 --with-http_ssl_module --with-http_v2_module --with-http_realip_module --with-stream_ssl_preread_module --with-http_addition_module --with-http_xslt_module=dynamic --with-http_image_filter_module=dynamic --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_random_index_module --with-http_secure_link_module --with-http_degradation_module --with-http_slice_module --with-http_stub_status_module --with-http_perl_module=dynamic --with-http_auth_request_module --with-mail=dynamic --with-mail_ssl_module --with-pcre --with-pcre-jit --with-stream=dynamic --with-stream_ssl_module --with-google_perftools_module --with-debug --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -m64 -mtune=generic' --with-ld-opt='-Wl,-z,relro -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -Wl,-E' - -# ARG CACHEBUST=1 - -RUN wget http://nginx.org/download/nginx-$NGINX_VERSION.tar.gz && \ - tar -xzf nginx-$NGINX_VERSION.tar.gz && \ - rm nginx-$NGINX_VERSION.tar.gz && \ - ln -sf nginx-$NGINX_VERSION nginx && \ - cd /root/dl/nginx && \ - ./configure --add-dynamic-module=../ngx-http-auth-jwt-module --prefix=/usr/share/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --http-client-body-temp-path=/var/lib/nginx/tmp/client_body --http-proxy-temp-path=/var/lib/nginx/tmp/proxy --http-fastcgi-temp-path=/var/lib/nginx/tmp/fastcgi --http-uwsgi-temp-path=/var/lib/nginx/tmp/uwsgi --http-scgi-temp-path=/var/lib/nginx/tmp/scgi --pid-path=/run/nginx.pid --lock-path=/run/lock/subsys/nginx --user=nginx --group=nginx --with-file-aio --with-ipv6 --with-http_ssl_module --with-http_v2_module --with-http_realip_module --with-stream_ssl_preread_module --with-http_addition_module --with-http_xslt_module=dynamic --with-http_image_filter_module=dynamic --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_random_index_module --with-http_secure_link_module --with-http_degradation_module --with-http_slice_module --with-http_stub_status_module --with-http_perl_module=dynamic --with-http_auth_request_module --with-mail=dynamic --with-mail_ssl_module --with-pcre --with-pcre-jit --with-stream=dynamic --with-stream_ssl_module --with-google_perftools_module --with-debug --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -m64 -mtune=generic -std=gnu99' --with-ld-opt='-Wl,-z,relro -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -Wl,-E' && \ - make modules && \ - cp /root/dl/nginx/objs/ngx_http_auth_jwt_module.so /usr/lib64/nginx/modules/. - -# Get nginx ready to run -COPY resources/nginx.conf /etc/nginx/nginx.conf -COPY resources/test-jwt-nginx.conf /etc/nginx/conf.d/test-jwt-nginx.conf -RUN rm -rf /usr/share/nginx/html -RUN cp -r /root/dl/nginx/html /usr/share/nginx -RUN cp -r /usr/share/nginx/html /usr/share/nginx/secure -RUN cp -r /usr/share/nginx/html /usr/share/nginx/secure-rs256 -RUN cp -r /usr/share/nginx/html /usr/share/nginx/secure-auth-header -RUN cp -r /usr/share/nginx/html /usr/share/nginx/secure-no-redirect - -ENTRYPOINT ["/usr/sbin/nginx"] - -EXPOSE 8000 \ No newline at end of file diff --git a/Dockerfile-test b/Dockerfile-test deleted file mode 100644 index adff57a..0000000 --- a/Dockerfile-test +++ /dev/null @@ -1,4 +0,0 @@ -FROM alpine:3.7 -RUN apk add --no-cache bash curl -COPY test.sh . -CMD ["./test.sh"] \ No newline at end of file diff --git a/Makefile b/Makefile deleted file mode 100644 index 8a2290b..0000000 --- a/Makefile +++ /dev/null @@ -1,59 +0,0 @@ -SHELL += -eu - -BLUE := \033[0;34m -GREEN := \033[0;32m -RED := \033[0;31m -NC := \033[0m - -DOCKER_ORG_NAME = teslagov -DOCKER_IMAGE_NAME = jwt-nginx - -.PHONY: all -all: - @$(MAKE) build-nginx - @$(MAKE) build-test-runner - @$(MAKE) start-nginx - @$(MAKE) test - -.PHONY: build-nginx -build-nginx: - @echo "${BLUE} Building...${NC}" - @docker image build -t $(DOCKER_ORG_NAME)/$(DOCKER_IMAGE_NAME) . ; \ - if [ $$? -ne 0 ] ; \ - then echo "${RED} Build failed :(${NC}" ; \ - else echo "${GREEN}✓ Successfully built NGINX module ${NC}" ; fi - -.PHONY: rebuild-nginx -rebuild-nginx: - @echo "${BLUE} Rebuilding...${NC}" - @docker image build -t $(DOCKER_ORG_NAME)/$(DOCKER_IMAGE_NAME) . --no-cache ; \ - if [ $$? -ne 0 ] ; \ - then echo "${RED} Build failed :(${NC}" ; \ - else echo "${GREEN}✓ Successfully rebuilt NGINX module ${NC}" ; fi - -.PHONY: stop-nginx -stop-nginx: - docker stop $(shell docker inspect --format="{{.Id}}" "$(DOCKER_IMAGE_NAME)-cont") ||: - -.PHONY: start-nginx -start-nginx: - docker run --rm --name "$(DOCKER_IMAGE_NAME)-cont" -d -p 8000:8000 $(DOCKER_ORG_NAME)/$(DOCKER_IMAGE_NAME) - docker cp $(DOCKER_IMAGE_NAME)-cont:/usr/lib64/nginx/modules/ngx_http_auth_jwt_module.so . - docker cp $(DOCKER_IMAGE_NAME)-cont:/usr/local/lib/libjansson.so.4.13.0 . - docker cp $(DOCKER_IMAGE_NAME)-cont:/usr/local/lib/libjwt.a . - docker cp $(DOCKER_IMAGE_NAME)-cont:/usr/local/lib/libjwt.la . - docker cp $(DOCKER_IMAGE_NAME)-cont:/usr/local/lib/libjwt.so.0.7.0 . - docker cp $(DOCKER_IMAGE_NAME)-cont:/usr/local/lib/pkgconfig/jansson.pc . - docker cp $(DOCKER_IMAGE_NAME)-cont:/usr/local/lib/pkgconfig/libjwt.pc . - -.PHONY: build-test-runner -build-test-runner: - docker image build -f Dockerfile-test -t $(DOCKER_ORG_NAME)/jwt-nginx-test-runner . - -.PHONY: frebuild-test-runner -rebuild-test-runner: - docker image build -f Dockerfile-test -t $(DOCKER_ORG_NAME)/jwt-nginx-test-runner . --no-cache - -.PHONY: test -test: - docker run --rm $(DOCKER_ORG_NAME)/jwt-nginx-test-runner diff --git a/README.md b/README.md index c04a8fd..0fb2ea8 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,52 @@ -# Intro -This is an NGINX module to check for a valid JWT and proxy to an upstream server or redirect to a login page. +# Auth-JWT NGINX Module -## Building and testing -To build the Docker image, start NGINX, and run our Bash test against it, run -```bash -make -``` +This is an NGINX module to check for a valid JWT and proxy to an upstream server or redirect to a login page. It supports additional features such as extracting claims from the JWT and placing them on the request/response headers. -When you make a change to the module, run `make rebuild-nginx`. +## Breaking Changes with v2 -When you make a change to `test.sh`, run `make rebuild-test-runner`. +The `v2` branch, which has now been merged to `master` includes breaking changes. Please see the initial v2 release for details, -| Command | Description | -| -------------------------- |:-------------------------------------------:| -| `make build-nginx` | Builds the NGINX image | -| `make rebuild-nginx` | Re-builds the NGINX image | -| `make build-test-runner` | Builds the image that will run `test.sh` | -| `make rebuild-test-runner` | Re-builds the image that will run `test.sh` | -| `make start-nginx` | Starts the NGINX container | -| `make stop-nginx` | Stops the NGINX container | -| `make test` | Runs `test.sh` against the NGINX container | +## Dependencies -You can re-run tests as many times as you like while NGINX is up. -When you're done running tests, make sure to stop the NGINX container. +This module depends on the [JWT C Library](https://github.com/benmcollins/libjwt). Transitively, that library depends on a JSON Parser called [Jansson](https://github.com/akheron/jansson) as well as the OpenSSL library. -The Dockerfile builds all of the dependencies as well as the module, -downloads a binary version of NGINX, and runs the module as a dynamic module. +## Directives -Tests get executed in containers. This project is 100% Docker-ized. +This module requires several new `nginx.conf` directives, which can be specified at the `http`, `server`, or `location` levels. See the [example NGINX config file](examples/nginx.conf) for more info. -## Dependencies -This module depends on the [JWT C Library](https://github.com/benmcollins/libjwt) +| Directive | Description | +| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `auth_jwt_key` | The key to use to decode/verify the JWT, *in binhex format* -- see below. | +| `auth_jwt_redirect` | Set to "on" to redirect to `auth_jwt_loginurl` if authentication fails. | +| `auth_jwt_loginurl` | The URL to redirect to if `auth_jwt_redirect` is enabled and authentication fails. | +| `auth_jwt_enabled` | Set to "on" to enable JWT checking. | +| `auth_jwt_algorithm` | The algorithm to use. One of: HS256, HS384, HS512, RS256, RS384, RS512 | +| `auth_jwt_location` | Indicates where the JWT is located in the request -- see below. | +| `auth_jwt_validate_sub` | Set to "on" to validate the `sub` claim (e.g. user id) in the JWT. | +| `auth_jwt_extract_var_claims` | Set to a space-delimited list of claims to extract from the JWT and make available as NGINX variables. These will be accessible via e.g: `$jwt_claim_sub` | +| `auth_jwt_extract_request_claims` | Set to a space-delimited list of claims to extract from the JWT and set as request headers. These will be accessible via e.g: `$http_jwt_sub` | +| `auth_jwt_extract_response_claims` | Set to a space-delimited list of claims to extract from the JWT and set as response headers. These will be accessible via e.g: `$sent_http_jwt_sub` | +| `auth_jwt_use_keyfile` | Set to "on" to read the key from a file rather than from the `auth_jwt_key` directive. | +| `auth_jwt_keyfile_path` | Set to the path from which the key should be read when `auth_jwt_use_keyfile` is enabled. | -Transitively, that library depends on a JSON Parser called -[Jansson](https://github.com/akheron/jansson) as well as the OpenSSL library. -## NGINX Directives -This module requires several new `nginx.conf` directives, -which can be specified in on the `main` `server` or `location` level. +## Algorithms -``` -auth_jwt_key "00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF"; # see docs for format based on algorithm -auth_jwt_loginurl "https://yourdomain.com/loginpage"; -auth_jwt_enabled on; -auth_jwt_algorithm HS256; # or RS256 -auth_jwt_validate_email on; # or off +The default algorithm is `HS256`, for symmetric key validation. When using one of the `HS*` algorithms, the value for `auth_jwt_key` should be specified in binhex format. It is recommended to use at least 256 bits of data (32 pairs of hex characters or 64 characters in total). Note that using more than 512 bits will not increase the security. For key guidelines please see [NIST Special Publication 800-107 Recommendation for Applications Using Approved Hash Algorithms](https://csrc.nist.gov/publications/detail/sp/800-107/rev-1/final), Section 5.3.2 The HMAC Key. + +To generate a 256-bit key (32 pairs of hex characters; 64 characters in total): + +```bash +openssl rand -hex 32 ``` -The default algorithm is 'HS256', for symmetric key validation. When using HS256, the value for `auth_jwt_key` should be specified in binhex format. It is recommended to use at least 256 bits of data (32 pairs of hex characters or 64 characters in total) as in the example above. Note that using more than 512 bits will not increase the security. For key guidelines please see NIST Special Publication 800-107 Recommendation for Applications Using Approved Hash Algorithms, Section 5.3.2 The HMAC Key. +### Additional Supported Algorithms -The configuration also supports the `auth_jwt_algorithm` 'RS256', for RSA 256-bit public key validation. If using "auth_jwt_algorithm RS256;", then the `auth_jwt_key` field must be set to your public key. -That is the public key, rather than a PEM certificate. I.e.: +The configuration also supports RSA public key validation via (e.g.) `auth_jwt_algorithm RS256`. When using the `RS*` alhorithms, the `auth_jwt_key` field must be set to your public key **OR** `auth_jwt_use_keyfile` should be set to `on` and `auth_jwt_keyfile_path` should point to the public key on disk. NGINX won't start if `auth_jwt_use_keyfile` is set to `on` and a key file is not provided. -``` +When using an `RS*` algorithm with an inline key, be sure to set `auth_jwt_key` to the _public key_, rather than a PEM certificate. E.g.: + +```nginx auth_jwt_key "-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0aPPpS7ufs0bGbW9+OFQ RvJwb58fhi2BuHMd7Ys6m8D1jHW/AhDYrYVZtUnA60lxwSJ/ZKreYOQMlNyZfdqA @@ -64,25 +58,204 @@ oQIDAQAB -----END PUBLIC KEY-----"; ``` -A typical use would be to specify the key and loginurl on the main level -and then only turn on the locations that you want to secure (not the login page). -Unauthorized requests are given 302 "Moved Temporarily" responses with a location of the specified loginurl. +When using an `RS*` algorithm with a public key file, do as follows: +```nginx +auth_jwt_use_keyfile on; +auth_jwt_keyfile_path "/path/to/pub_key.pem"; ``` -auth_jwt_redirect off; + +A typical use case would be to specify the key and login URL at the `http` level, and then only turn JWT authentication on for the locations which you want to secure (or vice-versa). Unauthorized requests will result in a `302 Moved Temporarily` response with the `Location` header set to the URL specified in the `auth_jwt_loginurl` directive, and a querystring parameter `return_url` whose value is the current / attempted URL. + +If you prefer to return `401 Unauthorized` rather than redirect, you may turn `auth_jwt_redirect` off: + +```nginx +auth_jwt_redirect off; ``` -If you prefer to return 401 Unauthorized, you may turn `auth_jwt_redirect` off. +## JWT Locations +By default, the`Authorization` header is used to provide a JWT for validation. However, you may use the `auth_jwt_location` directive to specify the name of the header or cookie which provides the JWT: + +```nginx +auth_jwt_location HEADER=auth-token; # get the JWT from the "auth-token" header +auth_jwt_location COOKIE=auth-token; # get the JWT from the "auth-token" cookie ``` -auth_jwt_validation_type AUTHORIZATION; -auth_jwt_validation_type COOKIE=rampartjwt; + +## `sub` Validation + +Optionally, the module can validate that a `sub` claim (e.g. the user's id) exists in the JWT. You may enable this feature as follows: + +```nginx +auth_jwt_validate_sub on; ``` -By default the authorization header is used to provide a JWT for validation. -However, you may use the `auth_jwt_validation_type` configuration to specify the name of a cookie that provides the JWT. +## Extracting Claims from the JWT + +You may specify claims to be extracted from the JWT and placed on the request and/or response headers. This is especially handly because the claims will then also be available as NGINX variables. + +If you only wish to access a claim as an NGINX variable, you should use `auth_jwt_extract_var_claims` so that the claim does not end up being sent to the client as a response header. However, if you do want the claim to be sent to the client in the response, you may use `auth_jwt_extract_response_claims` instead. + +_Please note that `number`, `boolean`, `array`, and `object` claims are not supported at this time -- only `string` claims are supported._ An error will be thrown if you attempt to extract a non-string claim. + +### Using Claims + +For example, you could configure an NGINX location which redirects to the current user's profile. Suppose `sub=abc-123`, the configuration below would redirect to `/profile/abc-123`. + +```nginx +location /profile/me { + auth_jwt_extract_var_claims sub; + + return 301 /profile/$jwt_claim_sub; +} +``` + +### Using Response Claims + +Response claims are used in the same way, with the only differences being: + - the variables are accessed via the `$sent_http_jwt_*` pattern, e.g. `$sent_http_jwt_sub`, and + - the headers are sent to the client. + +### Extracting Multiple Claims + +You may extract multiple claims by specifying all claims as arguments to a single directive, or by supplying multiple directives. The following two examples are equivalent. + +```nginx +auth_jwt_extract_request_claims sub firstName lastName; ``` -auth_jwt_validate_email off; + +```nginx +auth_jwt_extract_request_claims sub; +auth_jwt_extract_request_claims firstName; +auth_jwt_extract_request_claims lastName; +``` + +## Versioning + +This module has historically not been versioned, however, we are now starting to version the module in order to add clarity. We will add releases here in GitHub with additional details. In the future we may also publish pre-built modules for a selection of NGINX versions. + +## Contributing + +If you'd like to contribute to this repository, please first initiate the Git hooks by running `./.bin/init` (note the `.` before `bin`) -- this will ensure that tests are run before you push your changes. + +### Environment Set-up for Visual Studio Code + +1. Install the C/C++ extension from Microsoft. +2. Add a C/C++ config file at `.vscode/c_cpp_properties.json` with the following (or similar) content: + +```json +{ + "configurations": [ + { + "name": "Linux", + "includePath": [ + "${workspaceFolder}/**", + "~/Projects/nginx/objs/**", + "~/Projects/nginx/src/**", + "~/Projects/libjwt/include/**", + "~/Projects/jansson/src/**" + ], + "defines": [], + "compilerPath": "/usr/bin/clang", + "cStandard": "c17", + "cppStandard": "c++14", + "intelliSenseMode": "linux-clang-x64" + } + ], + "version": 4 +} +``` + +Note the `includePath` additions above -- please update them as appropriate. Next we need to pull these sources. + +#### Building NGINX + +1. Download the NGINX release matching the version you're targeting. +2. Extract the NGINX archive to wherever you'd like. +3. Update the `includePath` entires shown above to match the location you chose. +4. Enter the directory where you extracted NGINX and run: `./configure --with-compat` + +#### Cloning `libjwt` + +1. Clone this repository as follows (replace ``): `git clone git@github.com:benmcollins/libjwt.git ` +2. Enter the directory and switch to the latest tag: `git checkout $(git tag | sort -Vr | head -n 1)` +3. Update the `includePath` entires shown above to match the location you chose. + +#### Cloning `libjansson` + +1. Clone this repository as follows (replace ``): `git clone git@github.com:akheron/jansson.git ` +2. Enter the directory and switch to the latest tag: `git checkout $(git tag | sort -Vr | head -n 1)` +3. Update the `includePath` entires shown above to match the location you chose. + +#### Verifying Compliation + +Once you save your changes to `.vscode/c_cpp_properties.json`, you should see that warnings and errors in the Problems panel go away, at least temprorarily. Hopfeully they don't come back, but if they do, make sure your include paths are set correctly. + +### Building and Testing + +The `./scripts` file contains multiple commands to make things easy: + +| Command | Description | +| --------------------- | ----------------------------------------------------------------- | +| `build_module` | Builds the NGINX image. | +| `rebuild_module` | Re-builds the NGINX image. | +| `start` | Starts the NGINX container. | +| `stop` | Stops the NGINX container. | +| `cp_bin` | Copies the compiled binaries out of the NGINX container. | +| `build_test` | Builds the images used by the test stack. | +| `rebuild_test` | Re-builds the images used by the test stack. | +| `test` | Runs `test.sh` against the NGINX container. | +| `test_now` | Runs `test.sh` without rebuilding. | + +You can run multiple commands in sequence by separating them with a space, e.g.: + +```shell +./scripts build_module +./scripts test +``` + +To build the Docker images, module, start NGINX, and run the tests against it for all versions, you can simply do: + +```shell +./scripts all +``` + +When you make a change to the module, running `./scripts test` should build a fresh module and run the tests. Note that `rebuild_module` is not often needed as Docker will automatically rebuild the image if the source files have +changed. + +When you make a change to the test NGINX config or `test.sh`, run `./scripts test` to run the tests. + +The image produced with `./scripts build_module` only differs from the official NGINX image in two ways: + - the JWT module itself, and + - the `nginx.conf` file is overwritten with our own. + +The tests use a customized NGINX image, distinct from the main image, as well as a test runner image. By running `./scripts test`, the two test containers will be stood up via Docker Compose, then they'll be started, and the tests will run. At the end of the test run, both containers will be automatically stopped and destroyed. See below to learn how to trace test failures across runs. + +#### Tracing Test Failures + +After making changes and finding that some tests fail, it can be difficult to understand why. By default, logs are written to Docker's internal log mechanism, but they won't be persisted after the test run completes and the containers are removed. + +If you'd like to persist logs across test runs, you can configure the log driver to use `journald` (on Linux/Unix systems for example). You can do this by setting the environment variable `LOG_DRIVER` before running the tests: + +```shell +# need to rebuild the test runner with the proper log driver +export LOG_DRIVER=journald + +# rebuild the test images +./scripts rebuild_test + +# run the tests +./scripts test + +# check the logs -- adjust the container name as needed +journalctl -eu docker CONTAINER_NAME=nginx-auth-jwt-test-nginx +``` + +Now you'll be able to see logs from previous test runs. The best way to make use of this is to open two terminals, one where you run the tests, and one where you follow the logs: + +```shell +# terminal 1 +./scripts test + +# terminal 2 +journalctl -fu docker CONTAINER_NAME=jwt-nginx-test ``` -By default, the module will attempt to validate the email address field of the JWT, then set the x-email header of the -session, and will log an error if it isn't found. To disable this behavior, for instance if you are using a different -user identifier property such as 'sub', set `auth_jwt_validate_email` to the value `off`. diff --git a/config b/config index 317aeea..0271a79 100644 --- a/config +++ b/config @@ -1,8 +1,7 @@ -ngx_addon_name=ngx_http_auth_jwt_module - ngx_module_type=HTTP -ngx_module_name=ngx_http_auth_jwt_module -ngx_module_srcs="$ngx_addon_dir/src/ngx_http_auth_jwt_binary_converters.c $ngx_addon_dir/src/ngx_http_auth_jwt_header_processing.c $ngx_addon_dir/src/ngx_http_auth_jwt_string.c $ngx_addon_dir/src/ngx_http_auth_jwt_module.c" -ngx_module_libs="-ljansson -ljwt" +ngx_addon_name=ngx_http_auth_jwt_module +ngx_module_name=$ngx_addon_name +ngx_module_srcs="${ngx_addon_dir}/src/arrays.c ${ngx_addon_dir}/src/ngx_http_auth_jwt_binary_converters.c ${ngx_addon_dir}/src/ngx_http_auth_jwt_header_processing.c ${ngx_addon_dir}/src/ngx_http_auth_jwt_string.c ${ngx_addon_dir}/src/ngx_http_auth_jwt_module.c" +ngx_module_libs="-ljansson -ljwt -lm" . auto/module diff --git a/examples/nginx.conf b/examples/nginx.conf new file mode 100644 index 0000000..9795363 --- /dev/null +++ b/examples/nginx.conf @@ -0,0 +1,34 @@ +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log notice; +pid /var/run/nginx.pid; +load_module /usr/lib64/nginx/modules/ngx_http_auth_jwt_module.so; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + access_log /var/log/nginx/access.log main; + + auth_jwt_enabled on; + auth_jwt_algorithm 'put_algo_here'; + auth_jwt_key 'put_key_here'; + auth_jwt_location 'COOKIE=auth-token'; + auth_jwt_redirect on; + auth_jwt_loginurl 'put_login_url_here'; + + # Include other auth_jwt_* directives as needed. + + sendfile on; + keepalive_timeout 65; + + include /etc/nginx/conf.d/*.conf; +} diff --git a/nginx.dockerfile b/nginx.dockerfile new file mode 100644 index 0000000..68c67f0 --- /dev/null +++ b/nginx.dockerfile @@ -0,0 +1,175 @@ +ARG BASE_IMAGE=${:?required} +ARG NGINX_VERSION +ARG LIBJWT_VERSION + +FROM ${BASE_IMAGE} AS ngx_http_auth_jwt_builder +LABEL stage=ngx_http_auth_jwt_builder +ENV PATH="${PATH}:/etc/nginx" +ENV LD_LIBRARY_PATH=/usr/local/lib +ARG NGINX_VERSION +ARG LIBJWT_VERSION + +RUN <<` + set -e + apt-get update + apt-get upgrade -y +` + +RUN apt-get install -y curl git zlib1g-dev libpcre3-dev build-essential libpcre2-dev zlib1g-dev libpcre3-dev pkg-config cmake dh-autoreconf + +WORKDIR /root/build/libjansson +RUN <<` + set -e + git clone --depth 1 --branch v2.14 https://github.com/akheron/jansson . + cmake . -DJANSSON_BUILD_SHARED_LIBS=1 -DJANSSON_BUILD_DOCS=OFF + make + make check + make install +` + +WORKDIR /root/build/libjwt +RUN <<` + set -e + git clone --depth 1 --branch v${LIBJWT_VERSION} https://github.com/benmcollins/libjwt . + autoreconf -i + ./configure + make all + make install +` + +WORKDIR /root/build/ngx-http-auth-jwt-module +ADD config ./ +ADD src/*.h src/*.c ./src/ +WORKDIR /root/build +RUN <<` + set -e + mkdir nginx + curl -O http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz + tar -xzf nginx-${NGINX_VERSION}.tar.gz --strip-components 1 -C nginx +` + +WORKDIR /root/build/nginx +RUN <<` + set -e + BUILD_FLAGS='' + MAJ=$(echo ${NGINX_VERSION} | cut -f1 -d.) + MIN=$(echo ${NGINX_VERSION} | cut -f2 -d.) + REV=$(echo ${NGINX_VERSION} | cut -f3 -d.) + + # NGINX 1.23.0+ changes cookies to use a linked list, and renames `cookies` to `cookie` + if [ "${MAJ}" -gt 1 ] || [ "${MAJ}" -eq 1 -a "${MIN}" -ge 23 ]; then + BUILD_FLAGS="${BUILD_FLAGS} --with-cc-opt='-DNGX_LINKED_LIST_COOKIES=1'" + fi + + ./configure \ + --prefix=/etc/nginx \ + --sbin-path=/usr/sbin/nginx \ + --modules-path=/usr/lib64/nginx/modules \ + --conf-path=/etc/nginx/nginx.conf \ + --error-log-path=/var/log/nginx/error.log \ + --http-log-path=/var/log/nginx/access.log \ + --pid-path=/var/run/nginx.pid \ + --lock-path=/var/run/nginx.lock \ + --http-client-body-temp-path=/var/cache/nginx/client_temp \ + --http-proxy-temp-path=/var/cache/nginx/proxy_temp \ + --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp \ + --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp \ + --http-scgi-temp-path=/var/cache/nginx/scgi_temp \ + --user=nginx \ + --group=nginx \ + --with-compat \ + --with-debug \ + --with-file-aio \ + --with-threads \ + --with-http_addition_module \ + --with-http_auth_request_module \ + --with-http_dav_module \ + --with-http_flv_module \ + --with-http_gunzip_module \ + --with-http_gzip_static_module \ + --with-http_mp4_module \ + --with-http_random_index_module \ + --with-http_realip_module \ + --with-http_secure_link_module \ + --with-http_slice_module \ + --with-http_ssl_module \ + --with-http_stub_status_module \ + --with-http_sub_module \ + --with-http_v2_module \ + --with-mail \ + --with-mail_ssl_module \ + --with-stream \ + --with-stream_realip_module \ + --with-stream_ssl_module \ + --with-stream_ssl_preread_module \ + --with-cc-opt='-g -O2 -ffile-prefix-map=/data/builder/debuild/nginx-1.25.4/debian/debuild-base/nginx-1.25.4=. -fstack-protector-strong -Wformat -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fPIC' \ + --with-ld-opt='-Wl,-z,relro -Wl,-z,now -Wl,--as-needed -pie' \ + --add-dynamic-module=../ngx-http-auth-jwt-module \ + ${BUILD_FLAGS} + # --with-openssl=/usr/local \ +` + +RUN make modules +RUN make install + +WORKDIR /usr/lib/nginx/modules +RUN mv /root/build/nginx/objs/ngx_http_auth_jwt_module.so . +RUN rm -rf /root/build + +RUN <<` + set -e + apt-get remove -y curl git zlib1g-dev libpcre3-dev build-essential libpcre2-dev zlib1g-dev libpcre3-dev pkg-config cmake dh-autoreconf + # apt-get install -y gnupg2 ca-certificates lsb-release debian-archive-keyring + apt-get clean +` + +RUN <<` + set -e + groupadd nginx + useradd -g nginx nginx +` + +# RUN <<` +# set -e +# curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor > /usr/share/keyrings/nginx-archive-keyring.gpg +# printf "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] http://nginx.org/packages/debian `lsb_release -cs` nginx\n" > /etc/apt/sources.list.d/nginx.list +# printf "Package: *\nPin: origin nginx.org\nPin: release o=nginx\nPin-Priority: 900\n" > /etc/apt/preferences.d/99nginx +# ` + +# RUN <<` +# set -e +# apt-get update +# apt-get install -y nginx +# ` + +COPY <<` /etc/nginx/nginx.conf +daemon off; +user nginx; +pid /var/run/nginx.pid; + +load_module /usr/lib/nginx/modules/ngx_http_auth_jwt_module.so; + +worker_processes 1; + +events { + worker_connections 1024; +} + +http { + include mime.types; + default_type application/octet-stream; + + log_format main '\$remote_addr - \$remote_user [\$time_local] "\$request" ' + '\$status \$body_bytes_sent "\$http_referer" ' + '"\$http_user_agent" "\$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + include conf.d/*.conf; +} +` + +WORKDIR /var/cache/nginx +RUN chown nginx:nginx . + +WORKDIR / +CMD ["nginx"] diff --git a/openssl.dockerfile b/openssl.dockerfile new file mode 100644 index 0000000..9839d29 --- /dev/null +++ b/openssl.dockerfile @@ -0,0 +1,38 @@ +ARG BASE_IMAGE=debian:bookworm-slim + +FROM ${BASE_IMAGE} +ARG SSL_VERSION=3.2.1 +ENV SRC_DIR=/tmp/openssl-src +ENV OUT_DIR=/usr/local/.openssl +RUN chmod 1777 /tmp +RUN <<` + set -e + apt-get update + apt-get install -y curl build-essential libssl-dev libz-dev + apt-get remove -y openssl + apt-get clean +` +WORKDIR ${SRC_DIR} +RUN <<` + set -ex + curl --silent -LO https://www.openssl.org/source/openssl-${SSL_VERSION}.tar.gz + tar -xf openssl-${SSL_VERSION}.tar.gz --strip-components=1 +` +RUN ./config --prefix=${OUT_DIR} --openssldir=${OUT_DIR} shared zlib +RUN <<` + set -e + make + make test + make install +` +RUN <<` + set -e + echo "${OUT_DIR}/lib" > /etc/ld.so.conf.d/openssl-${SSL_VERSION}.conf + ldconfig + + ln -sf ${OUT_DIR}/bin/openssl /usr/bin/openssl + ln -sf ${OUT_DIR}/lib64/libssl.so.3 /lib/$(uname -m)-linux-gnu/libssl.so.3 + ln -sf ${OUT_DIR}/lib64/libcrypto.so.3 /lib/$(uname -m)-linux-gnu/libcrypto.so.3 +` +WORKDIR / +RUN rm -rf ${SRC_DIR} \ No newline at end of file diff --git a/resources/nginx.conf b/resources/nginx.conf deleted file mode 100644 index 7ea8afb..0000000 --- a/resources/nginx.conf +++ /dev/null @@ -1,43 +0,0 @@ - -user nginx; -worker_processes 1; - -error_log /var/log/nginx/error.log info; -pid /var/run/nginx.pid; - -load_module /usr/lib64/nginx/modules/ngx_http_auth_jwt_module.so; - -events { - worker_connections 1024; -} - - -http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - - log_format upstream_time '$remote_addr $sent_http_x_userid [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for" ' - 'rt="$request_time" uct="$upstream_connect_time" ' - 'uht="$upstream_header_time" urt="$upstream_response_time" ' - '$sent_http_x_email'; - - access_log /var/log/nginx/access.log upstream_time; - - sendfile on; - #tcp_nopush on; - - keepalive_timeout 65; - - #gzip on; - - proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Server $remote_addr; - - include /etc/nginx/conf.d/*.conf; -} - -daemon off; diff --git a/resources/test-jwt-nginx.conf b/resources/test-jwt-nginx.conf deleted file mode 100644 index b39eb95..0000000 --- a/resources/test-jwt-nginx.conf +++ /dev/null @@ -1,52 +0,0 @@ -server { - auth_jwt_key "00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF"; - auth_jwt_loginurl "https://teslagov.com"; - auth_jwt_enabled off; - auth_jwt_redirect on; - - listen 8000; - server_name localhost; - - location ~ ^/secure-no-redirect/ { - auth_jwt_enabled on; - auth_jwt_redirect off; - root /usr/share/nginx; - index index.html index.htm; - } - - location ~ ^/secure/ { - auth_jwt_enabled on; - auth_jwt_validation_type COOKIE=rampartjwt; - root /usr/share/nginx; - index index.html index.htm; - } - - location ~ ^/secure-auth-header/ { - auth_jwt_enabled on; - root /usr/share/nginx; - index index.html index.htm; - } - - location ~ ^/secure-rs256/ { - auth_jwt_enabled on; - auth_jwt_validation_type COOKIE=rampartjwt; - auth_jwt_algorithm RS256; - auth_jwt_key "-----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwtpMAM4l1H995oqlqdMh -uqNuffp4+4aUCwuFE9B5s9MJr63gyf8jW0oDr7Mb1Xb8y9iGkWfhouZqNJbMFry+ -iBs+z2TtJF06vbHQZzajDsdux3XVfXv9v6dDIImyU24MsGNkpNt0GISaaiqv51NM -ZQX0miOXXWdkQvWTZFXhmsFCmJLE67oQFSar4hzfAaCulaMD+b3Mcsjlh0yvSq7g -6swiIasEU3qNLKaJAZEzfywroVYr3BwM1IiVbQeKgIkyPS/85M4Y6Ss/T+OWi1Oe -K49NdYBvFP+hNVEoeZzJz5K/nd6C35IX0t2bN5CVXchUFmaUMYk2iPdhXdsC720t -BwIDAQAB ------END PUBLIC KEY-----"; - root /usr/share/nginx; - index index.html index.htm; - } - - location / { - root /usr/share/nginx/html; - index index.html index.htm; - } -} - diff --git a/scripts b/scripts new file mode 100755 index 0000000..7ce7024 --- /dev/null +++ b/scripts @@ -0,0 +1,275 @@ +#!/bin/bash -eu + +MAGENTA='\u001b[35m' +BLUE='\033[0;34m' +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' + +# supported SSL versions +SSL_VERSION_1_1_1w='1.1.1w' +SSL_VERSION_3_0_15='3.0.15' +SSL_VERSION_3_2_1='3.2.1' +SSL_VERSIONS=( + ${SSL_VERSION_1_1_1w} + ${SSL_VERSION_3_0_15} + ${SSL_VERSION_3_2_1} +) + +declare -A SSL_IMAGE_MAP +SSL_IMAGE_MAP[$SSL_VERSION_1_1_1w]="bullseye-slim:openssl-${SSL_VERSION_1_1_1w}" +SSL_IMAGE_MAP[$SSL_VERSION_3_0_15]="bookworm-slim:openssl-${SSL_VERSION_3_0_15}" +SSL_IMAGE_MAP[$SSL_VERSION_3_2_1]="bookworm-slim:openssl-${SSL_VERSION_3_2_1}" + +# supported NGINX versions -- for binary distribution +NGINX_VERSIONS=( + 1.20.2 # legacy + 1.22.1 # legacy + 1.24.0 # legacy + 1.26.2 # stable + 1.26.3 # stable + 1.27.3 # mainline + 1.27.4 # mainline +) + +# The following versions of libjwt are compatible: +# * v1.0 - v1.12.0 +# * v1.12.1 - v1.14.0 +# * v1.15.0+ +# At the time of writing this: +# * Debian and Ubuntu's repos have v1.10.2 +# * EPEL has v1.12.1 +# This compiles against each version prior to a breaking change and the latest release +LIBJWT_VERSION_DEBIAN=1.12.0 +LIBJWT_VERSION_EPEL=1.14.0 +LIBJWT_VERSION_LATEST=1.15.3 +LIBJWT_VERSIONS=( + ${LIBJWT_VERSION_DEBIAN} + ${LIBJWT_VERSION_EPEL} + ${LIBJWT_VERSION_LATEST} +) + +SSL_VERSION=${SSL_VERSION:-$SSL_VERSION_3_0_15} +NGINX_VERSION=${NGINX_VERSION:-${NGINX_VERSIONS[-1]}} +LIBJWT_VERSION=${LIBJWT_VERSION:-${LIBJWT_VERSION_DEBIAN}} +IMAGE_NAME=${IMAGE_NAME:-nginx-auth-jwt} +FULL_IMAGE_NAME=${ORG_NAME:-teslagov}/${IMAGE_NAME} +TEST_CONTAINER_NAME_PREFIX="${IMAGE_NAME}-test" +TEST_COMPOSE_FILE='test/docker-compose-test.yml' + +all() { + build_module + build_test + test_all +} + +build_base_image() { + local image=${SSL_IMAGE_MAP[$SSL_VERSION]} + local baseImage=${image%%:*} + + if [ -z ${image} ]; then + echo "Base image not set for SSL version :${SSL_VERSION}" + exit 1 + else + printf "${MAGENTA}Building ${baseImage} base image for SSL ${SSL_VERSION}...${NC}\n" + docker buildx build \ + --platform linux/amd64 \ + --build-arg BASE_IMAGE=debian:${baseImage} \ + --build-arg SSL_VERSION=${SSL_VERSION} \ + -f openssl.dockerfile \ + -t ${image} \ + . + fi +} + +build_module() { + local baseImage=${SSL_IMAGE_MAP[$SSL_VERSION]} + + build_base_image + + printf "${MAGENTA}Building module for NGINX ${NGINX_VERSION}, libjwt ${LIBJWT_VERSION}...${NC}\n" + docker buildx build \ + --platform linux/amd64 \ + -f nginx.dockerfile \ + -t ${FULL_IMAGE_NAME}:${NGINX_VERSION} \ + --build-arg BASE_IMAGE=${baseImage} \ + --build-arg NGINX_VERSION=${NGINX_VERSION} \ + --build-arg LIBJWT_VERSION=${LIBJWT_VERSION} \ + . + + if [ "$?" -ne 0 ]; then + printf "${RED}✘ Build failed ${NC}\n" + else + printf "${GREEN}✔ Successfully built NGINX module ${NC}\n" + fi +} + +rebuild_module() { + docker rmi -f $(docker images --filter=label=stage=ngx_http_auth_jwt_builder --quiet) 2> /dev/null || true + + build_module +} + +start() { + local port=$(get_port) + + printf "${MAGENTA}Starting NGINX container (${IMAGE_NAME}) on port ${port}...${NC}\n" + docker run --rm --name "${IMAGE_NAME}" -d -p ${port}:80 ${FULL_IMAGE_NAME}:${NGINX_VERSION} >/dev/null +} + +stop() { + docker stop "${IMAGE_NAME}" >/dev/null +} + +cp_bin() { + local destDir=bin + local stopContainer=0; + + if [ "$(docker container inspect -f '{{.State.Running}}' ${IMAGE_NAME} | true)" != "true" ]; then + start + stopContainer=1 + fi + + printf "${MAGENTA}Copying binaries to: ${destDir}${NC}\n" + rm -rf ${destDir}/* + mkdir -p ${destDir} + docker exec "${IMAGE_NAME}" sh -c "cd /; tar -chf - \ + usr/lib/nginx/modules/ngx_http_auth_jwt_module.so \ + usr/local/lib/libjansson.so.* \ + usr/local/lib/libjwt.*" | tar -xf - -C ${destDir} &>/dev/null + + if [ $stopContainer ]; then + printf "${MAGENTA}Stopping NGINX container (${IMAGE_NAME})...${NC}\n" + stop + fi +} + +make_release() { + local moduleVersion=$(git describe --tags --abbrev=0) + + printf "${MAGENTA}Making release for version ${moduleVersion} for NGINX ${NGINX_VERSION}...${NC}\n" + + rebuild_module + rebuild_test + test --no-build + cp_bin + + mkdir -p release + tar -czvf release/ngx-http-auth-jwt-module-${moduleVersion}_libjwt-${LIBJWT_VERSION}_nginx-${NGINX_VERSION}.tgz \ + README.md \ + -C bin/usr/lib/nginx/modules ngx_http_auth_jwt_module.so > /dev/null +} + +# Create releases for all NGINX versions defined in `NGINX_VERSIONS`. +make_releases() { + rm -rf release/* + + for NGINX_VERSION in ${NGINX_VERSIONS[@]}; do + for LIBJWT_VERSION in ${LIBJWT_VERSIONS[@]}; do + export NGINX_VERSION LIBJWT_VERSION + make_release + done + done +} + +build_test() { + local dockerArgs=${1:-} + local port=$(get_port) + local sslPort=$(get_port $((port + 1))) + local runnerBaseImage=${SSL_IMAGE_MAP[$SSL_VERSION]} + + export TEST_CONTAINER_NAME_PREFIX + export FULL_IMAGE_NAME + export NGINX_VERSION + + printf "${MAGENTA}Building test NGINX & runner using port ${port}...${NC}\n" + docker compose \ + -p ${TEST_CONTAINER_NAME_PREFIX} \ + -f ${TEST_COMPOSE_FILE} \ + build \ + --build-arg RUNNER_BASE_IMAGE=${runnerBaseImage} \ + --build-arg PORT=${port} \ + --build-arg SSL_PORT=${sslPort} \ + ${dockerArgs} +} + +rebuild_test() { + build_test --no-cache +} + +test_all() { + for SSL_VERSION in "${SSL_VERSIONS[@]}"; do + for NGINX_VERSION in "${NGINX_VERSIONS[@]}"; do + for LIBJWT_VERSION in ${LIBJWT_VERSIONS[@]}; do + export SSL_VERSION NGINX_VERSION LIBJWT_VERSION + test + done + done + done +} + +test() { + if [[ ! "$*" =~ --no-build ]]; then + build_module + build_test + fi + + trap 'test_cleanup' 0 + + printf "${MAGENTA}Running tests...${NC}\n" + docker compose \ + -p ${TEST_CONTAINER_NAME_PREFIX} \ + -f ${TEST_COMPOSE_FILE} up \ + --no-start + + test_now +} + +test_now() { + nginxContainerName="${TEST_CONTAINER_NAME_PREFIX}-nginx" + runnerContainerName="${TEST_CONTAINER_NAME_PREFIX}-runner" + + echo + echo "Executing tests with the following options:" + echo " SSL Version: ${SSL_VERSION}" + echo " LIBJWT Version: ${LIBJWT_VERSION}" + echo " NGINX Version: ${NGINX_VERSION}" + + docker start ${nginxContainerName} + + if [ "$(docker container inspect -f '{{.State.Running}}' ${nginxContainerName})" != "true" ]; then + printf "${RED}Failed to start container \"${nginxContainerName}\". See logs below:\n" + docker logs ${nginxContainerName} + printf "${NC}\n" + return 1 + fi + + docker start -a ${runnerContainerName} +} + +test_cleanup() { + docker compose \ + -p ${TEST_CONTAINER_NAME_PREFIX} \ + -f ${TEST_COMPOSE_FILE} down +} + +get_port() { + startPort=${1:-8000} + endPort=$((startPort + 100)) + + for p in $(seq ${startPort} ${endPort}); do + if ! ss -ln | grep -q ":${p} "; then + echo ${p} + break + fi + done +} + +if [ $# -eq 0 ]; then + all +else + fn=$1 + shift + + ${fn} "$@" +fi diff --git a/src/arrays.c b/src/arrays.c new file mode 100644 index 0000000..043c24e --- /dev/null +++ b/src/arrays.c @@ -0,0 +1,14 @@ +#include "arrays.h" +#include + +void merge_array(ngx_pool_t *pool, ngx_array_t **dest, const ngx_array_t *src, size_t size) +{ + // only merge if dest is non-null and src is null + if (src != NULL && *dest == NULL) + { + *dest = ngx_array_create(pool, src->nelts, size); + + ngx_memcpy((*dest)->elts, src->elts, src->nelts * size); + (*dest)->nelts = src->nelts; + } +} diff --git a/src/arrays.h b/src/arrays.h new file mode 100644 index 0000000..5e17158 --- /dev/null +++ b/src/arrays.h @@ -0,0 +1,7 @@ +#ifndef _ARRAYS_H +#define _ARRAYS_H +#include + +void merge_array(ngx_pool_t *pool, ngx_array_t **dest, const ngx_array_t *src, size_t size); + +#endif \ No newline at end of file diff --git a/src/ngx_http_auth_jwt_binary_converters.c b/src/ngx_http_auth_jwt_binary_converters.c index 8aea970..8b60560 100644 --- a/src/ngx_http_auth_jwt_binary_converters.c +++ b/src/ngx_http_auth_jwt_binary_converters.c @@ -8,42 +8,56 @@ */ #include "ngx_http_auth_jwt_binary_converters.h" - #include -int hex_char_to_binary( char ch, char* ret ) +int hex_char_to_binary(char ch, char *ret) { - ch = tolower( ch ); - if( isdigit( ch ) ) + ch = tolower(ch); + + if (isdigit(ch)) + { *ret = ch - '0'; - else if( ch >= 'a' && ch <= 'f' ) - *ret = ( ch - 'a' ) + 10; - else if( ch >= 'A' && ch <= 'F' ) - *ret = ( ch - 'A' ) + 10; + } + else if (ch >= 'a' && ch <= 'f') + { + *ret = (ch - 'a') + 10; + } + else if (ch >= 'A' && ch <= 'F') + { + *ret = (ch - 'A') + 10; + } else - return *ret = 0; - return 1; + { + return -1; + } + + return 0; } -int hex_to_binary( const char* str, u_char* buf, int len ) +int hex_to_binary(const char *str, u_char *buf, int len) { - u_char - *cpy = buf; - char - low, - high; - int - odd = len % 2; - - if (odd) { + int odd = len % 2; + + if (odd) + { return -1; } - - for (int i = 0; i < len; i += 2) { - hex_char_to_binary( *(str + i), &high ); - hex_char_to_binary( *(str + i + 1 ), &low ); + else + { + u_char *cpy = buf; + char low; + char high; - *cpy++ = low | (high << 4); + for (int i = 0; i < len; i += 2) + { + if (hex_char_to_binary(*(str + i), &high) != 0 || hex_char_to_binary(*(str + i + 1), &low) != 0) + { + return -2; + } + + *cpy++ = low | (high << 4); + } + + return 0; } - return 0; } \ No newline at end of file diff --git a/src/ngx_http_auth_jwt_header_processing.c b/src/ngx_http_auth_jwt_header_processing.c index e368c84..ff648b9 100644 --- a/src/ngx_http_auth_jwt_header_processing.c +++ b/src/ngx_http_auth_jwt_header_processing.c @@ -16,80 +16,73 @@ * Sample code from nginx. * https://www.nginx.com/resources/wiki/start/topics/examples/headers_management/?highlight=http%20settings */ -ngx_table_elt_t* search_headers_in(ngx_http_request_t *r, u_char *name, size_t len) +ngx_table_elt_t *search_headers_in(ngx_http_request_t *r, u_char *name, size_t len) { - ngx_list_part_t *part; - ngx_table_elt_t *h; - ngx_uint_t i; + ngx_list_part_t *part; + ngx_table_elt_t *h; + ngx_uint_t i; - // Get the first part of the list. There is usual only one part. - part = &r->headers_in.headers.part; - h = part->elts; + // Get the first part of the list. There is usual only one part. + part = &r->headers_in.headers.part; + h = part->elts; - // Headers list array may consist of more than one part, so loop through all of it - for (i = 0; /* void */ ; i++) - { - if (i >= part->nelts) - { - if (part->next == NULL) - { - /* The last part, search is done. */ - break; - } + // Headers list array may consist of more than one part, so loop through all of it + for (i = 0; /* void */; ++i) + { + if (i >= part->nelts) + { + if (part->next == NULL) + { + /* The last part, search is done. */ + break; + } - part = part->next; - h = part->elts; - i = 0; - } + part = part->next; + h = part->elts; + i = 0; + } - //Just compare the lengths and then the names case insensitively. - if (len != h[i].key.len || ngx_strcasecmp(name, h[i].key.data) != 0) - { - /* This header doesn't match. */ - continue; - } + // Just compare the lengths and then the names case insensitively. + if (len != h[i].key.len || ngx_strcasecmp(name, h[i].key.data) != 0) + { + /* This header doesn't match. */ + continue; + } - /* - * Ta-da, we got one! - * Note, we've stopped the search at the first matched header - * while more then one header may match. - */ - return &h[i]; - } + /* + * Ta-da, we got one! + * Note, we've stopped the search at the first matched header + * while more then one header may match. + */ + return &h[i]; + } - /* No headers was found */ - return NULL; + /* No headers found */ + return NULL; } -/** - * Sample code from nginx - * https://www.nginx.com/resources/wiki/start/topics/examples/headers_management/#how-can-i-set-a-header - */ -ngx_int_t set_custom_header_in_headers_out(ngx_http_request_t *r, ngx_str_t *key, ngx_str_t *value) { - ngx_table_elt_t *h; +ngx_int_t set_request_header(ngx_http_request_t *r, ngx_str_t *key, ngx_str_t *value) +{ + return set_header(ngx_list_push(&r->headers_in.headers), key, value); +} - /* - All we have to do is just to allocate the header... - */ - h = ngx_list_push(&r->headers_out.headers); - if (h == NULL) { - return NGX_ERROR; - } +ngx_int_t set_response_header(ngx_http_request_t *r, ngx_str_t *key, ngx_str_t *value) +{ + return set_header(ngx_list_push(&r->headers_out.headers), key, value); +} - /* - ... setup the header key ... - */ +ngx_int_t set_header(ngx_table_elt_t *h, ngx_str_t *key, ngx_str_t *value) +{ + if (h == NULL) + { + return NGX_ERROR; + } + else + { h->key = *key; - - /* - ... and the value. - */ h->value = *value; - - /* - Mark the header as not deleted. - */ h->hash = 1; return NGX_OK; + } } \ No newline at end of file diff --git a/src/ngx_http_auth_jwt_header_processing.h b/src/ngx_http_auth_jwt_header_processing.h index 0b64133..acd1762 100644 --- a/src/ngx_http_auth_jwt_header_processing.h +++ b/src/ngx_http_auth_jwt_header_processing.h @@ -9,6 +9,8 @@ #define _NGX_HTTP_AUTH_JWT_HEADER_PROCESSING_H ngx_table_elt_t* search_headers_in(ngx_http_request_t *r, u_char *name, size_t len); -ngx_int_t set_custom_header_in_headers_out(ngx_http_request_t *r, ngx_str_t *key, ngx_str_t *value); +ngx_int_t set_request_header(ngx_http_request_t *r, ngx_str_t *key, ngx_str_t *value); +ngx_int_t set_response_header(ngx_http_request_t *r, ngx_str_t *key, ngx_str_t *value); +ngx_int_t set_header(ngx_table_elt_t *h, ngx_str_t *key, ngx_str_t *value); #endif /* _NGX_HTTP_AUTH_JWT_HEADER_PROCESSING_H */ \ No newline at end of file diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index a44e3b9..59b84ac 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -14,442 +14,910 @@ #include +#include "arrays.h" #include "ngx_http_auth_jwt_header_processing.h" #include "ngx_http_auth_jwt_binary_converters.h" #include "ngx_http_auth_jwt_string.h" -typedef struct { - ngx_str_t auth_jwt_loginurl; - ngx_str_t auth_jwt_key; - ngx_flag_t auth_jwt_enabled; - ngx_flag_t auth_jwt_redirect; - ngx_str_t auth_jwt_validation_type; - ngx_str_t auth_jwt_algorithm; - ngx_flag_t auth_jwt_validate_email; - -} ngx_http_auth_jwt_loc_conf_t; - -static ngx_int_t ngx_http_auth_jwt_init(ngx_conf_t *cf); -static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r); -static void * ngx_http_auth_jwt_create_loc_conf(ngx_conf_t *cf); -static char * ngx_http_auth_jwt_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child); -static char * getJwt(ngx_http_request_t *r, ngx_str_t auth_jwt_validation_type); - -static ngx_command_t ngx_http_auth_jwt_commands[] = { - - { ngx_string("auth_jwt_loginurl"), - NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, - ngx_conf_set_str_slot, - NGX_HTTP_LOC_CONF_OFFSET, - offsetof(ngx_http_auth_jwt_loc_conf_t, auth_jwt_loginurl), - NULL }, - - { ngx_string("auth_jwt_key"), - NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, - ngx_conf_set_str_slot, - NGX_HTTP_LOC_CONF_OFFSET, - offsetof(ngx_http_auth_jwt_loc_conf_t, auth_jwt_key), - NULL }, - - { ngx_string("auth_jwt_enabled"), - NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG, - ngx_conf_set_flag_slot, - NGX_HTTP_LOC_CONF_OFFSET, - offsetof(ngx_http_auth_jwt_loc_conf_t, auth_jwt_enabled), - NULL }, - - { ngx_string("auth_jwt_redirect"), - NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG, - ngx_conf_set_flag_slot, - NGX_HTTP_LOC_CONF_OFFSET, - offsetof(ngx_http_auth_jwt_loc_conf_t, auth_jwt_redirect), - NULL }, - - { ngx_string("auth_jwt_validation_type"), - NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, - ngx_conf_set_str_slot, - NGX_HTTP_LOC_CONF_OFFSET, - offsetof(ngx_http_auth_jwt_loc_conf_t, auth_jwt_validation_type), - NULL }, - - { ngx_string("auth_jwt_algorithm"), - NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, - ngx_conf_set_str_slot, - NGX_HTTP_LOC_CONF_OFFSET, - offsetof(ngx_http_auth_jwt_loc_conf_t, auth_jwt_algorithm), - NULL }, - - { ngx_string("auth_jwt_validate_email"), - NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG, - ngx_conf_set_flag_slot, - NGX_HTTP_LOC_CONF_OFFSET, - offsetof(ngx_http_auth_jwt_loc_conf_t, auth_jwt_validate_email), - NULL }, - - ngx_null_command +#include +#include + +typedef struct +{ + ngx_str_t loginurl; + ngx_str_t key; + ngx_flag_t enabled; + ngx_flag_t redirect; + ngx_str_t jwt_location; + ngx_str_t algorithm; + ngx_flag_t validate_sub; + ngx_array_t *extract_var_claims; + ngx_array_t *extract_request_claims; + ngx_array_t *extract_response_claims; + ngx_str_t keyfile_path; + ngx_flag_t use_keyfile; + ngx_str_t _keyfile; +} auth_jwt_conf_t; + +typedef struct +{ + ngx_int_t validation_status; + ngx_array_t *claim_values; +} auth_jwt_ctx_t; + +static ngx_int_t init(ngx_conf_t *cf); +static void *create_conf(ngx_conf_t *cf); +static char *merge_conf(ngx_conf_t *cf, void *parent, void *child); +static char *merge_extract_var_claims(ngx_conf_t *cf, ngx_command_t *cmd, void *c); +static ngx_int_t get_jwt_var_claim(ngx_http_request_t *r, ngx_http_variable_value_t *v, uintptr_t data); +static char *merge_extract_request_claims(ngx_conf_t *cf, ngx_command_t *cmd, void *c); +static char *merge_extract_response_claims(ngx_conf_t *cf, ngx_command_t *cmd, void *c); +static auth_jwt_ctx_t *get_or_init_jwt_module_ctx(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf); +static auth_jwt_ctx_t *get_request_jwt_ctx(ngx_http_request_t *r); +static ngx_int_t handle_request(ngx_http_request_t *r); +static int validate_alg(auth_jwt_conf_t *jwtcf, jwt_t *jwt); +static int validate_exp(auth_jwt_conf_t *jwtcf, jwt_t *jwt); +static int validate_sub(auth_jwt_conf_t *jwtcf, jwt_t *jwt); +static ngx_int_t extract_var_claims(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf, jwt_t *jwt, auth_jwt_ctx_t *ctx); +static void extract_request_claims(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf, jwt_t *jwt); +static void extract_response_claims(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf, jwt_t *jwt); +static ngx_int_t redirect(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf); +static ngx_int_t load_public_key(ngx_conf_t *cf, auth_jwt_conf_t *conf); +static char *get_jwt(ngx_http_request_t *r, ngx_str_t jwt_location); + +static const char *JWT_HEADER_PREFIX = "JWT-"; + +static ngx_command_t auth_jwt_directives[] = { + {ngx_string("auth_jwt_loginurl"), + NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, + ngx_conf_set_str_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(auth_jwt_conf_t, loginurl), + NULL}, + + {ngx_string("auth_jwt_key"), + NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, + ngx_conf_set_str_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(auth_jwt_conf_t, key), + NULL}, + + {ngx_string("auth_jwt_enabled"), + NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_FLAG, + ngx_conf_set_flag_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(auth_jwt_conf_t, enabled), + NULL}, + + {ngx_string("auth_jwt_redirect"), + NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_FLAG, + ngx_conf_set_flag_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(auth_jwt_conf_t, redirect), + NULL}, + + {ngx_string("auth_jwt_location"), + NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, + ngx_conf_set_str_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(auth_jwt_conf_t, jwt_location), + NULL}, + + {ngx_string("auth_jwt_algorithm"), + NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, + ngx_conf_set_str_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(auth_jwt_conf_t, algorithm), + NULL}, + + {ngx_string("auth_jwt_validate_sub"), + NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_FLAG, + ngx_conf_set_flag_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(auth_jwt_conf_t, validate_sub), + NULL}, + + {ngx_string("auth_jwt_extract_var_claims"), + NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_1MORE, + merge_extract_var_claims, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(auth_jwt_conf_t, extract_var_claims), + NULL}, + + {ngx_string("auth_jwt_extract_request_claims"), + NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_1MORE, + merge_extract_request_claims, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(auth_jwt_conf_t, extract_request_claims), + NULL}, + + {ngx_string("auth_jwt_extract_response_claims"), + NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_1MORE, + merge_extract_response_claims, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(auth_jwt_conf_t, extract_response_claims), + NULL}, + + {ngx_string("auth_jwt_keyfile_path"), + NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, + ngx_conf_set_str_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(auth_jwt_conf_t, keyfile_path), + NULL}, + + {ngx_string("auth_jwt_use_keyfile"), + NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_FLAG, + ngx_conf_set_flag_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(auth_jwt_conf_t, use_keyfile), + NULL}, + + ngx_null_command}; + +static ngx_http_module_t auth_jwt_context = { + NULL, /* preconfiguration */ + init, /* postconfiguration */ + NULL, /* create main configuration */ + NULL, /* init main configuration */ + NULL, /* create server configuration */ + NULL, /* merge server configuration */ + create_conf, /* create location configuration */ + merge_conf /* merge location configuration */ }; +ngx_module_t ngx_http_auth_jwt_module = { + NGX_MODULE_V1, + &auth_jwt_context, /* module context */ + auth_jwt_directives, /* module directives */ + NGX_HTTP_MODULE, /* module type */ + NULL, /* init master */ + NULL, /* init module */ + NULL, /* init process */ + NULL, /* init thread */ + NULL, /* exit thread */ + NULL, /* exit process */ + NULL, /* exit master */ + NGX_MODULE_V1_PADDING}; + +static ngx_int_t init(ngx_conf_t *cf) +{ + ngx_http_core_main_conf_t *cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module); + ngx_http_handler_pt *h = ngx_array_push(&cmcf->phases[NGX_HTTP_ACCESS_PHASE].handlers); + + if (h == NULL) + { + return NGX_ERROR; + } + else + { + *h = handle_request; + + return NGX_OK; + } +} -static ngx_http_module_t ngx_http_auth_jwt_module_ctx = { - NULL, /* preconfiguration */ - ngx_http_auth_jwt_init, /* postconfiguration */ +static void *create_conf(ngx_conf_t *cf) +{ + auth_jwt_conf_t *conf = ngx_pcalloc(cf->pool, sizeof(auth_jwt_conf_t)); + + if (conf == NULL) + { + return NULL; + } + else + { + // ngx_str_t fields are initialized by the ngx_palloc call above -- only need to init flags and arrays here + conf->enabled = NGX_CONF_UNSET; + conf->redirect = NGX_CONF_UNSET; + conf->validate_sub = NGX_CONF_UNSET; + conf->redirect = NGX_CONF_UNSET; + conf->validate_sub = NGX_CONF_UNSET; + conf->extract_var_claims = NULL; + conf->extract_request_claims = NULL; + conf->extract_response_claims = NULL; + conf->use_keyfile = NGX_CONF_UNSET; + + return conf; + } +} - NULL, /* create main configuration */ - NULL, /* init main configuration */ +static char *merge_conf(ngx_conf_t *cf, void *parent, void *child) +{ + const auth_jwt_conf_t *prev = parent; + auth_jwt_conf_t *conf = child; + + ngx_conf_merge_str_value(conf->loginurl, prev->loginurl, ""); + ngx_conf_merge_str_value(conf->key, prev->key, ""); + ngx_conf_merge_str_value(conf->jwt_location, prev->jwt_location, "HEADER=Authorization"); + ngx_conf_merge_str_value(conf->algorithm, prev->algorithm, "HS256"); + ngx_conf_merge_str_value(conf->keyfile_path, prev->keyfile_path, ""); + ngx_conf_merge_off_value(conf->validate_sub, prev->validate_sub, 0); + merge_array(cf->pool, &conf->extract_var_claims, prev->extract_var_claims, sizeof(ngx_str_t)); + merge_array(cf->pool, &conf->extract_request_claims, prev->extract_request_claims, sizeof(ngx_str_t)); + merge_array(cf->pool, &conf->extract_response_claims, prev->extract_response_claims, sizeof(ngx_str_t)); + + if (conf->enabled == NGX_CONF_UNSET) + { + conf->enabled = prev->enabled == NGX_CONF_UNSET ? 0 : prev->enabled; + } + + if (conf->redirect == NGX_CONF_UNSET) + { + conf->redirect = prev->redirect == NGX_CONF_UNSET ? 0 : prev->redirect; + } + + if (conf->use_keyfile == NGX_CONF_UNSET) + { + conf->use_keyfile = prev->use_keyfile == NGX_CONF_UNSET ? 0 : prev->use_keyfile; + } + + // If the usage of the keyfile is specified, check if the key_path is also configured + if (conf->use_keyfile == 1) + { + if (ngx_strcmp(conf->keyfile_path.data, "") != 0) + { + if (load_public_key(cf, conf) != NGX_OK) + { + return NGX_CONF_ERROR; + } + } + else + { + ngx_log_error(NGX_LOG_ERR, cf->log, 0, "keyfile_path not specified"); + + return NGX_CONF_ERROR; + } + } + + return NGX_CONF_OK; +} - NULL, /* create server configuration */ - NULL, /* merge server configuration */ +static char *merge_extract_var_claims(ngx_conf_t *cf, ngx_command_t *cmd, void *c) +{ + auth_jwt_conf_t *conf = c; + ngx_array_t *claims = conf->extract_var_claims; + + if (claims == NULL) + { + claims = ngx_array_create(cf->pool, 1, sizeof(ngx_str_t)); + conf->extract_var_claims = claims; + } + + ngx_str_t *values = cf->args->elts; + + // start at 1 because the first element is the directive (auth_jwt_extract_var_claims) + for (ngx_uint_t i = 1; i < cf->args->nelts; ++i) + { + // add this claim's name to the config struct + ngx_str_t *element = ngx_array_push(claims); + + *element = values[i]; + + // add an http variable for this claim + size_t var_name_len = 10 + element->len; + u_char *buf = ngx_palloc(cf->pool, sizeof(u_char) * var_name_len); + + if (buf == NULL) + { + return NGX_CONF_ERROR; + } + else + { + ngx_sprintf(buf, "jwt_claim_%V", element); + ngx_str_t *var_name = ngx_palloc(cf->pool, sizeof(ngx_str_t)); + + if (var_name == NULL) + { + return NGX_CONF_ERROR; + } + else + { + var_name->data = buf; + var_name->len = var_name_len; + + // NGX_HTTP_VAR_CHANGEABLE simplifies the required logic by assuming a JWT claim will always be the same for a given request + ngx_http_variable_t *http_var = ngx_http_add_variable(cf, var_name, NGX_HTTP_VAR_CHANGEABLE); + + if (http_var == NULL) + { + ngx_log_error(NGX_LOG_ERR, cf->log, 0, "failed to add variable %V", var_name); + + return NGX_CONF_ERROR; + } + else + { + http_var->get_handler = get_jwt_var_claim; + + // store the index of this new claim in the claims array as the "data" that will be passed to the getter + ngx_uint_t *claim_idx = ngx_palloc(cf->pool, sizeof(ngx_uint_t)); + + if (claim_idx == NULL) + { + return NGX_CONF_ERROR; + } + else + { + *claim_idx = claims->nelts - 1; + http_var->data = (uintptr_t)claim_idx; + } + } + } + } + } + + return NGX_CONF_OK; +} - ngx_http_auth_jwt_create_loc_conf, /* create location configuration */ - ngx_http_auth_jwt_merge_loc_conf /* merge location configuration */ -}; +static ngx_int_t get_jwt_var_claim(ngx_http_request_t *r, ngx_http_variable_value_t *v, uintptr_t data) +{ + ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "getting jwt value for var index %l", *((ngx_uint_t *)data)); + auth_jwt_ctx_t *ctx = get_request_jwt_ctx(r); + + if (ctx == NULL) + { + ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "no module context found while getting jwt value"); + + return NGX_ERROR; + } + else + { + ngx_uint_t *claim_idx = (ngx_uint_t *)data; + ngx_str_t claim_value = ((ngx_str_t *)ctx->claim_values->elts)[*claim_idx]; + + v->valid = 1; + v->no_cacheable = 0; + v->not_found = 0; + v->len = claim_value.len; + v->data = claim_value.data; + + return NGX_OK; + } +} + +static char *merge_extract_claims(ngx_conf_t *cf, ngx_array_t *claims) +{ + ngx_str_t *values = cf->args->elts; + // start at 1 because the first element is the directive (auth_jwt_extract_X_claims) + for (ngx_uint_t i = 1; i < cf->args->nelts; ++i) + { + ngx_str_t *element = ngx_array_push(claims); -ngx_module_t ngx_http_auth_jwt_module = { - NGX_MODULE_V1, - &ngx_http_auth_jwt_module_ctx, /* module context */ - ngx_http_auth_jwt_commands, /* module directives */ - NGX_HTTP_MODULE, /* module type */ - NULL, /* init master */ - NULL, /* init module */ - NULL, /* init process */ - NULL, /* init thread */ - NULL, /* exit thread */ - NULL, /* exit process */ - NULL, /* exit master */ - NGX_MODULE_V1_PADDING -}; + *element = values[i]; + } + return NGX_CONF_OK; +} -static ngx_int_t ngx_http_auth_jwt_handler(ngx_http_request_t *r) +static char *merge_extract_request_claims(ngx_conf_t *cf, ngx_command_t *cmd, void *c) { - ngx_str_t useridHeaderName = ngx_string("x-userid"); - ngx_str_t emailHeaderName = ngx_string("x-email"); - char* jwtCookieValChrPtr; - char* return_url; - ngx_http_auth_jwt_loc_conf_t *jwtcf; - u_char *keyBinary; - jwt_t *jwt = NULL; - int jwtParseReturnCode; - jwt_alg_t alg; - const char* sub; - const char* email; - ngx_str_t sub_t; - ngx_str_t email_t; - time_t exp; - time_t now; - ngx_str_t auth_jwt_algorithm; - int keylen; - - jwtcf = ngx_http_get_module_loc_conf(r, ngx_http_auth_jwt_module); - - if (!jwtcf->auth_jwt_enabled) - { - return NGX_DECLINED; - } - - // pass through options requests without token authentication - if (r->method == NGX_HTTP_OPTIONS) - { - return NGX_DECLINED; - } - - jwtCookieValChrPtr = getJwt(r, jwtcf->auth_jwt_validation_type); - if (jwtCookieValChrPtr == NULL) - { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to find a jwt"); - goto redirect; - } - - // convert key from hex to binary, if a symmetric key - - auth_jwt_algorithm = jwtcf->auth_jwt_algorithm; - if (auth_jwt_algorithm.len == 0 || (auth_jwt_algorithm.len == sizeof("HS256") - 1 && ngx_strncmp(auth_jwt_algorithm.data, "HS256", sizeof("HS256") - 1)==0)) - { - keylen = jwtcf->auth_jwt_key.len / 2; - keyBinary = ngx_palloc(r->pool, keylen); - if (0 != hex_to_binary((char *)jwtcf->auth_jwt_key.data, keyBinary, jwtcf->auth_jwt_key.len)) - { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to turn hex key into binary"); - goto redirect; - } - } - else if ( auth_jwt_algorithm.len == sizeof("RS256") - 1 && ngx_strncmp(auth_jwt_algorithm.data, "RS256", sizeof("RS256") - 1) == 0 ) - { - // in this case, 'Binary' is a misnomer, as it is the public key string itself - keyBinary = jwtcf->auth_jwt_key.data; - keylen = jwtcf->auth_jwt_key.len; - } - else - { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "unsupported algorithm"); - goto redirect; - } - - // validate the jwt - jwtParseReturnCode = jwt_decode(&jwt, jwtCookieValChrPtr, keyBinary, keylen); - if (jwtParseReturnCode != 0) - { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to parse jwt"); - goto redirect; - } - - // validate the algorithm - alg = jwt_get_alg(jwt); - if (alg != JWT_ALG_HS256 && alg != JWT_ALG_RS256) - { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "invalid algorithm in jwt %d", alg); - goto redirect; - } - - // validate the exp date of the JWT - exp = (time_t)jwt_get_grant_int(jwt, "exp"); - now = time(NULL); - if (exp < now) - { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the jwt has expired"); - goto redirect; - } - - // extract the userid - sub = jwt_get_grant(jwt, "sub"); - if (sub == NULL) - { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the jwt does not contain a subject"); - } - else - { - sub_t = ngx_char_ptr_to_str_t(r->pool, (char *)sub); - set_custom_header_in_headers_out(r, &useridHeaderName, &sub_t); - } - - if (jwtcf->auth_jwt_validate_email == 1) - { - email = jwt_get_grant(jwt, "emailAddress"); - if (email == NULL) - { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the jwt does not contain an email address"); - } - else - { - email_t = ngx_char_ptr_to_str_t(r->pool, (char *)email); - set_custom_header_in_headers_out(r, &emailHeaderName, &email_t); - } - } - - jwt_free(jwt); - - return NGX_OK; - - redirect: - - if (jwt) - { - jwt_free(jwt); - } - - r->headers_out.location = ngx_list_push(&r->headers_out.headers); - - if (r->headers_out.location == NULL) - { - ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); - } - - r->headers_out.location->hash = 1; - r->headers_out.location->key.len = sizeof("Location") - 1; - r->headers_out.location->key.data = (u_char *) "Location"; - - if (r->method == NGX_HTTP_GET) - { - int loginlen; - char * scheme; - ngx_str_t server; - ngx_str_t uri_variable_name = ngx_string("request_uri"); - ngx_int_t uri_variable_hash; - ngx_http_variable_value_t * request_uri_var; - ngx_str_t uri; - ngx_str_t uri_escaped; - uintptr_t escaped_len; - - loginlen = jwtcf->auth_jwt_loginurl.len; - - scheme = (r->connection->ssl) ? "https" : "http"; - server = r->headers_in.server; - - // get the URI - uri_variable_hash = ngx_hash_key(uri_variable_name.data, uri_variable_name.len); - request_uri_var = ngx_http_get_variable(r, &uri_variable_name, uri_variable_hash); - - // get the URI - if(request_uri_var && !request_uri_var->not_found && request_uri_var->valid) - { - // ideally we would like the uri with the querystring parameters - uri.data = ngx_palloc(r->pool, request_uri_var->len); - uri.len = request_uri_var->len; - ngx_memcpy(uri.data, request_uri_var->data, request_uri_var->len); - - // ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "found uri with querystring %s", ngx_str_t_to_char_ptr(r->pool, uri)); - } - else - { - // fallback to the querystring without params - uri = r->uri; - - // ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "fallback to querystring without params"); - } - - // escape the URI - escaped_len = 2 * ngx_escape_uri(NULL, uri.data, uri.len, NGX_ESCAPE_ARGS) + uri.len; - uri_escaped.data = ngx_palloc(r->pool, escaped_len); - uri_escaped.len = escaped_len; - ngx_escape_uri(uri_escaped.data, uri.data, uri.len, NGX_ESCAPE_ARGS); - - r->headers_out.location->value.len = loginlen + sizeof("?return_url=") - 1 + strlen(scheme) + sizeof("://") - 1 + server.len + uri_escaped.len; - return_url = ngx_palloc(r->pool, r->headers_out.location->value.len); - ngx_memcpy(return_url, jwtcf->auth_jwt_loginurl.data, jwtcf->auth_jwt_loginurl.len); - int return_url_idx = jwtcf->auth_jwt_loginurl.len; - ngx_memcpy(return_url+return_url_idx, "?return_url=", sizeof("?return_url=") - 1); - return_url_idx += sizeof("?return_url=") - 1; - ngx_memcpy(return_url+return_url_idx, scheme, strlen(scheme)); - return_url_idx += strlen(scheme); - ngx_memcpy(return_url+return_url_idx, "://", sizeof("://") - 1); - return_url_idx += sizeof("://") - 1; - ngx_memcpy(return_url+return_url_idx, server.data, server.len); - return_url_idx += server.len; - ngx_memcpy(return_url+return_url_idx, uri_escaped.data, uri_escaped.len); - return_url_idx += uri_escaped.len; - r->headers_out.location->value.data = (u_char *)return_url; - - // ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "return_url: %s", ngx_str_t_to_char_ptr(r->pool, r->headers_out.location->value)); - } - else - { - // for non-get requests, redirect to the login page without a return URL - r->headers_out.location->value.len = jwtcf->auth_jwt_loginurl.len; - r->headers_out.location->value.data = jwtcf->auth_jwt_loginurl.data; - } - - if (jwtcf->auth_jwt_redirect) - { - return NGX_HTTP_MOVED_TEMPORARILY; - } - else - { - return NGX_HTTP_UNAUTHORIZED; - } + auth_jwt_conf_t *conf = c; + ngx_array_t *claims = conf->extract_request_claims; + + if (claims == NULL) + { + claims = ngx_array_create(cf->pool, 1, sizeof(ngx_str_t)); + conf->extract_request_claims = claims; + } + + return merge_extract_claims(cf, claims); } +static char *merge_extract_response_claims(ngx_conf_t *cf, ngx_command_t *cmd, void *c) +{ + auth_jwt_conf_t *conf = c; + ngx_array_t *claims = conf->extract_response_claims; + + if (claims == NULL) + { + claims = ngx_array_create(cf->pool, 1, sizeof(ngx_str_t)); + conf->extract_response_claims = claims; + } -static ngx_int_t ngx_http_auth_jwt_init(ngx_conf_t *cf) + return merge_extract_claims(cf, claims); +} + +static auth_jwt_ctx_t *get_or_init_jwt_module_ctx(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf) +{ + auth_jwt_ctx_t *ctx = ngx_http_get_module_ctx(r, ngx_http_auth_jwt_module); + + if (ctx != NULL) + { + return ctx; + } + else + { + ctx = ngx_pcalloc(r->pool, sizeof(auth_jwt_ctx_t)); + + if (ctx == NULL) + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "error allocating jwt module context"); + return ctx; + } + else + { + if (jwtcf->extract_var_claims != NULL) + { + ctx->claim_values = ngx_array_create(r->pool, jwtcf->extract_var_claims->nelts, sizeof(ngx_str_t)); + + if (ctx->claim_values == NULL) + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "error initializing jwt module context"); + return NULL; + } + } + + ctx->validation_status = NGX_AGAIN; + ngx_http_set_ctx(r, ctx, ngx_http_auth_jwt_module); + + return ctx; + } + } +} + +// this creates the module's context struct and extracts claim vars the first time it is called, +// either from the access-phase handler or an http var getter +static auth_jwt_ctx_t *get_request_jwt_ctx(ngx_http_request_t *r) { - ngx_http_handler_pt *h; - ngx_http_core_main_conf_t *cmcf; + auth_jwt_conf_t *jwtcf = ngx_http_get_module_loc_conf(r, ngx_http_auth_jwt_module); + + if (!jwtcf->enabled) + { + return NULL; + } + + auth_jwt_ctx_t *ctx = get_or_init_jwt_module_ctx(r, jwtcf); + + if (ctx == NULL) + { + return NULL; + } + else if (ctx->validation_status != NGX_AGAIN) + { + // we already validated and extacted everything we care about, so we just return the already-complete context + return ctx; + } + + char *jwtPtr = get_jwt(r, jwtcf->jwt_location); + + if (jwtPtr == NULL) + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to find a JWT"); + ctx->validation_status = NGX_ERROR; + return ctx; + } + else + { + ngx_str_t algorithm = jwtcf->algorithm; + int keyLength; + u_char *key; + jwt_t *jwt = NULL; + + if (algorithm.len == 0 || (algorithm.len == 5 && ngx_strncmp(algorithm.data, "HS", 2) == 0)) + { + keyLength = jwtcf->key.len / 2; + key = ngx_palloc(r->pool, keyLength); + + if (0 != hex_to_binary((char *)jwtcf->key.data, key, jwtcf->key.len)) + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to turn hex key into binary"); + ctx->validation_status = NGX_ERROR; + return ctx; + } + } + else if (algorithm.len == 5 && (ngx_strncmp(algorithm.data, "RS", 2) == 0 || ngx_strncmp(algorithm.data, "ES", 2) == 0)) + { + if (jwtcf->use_keyfile == 1) + { + keyLength = jwtcf->_keyfile.len; + key = (u_char *)jwtcf->_keyfile.data; + } + else + { + keyLength = jwtcf->key.len; + key = jwtcf->key.data; + } + } + else + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "unsupported algorithm %s", algorithm); + ctx->validation_status = NGX_ERROR; + return ctx; + } + + if (jwt_decode(&jwt, jwtPtr, key, keyLength) != 0 || !jwt) + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to parse JWT"); + ctx->validation_status = NGX_ERROR; + } + else if (validate_alg(jwtcf, jwt) != 0) + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "invalid algorithm specified"); + ctx->validation_status = NGX_ERROR; + } + else if (validate_exp(jwtcf, jwt) != 0) + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the JWT has expired"); + ctx->validation_status = NGX_ERROR; + } + else if (validate_sub(jwtcf, jwt) != 0) + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the JWT does not contain a subject"); + ctx->validation_status = NGX_ERROR; + } + else + { + extract_request_claims(r, jwtcf, jwt); + extract_response_claims(r, jwtcf, jwt); + ctx->validation_status = extract_var_claims(r, jwtcf, jwt, ctx); + } + + jwt_free(jwt); + return ctx; + } +} - cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module); +static ngx_int_t handle_request(ngx_http_request_t *r) +{ + auth_jwt_conf_t *jwtcf = ngx_http_get_module_loc_conf(r, ngx_http_auth_jwt_module); + auth_jwt_ctx_t *ctx = get_request_jwt_ctx(r); + + if (!jwtcf->enabled) + { + return NGX_DECLINED; + } + else if (r->method == NGX_HTTP_OPTIONS) // pass through options requests without token authentication + { + return NGX_DECLINED; + } + else if (!ctx) + { + return NGX_ERROR; + } + else if (ctx->validation_status == NGX_ERROR) + { + return redirect(r, jwtcf); + } + else + { + return ctx->validation_status; + } +} - h = ngx_array_push(&cmcf->phases[NGX_HTTP_ACCESS_PHASE].handlers); - if (h == NULL) - { - return NGX_ERROR; - } +static int validate_alg(auth_jwt_conf_t *jwtcf, jwt_t *jwt) +{ + const jwt_alg_t alg = jwt_get_alg(jwt); - *h = ngx_http_auth_jwt_handler; + if (alg != JWT_ALG_HS256 && alg != JWT_ALG_HS384 && alg != JWT_ALG_HS512 && alg != JWT_ALG_RS256 && alg != JWT_ALG_RS384 && alg != JWT_ALG_RS512 && alg != JWT_ALG_ES256 && alg != JWT_ALG_ES384 && alg != JWT_ALG_ES512) + { + return 1; + } - return NGX_OK; + return 0; } +static int validate_exp(auth_jwt_conf_t *jwtcf, jwt_t *jwt) +{ + const time_t exp = (time_t)jwt_get_grant_int(jwt, "exp"); + const time_t now = time(NULL); + + if (exp < now) + { + return 1; + } + + return 0; +} -static void * -ngx_http_auth_jwt_create_loc_conf(ngx_conf_t *cf) +static int validate_sub(auth_jwt_conf_t *jwtcf, jwt_t *jwt) { - ngx_http_auth_jwt_loc_conf_t *conf; - - conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_auth_jwt_loc_conf_t)); - if (conf == NULL) - { - return NULL; - } - - // set the flag to unset - conf->auth_jwt_enabled = (ngx_flag_t) -1; - conf->auth_jwt_redirect = (ngx_flag_t) -1; - conf->auth_jwt_validate_email = (ngx_flag_t) -1; - - ngx_conf_log_error(NGX_LOG_DEBUG, cf, 0, "Created Location Configuration"); - - return conf; + if (jwtcf->validate_sub == 1) + { + const char *sub = jwt_get_grant(jwt, "sub"); + + if (sub == NULL) + { + return 1; + } + } + + return 0; } +static ngx_int_t extract_var_claims(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf, jwt_t *jwt, auth_jwt_ctx_t *ctx) +{ + ngx_array_t *claims = jwtcf->extract_var_claims; + + if (claims == NULL || claims->nelts == 0) + { + return NGX_OK; + } + else + { + const ngx_str_t *claimsPtr = claims->elts; + + for (uint i = 0; i < claims->nelts; ++i) + { + const ngx_str_t claim = claimsPtr[i]; + const char *claimValue = jwt_get_grant(jwt, (char *)claim.data); + ngx_str_t value = ngx_string(""); + + if (claimValue != NULL && strlen(claimValue) > 0) + { + value = char_ptr_to_ngx_str_t(r->pool, claimValue); + } + + ((ngx_str_t *)ctx->claim_values->elts)[i] = value; + ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "set var %V to JWT claim value %s", &claim, value.data); + } + + return NGX_OK; + } +} -static char * -ngx_http_auth_jwt_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) +static void extract_claims(ngx_http_request_t *r, jwt_t *jwt, ngx_array_t *claims, ngx_int_t (*set_header)(ngx_http_request_t *r, ngx_str_t *key, ngx_str_t *value)) { - ngx_http_auth_jwt_loc_conf_t *prev = parent; - ngx_http_auth_jwt_loc_conf_t *conf = child; - - ngx_conf_merge_str_value(conf->auth_jwt_loginurl, prev->auth_jwt_loginurl, ""); - ngx_conf_merge_str_value(conf->auth_jwt_key, prev->auth_jwt_key, ""); - ngx_conf_merge_str_value(conf->auth_jwt_validation_type, prev->auth_jwt_validation_type, ""); - ngx_conf_merge_str_value(conf->auth_jwt_algorithm, prev->auth_jwt_algorithm, "HS256"); - ngx_conf_merge_off_value(conf->auth_jwt_validate_email, prev->auth_jwt_validate_email, 1); - - if (conf->auth_jwt_enabled == ((ngx_flag_t) -1)) - { - conf->auth_jwt_enabled = (prev->auth_jwt_enabled == ((ngx_flag_t) -1)) ? 0 : prev->auth_jwt_enabled; - } - - if (conf->auth_jwt_redirect == ((ngx_flag_t) -1)) - { - conf->auth_jwt_redirect = (prev->auth_jwt_redirect == ((ngx_flag_t) -1)) ? 0 : prev->auth_jwt_redirect; - } - - return NGX_CONF_OK; + if (claims != NULL && claims->nelts > 0) + { + const ngx_str_t *claimsPtr = claims->elts; + + for (uint i = 0; i < claims->nelts; ++i) + { + const ngx_str_t claim = claimsPtr[i]; + const char *value = jwt_get_grant(jwt, (char *)claim.data); + + if (value != NULL && strlen(value) > 0) + { + ngx_uint_t claimHeaderLen = strlen(JWT_HEADER_PREFIX) + claim.len; + ngx_str_t claimHeader = ngx_null_string; + ngx_str_t claimValue = char_ptr_to_ngx_str_t(r->pool, value); + + claimHeader.data = ngx_palloc(r->pool, claimHeaderLen); + claimHeader.len = claimHeaderLen; + ngx_snprintf(claimHeader.data, claimHeaderLen, "%s%V", JWT_HEADER_PREFIX, &claim); + + set_header(r, &claimHeader, &claimValue); + } + } + } } -static char * getJwt(ngx_http_request_t *r, ngx_str_t auth_jwt_validation_type) +static void extract_request_claims(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf, jwt_t *jwt) { - static const ngx_str_t authorizationHeaderName = ngx_string("Authorization"); - ngx_table_elt_t *authorizationHeader; - char* jwtCookieValChrPtr = NULL; - ngx_str_t jwtCookieVal; - ngx_int_t n; - ngx_str_t authorizationHeaderStr; - - ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, "auth_jwt_validation_type.len %d", auth_jwt_validation_type.len); - - if (auth_jwt_validation_type.len == 0 || (auth_jwt_validation_type.len == sizeof("AUTHORIZATION") - 1 && ngx_strncmp(auth_jwt_validation_type.data, "AUTHORIZATION", sizeof("AUTHORIZATION") - 1)==0)) - { - // using authorization header - authorizationHeader = search_headers_in(r, authorizationHeaderName.data, authorizationHeaderName.len); - if (authorizationHeader != NULL) - { - ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, "Found authorization header len %d", authorizationHeader->value.len); - - authorizationHeaderStr.data = authorizationHeader->value.data + sizeof("Bearer ") - 1; - authorizationHeaderStr.len = authorizationHeader->value.len - (sizeof("Bearer ") - 1); - - jwtCookieValChrPtr = ngx_str_t_to_char_ptr(r->pool, authorizationHeaderStr); - - ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, "Authorization header: %s", jwtCookieValChrPtr); - } - } - else if (auth_jwt_validation_type.len > sizeof("COOKIE=") && ngx_strncmp(auth_jwt_validation_type.data, "COOKIE=", sizeof("COOKIE=") - 1)==0) - { - auth_jwt_validation_type.data += sizeof("COOKIE=") - 1; - auth_jwt_validation_type.len -= sizeof("COOKIE=") - 1; - - // get the cookie - // TODO: the cookie name could be passed in dynamicallly - n = ngx_http_parse_multi_header_lines(&r->headers_in.cookies, &auth_jwt_validation_type, &jwtCookieVal); - if (n != NGX_DECLINED) - { - jwtCookieValChrPtr = ngx_str_t_to_char_ptr(r->pool, jwtCookieVal); - } - } - - return jwtCookieValChrPtr; + extract_claims(r, jwt, jwtcf->extract_request_claims, set_request_header); } +static void extract_response_claims(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf, jwt_t *jwt) +{ + extract_claims(r, jwt, jwtcf->extract_response_claims, set_response_header); +} +static ngx_int_t redirect(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf) +{ + if (jwtcf->redirect) + { + r->headers_out.location = ngx_list_push(&r->headers_out.headers); + + if (r->headers_out.location == NULL) + { + ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); + } + + r->headers_out.location->hash = 1; + r->headers_out.location->key.len = strlen("Location"); + r->headers_out.location->key.data = (u_char *)"Location"; + + if (r->method == NGX_HTTP_GET) + { + const int loginlen = jwtcf->loginurl.len; + const char *scheme = r->connection->ssl ? "https" : "http"; + ngx_str_t port_variable_name = ngx_string("server_port"); + ngx_int_t port_variable_hash = ngx_hash_key(port_variable_name.data, port_variable_name.len); + ngx_http_variable_value_t *port_var = ngx_http_get_variable(r, &port_variable_name, port_variable_hash); + char *port_str = ""; + uint port_str_len = 0; + const ngx_str_t server = r->headers_in.server; + ngx_str_t uri_variable_name = ngx_string("request_uri"); + ngx_int_t uri_variable_hash = ngx_hash_key(uri_variable_name.data, uri_variable_name.len); + ngx_http_variable_value_t *uri_var = ngx_http_get_variable(r, &uri_variable_name, uri_variable_hash); + ngx_str_t uri; + ngx_str_t uri_escaped; + uintptr_t escaped_len; + char *return_url; + int return_url_idx; + + // get the URI + if (uri_var && !uri_var->not_found && uri_var->valid) + { + // ideally we would like the URI with the querystring parameters + uri.data = ngx_palloc(r->pool, uri_var->len); + uri.len = uri_var->len; + ngx_memcpy(uri.data, uri_var->data, uri_var->len); + } + else + { + // fallback to the querystring without params + uri = r->uri; + } + + if (port_var && !port_var->not_found && port_var->valid) + { + const ngx_uint_t port_num = ngx_atoi(port_var->data, port_var->len); + const bool is_default_port_80 = !r->connection->ssl && port_num == 80; + const bool is_default_port_443 = r->connection->ssl && port_num == 443; + const bool is_non_default_port = !is_default_port_80 && !is_default_port_443; + + if (is_non_default_port) + { + port_str = ngx_palloc(r->pool, NGX_INT_T_LEN + 2); + + ngx_snprintf((u_char *)port_str, sizeof(port_str), ":%d", port_num); + port_str_len = strlen(port_str); + } + } + + + // escape the URI + escaped_len = 2 * ngx_escape_uri(NULL, uri.data, uri.len, NGX_ESCAPE_ARGS) + uri.len; + uri_escaped.data = ngx_palloc(r->pool, escaped_len); + uri_escaped.len = escaped_len; + ngx_escape_uri(uri_escaped.data, uri.data, uri.len, NGX_ESCAPE_ARGS); + + // Add up the lengths of: login URL, "?return_url=", scheme, "://", server, port, uri (path) + r->headers_out.location->value.len = loginlen + 12 + strlen(scheme) + 3 + server.len + port_str_len + uri_escaped.len; + + return_url = ngx_palloc(r->pool, r->headers_out.location->value.len); + + ngx_memcpy(return_url, jwtcf->loginurl.data, jwtcf->loginurl.len); + return_url_idx = jwtcf->loginurl.len; + + ngx_memcpy(return_url + return_url_idx, "?return_url=", 12); + return_url_idx += 12; + + ngx_memcpy(return_url + return_url_idx, scheme, strlen(scheme)); + return_url_idx += strlen(scheme); + + ngx_memcpy(return_url + return_url_idx, "://", 3); + return_url_idx += 3; + + ngx_memcpy(return_url + return_url_idx, server.data, server.len); + return_url_idx += server.len; + + if (port_str_len > 0) + { + ngx_memcpy(return_url + return_url_idx, port_str, port_str_len); + return_url_idx += port_str_len; + } + + if (uri_escaped.len > 0) + { + ngx_memcpy(return_url + return_url_idx, uri_escaped.data, uri_escaped.len); + } + + r->headers_out.location->value.data = (u_char *)return_url; + } + else + { + // for non-get requests, redirect to the login page without a return URL + r->headers_out.location->value.len = jwtcf->loginurl.len; + r->headers_out.location->value.data = jwtcf->loginurl.data; + } + + return NGX_HTTP_MOVED_TEMPORARILY; + } + + // When no redirect is needed, no "Location" header construction is needed, and we can respond with a 401 + return NGX_HTTP_UNAUTHORIZED; +} +// Loads the public key into the location config struct +static ngx_int_t load_public_key(ngx_conf_t *cf, auth_jwt_conf_t *conf) +{ + FILE *keyFile = fopen((const char *)conf->keyfile_path.data, "rb"); + + // Check if file exists or is correctly opened + if (keyFile == NULL) + { + ngx_log_error(NGX_LOG_ERR, cf->log, 0, "failed to open public key file"); + return NGX_ERROR; + } + else + { + u_long keySize; + u_long keySizeRead; + + // Read file length + fseek(keyFile, 0, SEEK_END); + keySize = ftell(keyFile); + fseek(keyFile, 0, SEEK_SET); + + if (keySize == 0) + { + ngx_log_error(NGX_LOG_ERR, cf->log, 0, "invalid public key file size of 0"); + return NGX_ERROR; + } + else + { + conf->_keyfile.data = ngx_palloc(cf->pool, keySize); + keySizeRead = fread(conf->_keyfile.data, 1, keySize, keyFile); + fclose(keyFile); + + if (keySizeRead == keySize) + { + conf->_keyfile.len = (int)keySize; + + return NGX_OK; + } + else + { + ngx_log_error(NGX_LOG_ERR, cf->log, 0, "public key size %i does not match expected size of %i", keySizeRead, keySize); + return NGX_ERROR; + } + } + } +} +static char *get_jwt(ngx_http_request_t *r, ngx_str_t jwt_location) +{ + static const char *HEADER_PREFIX = "HEADER="; + static const char *COOKIE_PREFIX = "COOKIE="; + char *jwtPtr = NULL; + + ngx_log_debug(NGX_LOG_DEBUG, r->connection->log, 0, "jwt_location.len %d", jwt_location.len); + + if (jwt_location.len > strlen(HEADER_PREFIX) && ngx_strncmp(jwt_location.data, HEADER_PREFIX, strlen(HEADER_PREFIX)) == 0) + { + ngx_table_elt_t *jwtHeaderVal; + + jwt_location.data += strlen(HEADER_PREFIX); + jwt_location.len -= strlen(HEADER_PREFIX); + + jwtHeaderVal = search_headers_in(r, jwt_location.data, jwt_location.len); + + if (jwtHeaderVal != NULL) + { + static const char *BEARER_PREFIX = "Bearer "; + + if (ngx_strncasecmp(jwtHeaderVal->value.data, (u_char *)BEARER_PREFIX, strlen(BEARER_PREFIX)) == 0) + { + ngx_str_t jwtHeaderValWithoutBearer = jwtHeaderVal->value; + + jwtHeaderValWithoutBearer.data += strlen(BEARER_PREFIX); + jwtHeaderValWithoutBearer.len -= strlen(BEARER_PREFIX); + + jwtPtr = ngx_str_t_to_char_ptr(r->pool, jwtHeaderValWithoutBearer); + } + else + { + jwtPtr = ngx_str_t_to_char_ptr(r->pool, jwtHeaderVal->value); + } + } + } + else if (jwt_location.len > strlen(COOKIE_PREFIX) && ngx_strncmp(jwt_location.data, COOKIE_PREFIX, strlen(COOKIE_PREFIX)) == 0) + { + bool has_cookie = false; + ngx_str_t jwtCookieVal; + + jwt_location.data += strlen(COOKIE_PREFIX); + jwt_location.len -= strlen(COOKIE_PREFIX); + +#ifndef NGX_LINKED_LIST_COOKIES + if (ngx_http_parse_multi_header_lines(&r->headers_in.cookies, &jwt_location, &jwtCookieVal) != NGX_DECLINED) + { + has_cookie = true; + } +#else + if (ngx_http_parse_multi_header_lines(r, r->headers_in.cookie, &jwt_location, &jwtCookieVal) != NULL) + { + has_cookie = true; + } +#endif + + if (has_cookie == true) + { + jwtPtr = ngx_str_t_to_char_ptr(r->pool, jwtCookieVal); + } + } + + return jwtPtr; +} diff --git a/src/ngx_http_auth_jwt_string.c b/src/ngx_http_auth_jwt_string.c index 186121f..f472171 100644 --- a/src/ngx_http_auth_jwt_string.c +++ b/src/ngx_http_auth_jwt_string.c @@ -15,18 +15,22 @@ char* ngx_str_t_to_char_ptr(ngx_pool_t *pool, ngx_str_t str) { char* char_ptr = ngx_palloc(pool, str.len + 1); ngx_memcpy(char_ptr, str.data, str.len); + *(char_ptr + str.len) = '\0'; + return char_ptr; } /** copies a character pointer string to an nginx string structure */ -ngx_str_t ngx_char_ptr_to_str_t(ngx_pool_t *pool, char* char_ptr) +ngx_str_t char_ptr_to_ngx_str_t(ngx_pool_t *pool, const char* char_ptr) { - int len = strlen(char_ptr); - + const int len = strlen(char_ptr); ngx_str_t str_t; + + str_t.len = len; str_t.data = ngx_palloc(pool, len); + ngx_memcpy(str_t.data, char_ptr, len); - str_t.len = len; + return str_t; } \ No newline at end of file diff --git a/src/ngx_http_auth_jwt_string.h b/src/ngx_http_auth_jwt_string.h index 594785b..4440d8b 100644 --- a/src/ngx_http_auth_jwt_string.h +++ b/src/ngx_http_auth_jwt_string.h @@ -13,6 +13,6 @@ #include char* ngx_str_t_to_char_ptr(ngx_pool_t *pool, ngx_str_t str); -ngx_str_t ngx_char_ptr_to_str_t(ngx_pool_t *pool, char* char_ptr); +ngx_str_t char_ptr_to_ngx_str_t(ngx_pool_t *pool, const char* char_ptr); #endif /* _NGX_HTTP_AUTH_JWT_STRING_H */ \ No newline at end of file diff --git a/test.sh b/test.sh deleted file mode 100755 index 955bb13..0000000 --- a/test.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env bash - -RED='\033[01;31m' -GREEN='\033[01;32m' -NONE='\033[00m' - -test_jwt () { - local name=$1 - local path=$2 - local expect=$3 - local extra=$4 - - cmd="curl -X GET -o /dev/null --silent --head --write-out '%{http_code}' http://host.docker.internal:8000$path -H 'cache-control: no-cache' $extra" - - test=$( eval ${cmd} ) - if [ "$test" -eq "$expect" ];then - echo -e "${GREEN}${name}: passed (${test})${NONE}"; - else - echo -e "${RED}${name}: failed (${test})${NONE}"; - fi -} - -main() { - local VALIDJWT=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.TvDD63ZOqFKgE-uxPDdP5aGIsbl5xPKz4fMul3Zlti4 - local MISSING_SUB_JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmaXJzdE5hbWUiOiJoZWxsbyIsImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwicm9sZXMiOlsidGhpcyIsInRoYXQiLCJ0aGVvdGhlciJdLCJpc3MiOiJpc3N1ZXIiLCJwZXJzb25JZCI6Ijc1YmIzY2M3LWI5MzMtNDRmMC05M2M2LTE0N2IwODJmYWRiNSIsImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.lD6jUsazVtzeGhRTNeP_b2Zs6O798V2FQql11QOEI1Q - local MISSING_EMAIL_JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwiaXNzIjoiaXNzdWVyIiwicGVyc29uSWQiOiI3NWJiM2NjNy1iOTMzLTQ0ZjAtOTNjNi0xNDdiMDgyZmFkYjUiLCJleHAiOjE5MDg4MzUyMDAsImlhdCI6MTQ4ODgxOTYwMCwidXNlcm5hbWUiOiJoZWxsby53b3JsZCJ9.tJoAl_pvq95hK7GKqsp5TU462pLTbmSYZc1fAHzcqWM - local VALID_RS256_JWT=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.cn5Gb75XL-r7TMsPuqzWoKZ06ZsyF_VZIG0Ohn8uZZFeF8dFUhSrEOYe8WFN6Eon8a8LC0OCI9eNdGiD4m_e9TD1Iz2juqaeos-6yd7SWuODr4YS8KD3cqfXndnLRPzp9PC_UIpATsbqOmxGDrRKvHsQq0TuIXImU3rM_m3kFJFgtoJFHx3KmZUo_Ozkyhhc6Pukikhy6odNAtEyLHP5_tabMXtkeAuIlG8dhjAxef4mJLexYFclG-vl7No5VBU4JrMbfgyxtobcYoE-bDIpmQHywrwo6Li7X0hgHJ17sfS3G2YMHmE-Ij_W2Lf9kf5r2r12DUvg44SLIfM58pCINQ - - test_jwt "Insecure test" "/" "200" - - test_jwt "Secure test without jwt cookie" "/secure/" "302" - - test_jwt "Secure test with jwt cookie" "/secure/" "200" "--cookie \"rampartjwt=${VALIDJWT}\"" - - test_jwt "Secure test with jwt auth header" "/secure-auth-header/" "200" "--header \"Authorization: Bearer ${VALIDJWT}\"" - - test_jwt "Secure test without jwt auth header" "/secure-auth-header/" "302" - - test_jwt "Secure test without jwt auth header" "/secure-no-redirect/" "401" - - test_jwt "Secure test with jwt cookie - with no sub" "/secure/" "200" " --cookie \"rampartjwt=${MISSING_SUB_JWT}\"" - - test_jwt "Secure test with jwt cookie - with no email" "/secure/" "200" " --cookie \"rampartjwt=${MISSING_EMAIL_JWT}\"" - - test_jwt "Secure test with rs256 jwt cookie" "/secure-rs256/" "200" " --cookie \"rampartjwt=${VALID_RS256_JWT}\"" -} - -main "$@" \ No newline at end of file diff --git a/test/docker-compose-test.yml b/test/docker-compose-test.yml new file mode 100644 index 0000000..cc570c5 --- /dev/null +++ b/test/docker-compose-test.yml @@ -0,0 +1,25 @@ +services: + + nginx: + container_name: ${TEST_CONTAINER_NAME_PREFIX:?required}-nginx + build: + context: . + dockerfile: test-nginx.dockerfile + platforms: + - linux/amd64 + args: + BASE_IMAGE: ${FULL_IMAGE_NAME}:${NGINX_VERSION:?required} + platform: linux/amd64 + logging: + driver: ${LOG_DRIVER:-journald} + + runner: + container_name: ${TEST_CONTAINER_NAME_PREFIX:?required}-runner + build: + context: . + dockerfile: test-runner.dockerfile + platforms: + - linux/amd64 + platform: linux/amd64 + depends_on: + - nginx diff --git a/test/ec_key_256.pem b/test/ec_key_256.pem new file mode 100644 index 0000000..4206969 --- /dev/null +++ b/test/ec_key_256.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgOlEBGcZxxhv8FkN0 +YIvax6fnhJbMeotzIEBxIglkNu6hRANCAATP1NpDzvZmKd2Mw6hIrv4nzUfNu7OK +mT5VuL5LhvUgzTqVGuxwevA7DlFsNVSfCljIBG3geio3fcd4k0Z9SygL +-----END PRIVATE KEY----- diff --git a/test/ec_key_384.pem b/test/ec_key_384.pem new file mode 100644 index 0000000..2aa5780 --- /dev/null +++ b/test/ec_key_384.pem @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDADyrL6llSQoQOZ/PF/ +l761kAbrTwn4vu30Kr34ScW6bRKVXLq3cT3QssJ1nF9B63qhZANiAAQ48dOfIEd3 +0TCVE0JT4ZU0Db7Ftz+ex7lojP7uqTY9OI59yoMB01zUN4JK30BRXS9Yv0A9Bu1z +fgLu93FSn0kd0zIPMvuu5LUt60M/miSt2lA0OrqFhKjx6FFdN/lNh64= +-----END PRIVATE KEY----- diff --git a/test/ec_key_521.pem b/test/ec_key_521.pem new file mode 100644 index 0000000..10471dc --- /dev/null +++ b/test/ec_key_521.pem @@ -0,0 +1,8 @@ +-----BEGIN PRIVATE KEY----- +MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIAKkag6aVn4XAbaALo +0b3pypdP5RBX7uKxHmKlkNCcpA0oVTdgjnM5NpJP8ZOM6NjVhEzsn6c/Tdn8hL8w +SI55hFWhgYkDgYYABABpTipSvbs8fq44u4fA+v7DTNYViA58sqbrxjxdzwWZ8eEj +CXsH7yzSGx3Y19NSyrX8HbjWmrj5uxiKeFCB8mGzTwDcFIKCMeMkHjZs/fmVOumR +a2XSpj7BP6wqcN6Pf+UqECivGAZGRHoabo/dm5zF9M3gO+G9eOrf3G1wgFFM7Vzb +Ow== +-----END PRIVATE KEY----- diff --git a/test/etc/nginx/conf.d/test.conf b/test/etc/nginx/conf.d/test.conf new file mode 100644 index 0000000..4e5d764 --- /dev/null +++ b/test/etc/nginx/conf.d/test.conf @@ -0,0 +1,448 @@ +error_log /var/log/nginx/debug.log debug; +access_log /var/log/nginx/access.log; + +server { + listen %{PORT}; + listen %{SSL_PORT} ssl; + server_name localhost; + + ssl_certificate /etc/nginx/test.crt; + ssl_certificate_key /etc/nginx/test.key; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + + auth_jwt_key "00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF"; + auth_jwt_loginurl "https://example.com/login"; + auth_jwt_enabled off; + + location / { + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/cookie/default { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location COOKIE=jwt; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/cookie/default/validate-sub { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_validate_sub on; + auth_jwt_location COOKIE=jwt; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/cookie/default/no-redirect { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location COOKIE=jwt; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/cookie/hs256 { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location COOKIE=jwt; + auth_jwt_algorithm HS256; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/cookie/hs384 { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location COOKIE=jwt; + auth_jwt_algorithm HS384; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/cookie/hs512 { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location COOKIE=jwt; + auth_jwt_algorithm HS512; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/cookie/es256 { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location COOKIE=jwt; + auth_jwt_algorithm ES256; + auth_jwt_key "-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEz9TaQ872ZindjMOoSK7+J81Hzbuz +ipk+Vbi+S4b1IM06lRrscHrwOw5RbDVUnwpYyARt4HoqN33HeJNGfUsoCw== +-----END PUBLIC KEY-----"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/cookie/es384 { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location COOKIE=jwt; + auth_jwt_algorithm ES384; + auth_jwt_key "-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEOPHTnyBHd9EwlRNCU+GVNA2+xbc/nse5 +aIz+7qk2PTiOfcqDAdNc1DeCSt9AUV0vWL9APQbtc34C7vdxUp9JHdMyDzL7ruS1 +LetDP5okrdpQNDq6hYSo8ehRXTf5TYeu +-----END PUBLIC KEY-----"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/cookie/es512 { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location COOKIE=jwt; + auth_jwt_algorithm ES512; + auth_jwt_key "-----BEGIN PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAaU4qUr27PH6uOLuHwPr+w0zWFYgO +fLKm68Y8Xc8FmfHhIwl7B+8s0hsd2NfTUsq1/B241pq4+bsYinhQgfJhs08A3BSC +gjHjJB42bP35lTrpkWtl0qY+wT+sKnDej3/lKhAorxgGRkR6Gm6P3ZucxfTN4Dvh +vXjq39xtcIBRTO1c2zs= +-----END PUBLIC KEY-----"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/default { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location HEADER=Authorization; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/default/no-redirect { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location HEADER=Authorization; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/default/proxy-header { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location HEADER=Authorization; + + add_header "Test-Authorization" "$http_authorization"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/rs256 { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location HEADER=Authorization; + auth_jwt_key "-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwtpMAM4l1H995oqlqdMh +uqNuffp4+4aUCwuFE9B5s9MJr63gyf8jW0oDr7Mb1Xb8y9iGkWfhouZqNJbMFry+ +iBs+z2TtJF06vbHQZzajDsdux3XVfXv9v6dDIImyU24MsGNkpNt0GISaaiqv51NM +ZQX0miOXXWdkQvWTZFXhmsFCmJLE67oQFSar4hzfAaCulaMD+b3Mcsjlh0yvSq7g +6swiIasEU3qNLKaJAZEzfywroVYr3BwM1IiVbQeKgIkyPS/85M4Y6Ss/T+OWi1Oe +K49NdYBvFP+hNVEoeZzJz5K/nd6C35IX0t2bN5CVXchUFmaUMYk2iPdhXdsC720t +BwIDAQAB +-----END PUBLIC KEY-----"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/es256 { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location HEADER=Authorization; + auth_jwt_key "-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEz9TaQ872ZindjMOoSK7+J81Hzbuz +ipk+Vbi+S4b1IM06lRrscHrwOw5RbDVUnwpYyARt4HoqN33HeJNGfUsoCw== +-----END PUBLIC KEY-----"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/es384 { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location HEADER=Authorization; + auth_jwt_key "-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEOPHTnyBHd9EwlRNCU+GVNA2+xbc/nse5 +aIz+7qk2PTiOfcqDAdNc1DeCSt9AUV0vWL9APQbtc34C7vdxUp9JHdMyDzL7ruS1 +LetDP5okrdpQNDq6hYSo8ehRXTf5TYeu +-----END PUBLIC KEY-----"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/es512 { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location HEADER=Authorization; + auth_jwt_key "-----BEGIN PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAaU4qUr27PH6uOLuHwPr+w0zWFYgO +fLKm68Y8Xc8FmfHhIwl7B+8s0hsd2NfTUsq1/B241pq4+bsYinhQgfJhs08A3BSC +gjHjJB42bP35lTrpkWtl0qY+wT+sKnDej3/lKhAorxgGRkR6Gm6P3ZucxfTN4Dvh +vXjq39xtcIBRTO1c2zs= +-----END PUBLIC KEY-----"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/rs256/file { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location HEADER=Authorization; + auth_jwt_algorithm RS256; + auth_jwt_use_keyfile on; + auth_jwt_keyfile_path "/etc/nginx/rsa_key_2048-pub.pem"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/rs384/file { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location HEADER=Authorization; + auth_jwt_algorithm RS384; + auth_jwt_use_keyfile on; + auth_jwt_keyfile_path "/etc/nginx/rsa_key_2048-pub.pem"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/rs512/file { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location HEADER=Authorization; + auth_jwt_algorithm RS512; + auth_jwt_use_keyfile on; + auth_jwt_keyfile_path "/etc/nginx/rsa_key_2048-pub.pem"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/es256/file { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location HEADER=Authorization; + auth_jwt_algorithm ES256; + auth_jwt_use_keyfile on; + auth_jwt_keyfile_path "/etc/nginx/ec_key_256-pub.pem"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/es384/file { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location HEADER=Authorization; + auth_jwt_algorithm ES384; + auth_jwt_use_keyfile on; + auth_jwt_keyfile_path "/etc/nginx/ec_key_384-pub.pem"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/auth-header/es512/file { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location HEADER=Authorization; + auth_jwt_algorithm ES512; + auth_jwt_use_keyfile on; + auth_jwt_keyfile_path "/etc/nginx/ec_key_521-pub.pem"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/custom-header/hs256 { + auth_jwt_enabled on; + auth_jwt_redirect on; + auth_jwt_location HEADER=Auth-Token; + auth_jwt_algorithm HS256; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/extract-claim/request/sub { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location HEADER=Authorization; + auth_jwt_extract_request_claims sub; + + add_header "Test" "sub=$http_jwt_sub"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/extract-claim/request/name-1 { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location HEADER=Authorization; + auth_jwt_extract_request_claims firstName lastName; + + add_header "Test" "firstName=$http_jwt_firstname; lastName=$http_jwt_lastname"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/extract-claim/request/name-2 { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location HEADER=Authorization; + auth_jwt_extract_request_claims firstName; + auth_jwt_extract_request_claims lastName; + + add_header "Test" "firstName=$http_jwt_firstname; lastName=$http_jwt_lastname"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/extract-claim/request/nested { + location /secure/extract-claim/request/nested { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location HEADER=Authorization; + auth_jwt_extract_request_claims username; + + add_header "Test" "username=$http_jwt_username"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + } + + location /secure/extract-claim/response/sub { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location HEADER=Authorization; + auth_jwt_extract_response_claims sub; + + add_header "Test" "sub=$sent_http_jwt_sub"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/extract-claim/response/name-1 { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location HEADER=Authorization; + auth_jwt_extract_response_claims firstName lastName; + + add_header "Test" "firstName=$sent_http_jwt_firstname; lastName=$sent_http_jwt_lastname"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/extract-claim/response/name-2 { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location HEADER=Authorization; + auth_jwt_extract_response_claims firstName; + auth_jwt_extract_response_claims lastName; + + add_header "Test" "firstName=$sent_http_jwt_firstname; lastName=$sent_http_jwt_lastname"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + + location /secure/extract-claim/response/nested { + location /secure/extract-claim/response/nested { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location HEADER=Authorization; + auth_jwt_extract_response_claims username; + + add_header "Test" "username=$sent_http_jwt_username"; + + alias /usr/share/nginx/html/; + try_files index.html =404; + } + } + + location /secure/extract-claim/if/sub { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location HEADER=Authorization; + auth_jwt_extract_var_claims sub; + + if ($jwt_claim_sub = 'some-long-uuid') { + return 200; + } + + return 401; + } + + location /secure/extract-claim/body/sub { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location HEADER=Authorization; + auth_jwt_extract_var_claims sub; + + return 200 "sub: $jwt_claim_sub"; + } + + location /secure/extract-claim/body/multiple { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location HEADER=Authorization; + auth_jwt_validate_sub on; + auth_jwt_extract_var_claims firstName middleName lastName; + + return 200 "you are: $jwt_claim_firstName $jwt_claim_middleName $jwt_claim_lastName"; + } + + location /profile { + auth_jwt_enabled on; + auth_jwt_redirect off; + auth_jwt_location HEADER=Authorization; + auth_jwt_validate_sub on; + + location /profile/me { + auth_jwt_extract_var_claims sub; + + return 301 /profile/$jwt_claim_sub; + } + } + + location /return-url { + auth_jwt_enabled on; + auth_jwt_redirect on; + } +} diff --git a/test/etc/nginx/ec_key_256-pub.pem b/test/etc/nginx/ec_key_256-pub.pem new file mode 100644 index 0000000..3306ea0 --- /dev/null +++ b/test/etc/nginx/ec_key_256-pub.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEz9TaQ872ZindjMOoSK7+J81Hzbuz +ipk+Vbi+S4b1IM06lRrscHrwOw5RbDVUnwpYyARt4HoqN33HeJNGfUsoCw== +-----END PUBLIC KEY----- diff --git a/test/etc/nginx/ec_key_384-pub.pem b/test/etc/nginx/ec_key_384-pub.pem new file mode 100644 index 0000000..e642ed1 --- /dev/null +++ b/test/etc/nginx/ec_key_384-pub.pem @@ -0,0 +1,5 @@ +-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEOPHTnyBHd9EwlRNCU+GVNA2+xbc/nse5 +aIz+7qk2PTiOfcqDAdNc1DeCSt9AUV0vWL9APQbtc34C7vdxUp9JHdMyDzL7ruS1 +LetDP5okrdpQNDq6hYSo8ehRXTf5TYeu +-----END PUBLIC KEY----- diff --git a/test/etc/nginx/ec_key_521-pub.pem b/test/etc/nginx/ec_key_521-pub.pem new file mode 100644 index 0000000..0cb875c --- /dev/null +++ b/test/etc/nginx/ec_key_521-pub.pem @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAaU4qUr27PH6uOLuHwPr+w0zWFYgO +fLKm68Y8Xc8FmfHhIwl7B+8s0hsd2NfTUsq1/B241pq4+bsYinhQgfJhs08A3BSC +gjHjJB42bP35lTrpkWtl0qY+wT+sKnDej3/lKhAorxgGRkR6Gm6P3ZucxfTN4Dvh +vXjq39xtcIBRTO1c2zs= +-----END PUBLIC KEY----- diff --git a/resources/rsa_key_2048-pub.pem b/test/etc/nginx/rsa_key_2048-pub.pem similarity index 100% rename from resources/rsa_key_2048-pub.pem rename to test/etc/nginx/rsa_key_2048-pub.pem diff --git a/test/etc/nginx/test.crt b/test/etc/nginx/test.crt new file mode 100644 index 0000000..fb406ba --- /dev/null +++ b/test/etc/nginx/test.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIUMG9M4Itu0cOyX0+La+7huiIoX6YwDQYJKoZIhvcNAQEL +BQAwcTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCFZpcmdpbmlhMRUwEwYDVQQHDAxG +YWxscyBDaHVyY2gxHzAdBgNVBAoMFlRlc2xhIEdvdmVybm1lbnQsIEluYy4xFzAV +BgNVBAsMDk5HSU5YIEF1dGggSldUMB4XDTI0MDMxNTE4MTM1MloXDTM0MDMxMzE4 +MTM1MlowcTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCFZpcmdpbmlhMRUwEwYDVQQH +DAxGYWxscyBDaHVyY2gxHzAdBgNVBAoMFlRlc2xhIEdvdmVybm1lbnQsIEluYy4x +FzAVBgNVBAsMDk5HSU5YIEF1dGggSldUMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAih41Ct5XgcSTz7ZVAjBb0t0z9Qae08aseoMEKJf7AmNqKtsvzeAw +/DJxOWJR5VPtUWhFAmXxPfG2B6aiSIVJVpG9yzcdQlCvyJG7Ub4QCm5GXwpU+zDC +qmD5ksz9QMdOzvRLypAU1ciZiCXjwpUnW+BZyZ9Tpmsxm6/gOzkd3rxoIbc9uXxp +5o4n6k02EPSzLzUhkZnhLQrOAGUB7+q11FAU5eNMlTWC9gQUsbNaTVtKmM2eV9BA +UHdX2GbkfFbN22l3Wey4oyNZWmye1ZFOPyBR+tyU3pofhb+R+hTFmeNBzrJq3i30 +Qi0B8AnulKdOjnTysPYjDTrN6xcVDWNmPQIDAQABo1MwUTAdBgNVHQ4EFgQUczdy +7s64NJHNGsQTf/zwFnQe6LMwHwYDVR0jBBgwFoAUczdy7s64NJHNGsQTf/zwFnQe +6LMwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAfcxCiz6ShHof +lXiE2j+s556SM2n8oW/S1BSjFC2wF1uKVeMJA1gAaWObC3ElqffFlqTdCorhgRS/ +knWa+Sqe/jWBSgwLG/e5DvxXWjD7b7kZdAZNy9evs5nhVfcLT+GyvB/z5GdAFY7s +xYmLrC07ubhHIL9h7lhNKbRr++o+BcClQBZKRO4fxBwXxqx/rHudjH87Wr61Ov52 +90xNjwcqvevY0skmPao5+oyxkURdKZualNxiOGMPpywkpJkfl8Az5xKAJhUMAtFR +smhQduejEkcxfxtsiYgVoulI29GAsMr9zHps9zb5k0+SWIiSixjQ0CpRhLcNYu4F +QPgLQLGwUQ== +-----END CERTIFICATE----- diff --git a/test/etc/nginx/test.key b/test/etc/nginx/test.key new file mode 100644 index 0000000..13ec754 --- /dev/null +++ b/test/etc/nginx/test.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCKHjUK3leBxJPP +tlUCMFvS3TP1Bp7Txqx6gwQol/sCY2oq2y/N4DD8MnE5YlHlU+1RaEUCZfE98bYH +pqJIhUlWkb3LNx1CUK/IkbtRvhAKbkZfClT7MMKqYPmSzP1Ax07O9EvKkBTVyJmI +JePClSdb4FnJn1OmazGbr+A7OR3evGghtz25fGnmjifqTTYQ9LMvNSGRmeEtCs4A +ZQHv6rXUUBTl40yVNYL2BBSxs1pNW0qYzZ5X0EBQd1fYZuR8Vs3baXdZ7LijI1la +bJ7VkU4/IFH63JTemh+Fv5H6FMWZ40HOsmreLfRCLQHwCe6Up06OdPKw9iMNOs3r +FxUNY2Y9AgMBAAECggEAAkwEggGp/xb67FCyDJ8rdimTZFPi9U7coUCN8HNI/qrf +lTnfvox0oOUUqMMmIIQeS/HJ4ANvZe8GO3QkE8R5Sg7F0yjZL2tyTCNPgOMCMK8E +mmHS58brHdrbm658C1ILnfmssjNmNueNbuW00Koa8imCsY2ZEW+L7vTKuMFqg6c+ +BDJxC4yoCPwSTVfcajjzI6FVfphE0pd8Ho/sE8vTqdmovh23+vgfNUq1L9Smvf7R +YLM+hS1ouRP2BI5AN0sm04Kxd8MKPzuwCxteoZ9Y9YHyr1JeWGTTL0T24+LwUee/ +24zXZFrzpTgmtDYeEuVWsF5bP/fMS4Fctda3pdJMsQKBgQDCANjGDwwfSCCev2kl +WdrFJywhn5hWLWFwlo/FwLOsFJtejaBwIDRQCMPZ74H+KMHwUnO3vTanKJWqDRP9 +CdMh94C1BqobRV6rN4HgA4Opxim1EyRWHV6ui41zokk2mJrwUzKkR8t9lt9EZKrk +ZPyKER9A4hBqBmYvaYxodN8U1QKBgQC2QXUQq9j7niT7t4xMi0e9vnPLs0z1yUK9 +0nzKwTHDPflk3o2sKvH7199qVkc15JQ9DQ7NuYD7ezLbE3DJuVzpNDAfNXmfWHmp +7ukdnxyn6ZCmzQY7/fTpJTEGKVQMVCgf2f5ANgxm5EmN0yWRMcEt1VXIwCisY56p +o6nwv/1fyQKBgQCJBnIVyjEEszwfBBEvCX0kvVtFUGUXkSv+isl3onkFNPTcXuoP +6B8q3FYAy1MkggMhTAthnqpIfLjhCCWzFspidl8Y/WEOq/uGsUjxQWowcr+onqGO +lWX3oKfDIb/WaQkeb5UYRYFr7jE6LGQrt0xL9HX/rOxtBqIMIN/EM7ARFQKBgDAJ +zMtaIFUh9+mJFafPRleS7X6RggV+yOKzqkTe6zjlCuk1Z+4rW6Df43lpyFdCKnh1 +CqPa805VyK/Jzf69pumo4c44EBiZ/2d1G2i9WZZAj+oHPE9vvq/9J5DSL98YB4Nt +uABAvsAYB/Mj5lEA5kQoaPYDADWABH/+LXrRf/1RAoGAUvxPvmpkGMC+KdmjLam7 +CPC3+y4MZOyZ11BhOxLhd1K2qcQd9K7tkjUhNxRn5GVzpzOKeFJFtiih2uN+PBNJ +oylPR03uk/7D52b1OYaJhs9bQkth//Qk935nyRM26C2vG4tQLfT/cFi5F53n0ZCQ +7e8O6+QY0lZnpvsfnt8YIsM= +-----END PRIVATE KEY----- diff --git a/resources/rsa_key_2048.pem b/test/rsa_key_2048.pem similarity index 100% rename from resources/rsa_key_2048.pem rename to test/rsa_key_2048.pem diff --git a/test/test-nginx.dockerfile b/test/test-nginx.dockerfile new file mode 100644 index 0000000..f8c323f --- /dev/null +++ b/test/test-nginx.dockerfile @@ -0,0 +1,19 @@ +ARG BASE_IMAGE + +FROM ${BASE_IMAGE:?required} +ARG PORT +ARG SSL_PORT + +COPY etc/ /etc/ + +COPY <<` /usr/share/nginx/html/index.html + + Test + +

NGINX Auth-JWT Module Test

+ + +` + +RUN sed -i "s|%{PORT}|${PORT:?required}|" /etc/nginx/conf.d/test.conf +RUN sed -i "s|%{SSL_PORT}|${SSL_PORT:?required}|" /etc/nginx/conf.d/test.conf diff --git a/test/test-runner.dockerfile b/test/test-runner.dockerfile new file mode 100644 index 0000000..377c40e --- /dev/null +++ b/test/test-runner.dockerfile @@ -0,0 +1,18 @@ +ARG RUNNER_BASE_IMAGE + +FROM ${RUNNER_BASE_IMAGE:?required} +ARG PORT +ARG SSL_PORT + +ENV PORT=${PORT:?required} +ENV SSL_PORT=${SSL_PORT:?required} + +RUN <<` + set -e + apt-get update + apt-get install -y curl bash +` + +COPY test.sh . + +CMD ["./test.sh"] diff --git a/test/test.sh b/test/test.sh new file mode 100755 index 0000000..c726a75 --- /dev/null +++ b/test/test.sh @@ -0,0 +1,387 @@ +#!/bin/bash -eu + +# set a test # here to execute only that test and output additional info +DEBUG= + +RED='\e[31m' +GREEN='\e[32m' +GRAY='\e[90m' +NC='\e[00m' + +NUM_TESTS=0; +NUM_SKIPPED=0; +NUM_FAILED=0; + +run_test () { + NUM_TESTS=$((${NUM_TESTS} + 1)); + + if [ "${DEBUG}" == '' ] || [ ${DEBUG} == ${NUM_TESTS} ]; then + local OPTIND; + local name= + local path= + local expectedCode= + local expectedResponseRegex= + local extraCurlOpts= + local scheme='http' + local port=${PORT} + local curlCommand= + local exitCode= + local response= + local testNum="${GRAY}${NUM_TESTS}${NC}\t" + + while getopts "n:asp:r:c:x:" option; do + case $option in + n) + name=$OPTARG;; + s) + scheme='https' + port=${SSL_PORT};; + p) + path=$OPTARG;; + c) + expectedCode=$OPTARG;; + r) + expectedResponseRegex=$OPTARG;; + x) + extraCurlOpts=$OPTARG;; + \?) # Invalid option + printf "Error: Invalid option\n"; + exit;; + esac + done + + curlCommand="curl -skv ${scheme}://nginx:${port}${path} -H 'Cache-Control: no-cache' ${extraCurlOpts} 2>&1" + response=$(eval "${curlCommand}") + exitCode=$? + + printf "\n${testNum}" + + if [ "${exitCode}" -ne "0" ]; then + printf "${RED}${name} -- unexpected exit code from cURL\n\tcURL Exit Code: ${exitCode}"; + NUM_FAILED=$((${NUM_FAILED} + 1)); + else + local okay=1 + + if [ "${expectedCode}" != "" ]; then + local responseCode=$(echo "${response}" | grep -Eo 'HTTP/1.1 ([0-9]{3})' | awk '{print $2}') + + if [ "${expectedCode}" != "${responseCode}" ]; then + printf "${RED}${name} -- unexpected status code\n\tExpected: ${expectedCode}\n\tActual: ${responseCode}\n\tPath: ${path}" + NUM_FAILED=$((${NUM_FAILED} + 1)) + okay=0 + fi + fi + + if [ "${okay}" == '1' ] && [ "${expectedResponseRegex}" != "" ] && ! [[ "${response}" =~ ${expectedResponseRegex} ]]; then + printf "${RED}${name} -- regex not found in response\n\tPath: ${path}\n\tRegEx: ${expectedResponseRegex//%/%%}" + NUM_FAILED=$((${NUM_FAILED} + 1)) + okay=0 + fi + + if [ "${okay}" == '1' ]; then + printf "${GREEN}${name}"; + fi + fi + + if [ "${DEBUG}" == "${NUM_TESTS}" ]; then + printf '\n\tcURL Command: %s' "${curlCommand:---}" + printf '\n\tResponse: %s' "${response:---}" + fi + + printf "${NC}\n" + else + NUM_SKIPPED=$((${NUM_SKIPPED} + 1)) + fi +} + +main() { + local JWT_HS256_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ.r8tG8IZheiQ-i6HqUYyJj9V6dipgcQ4ZIdxau6QCZDo + local JWT_HS256_MISSING_SUB=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmaXJzdE5hbWUiOiJoZWxsbyIsImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwicm9sZXMiOlsidGhpcyIsInRoYXQiLCJ0aGVvdGhlciJdLCJpc3MiOiJpc3N1ZXIiLCJwZXJzb25JZCI6Ijc1YmIzY2M3LWI5MzMtNDRmMC05M2M2LTE0N2IwODJmYWRiNSIsImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.lD6jUsazVtzeGhRTNeP_b2Zs6O798V2FQql11QOEI1Q + local JWT_HS256_MISSING_EMAIL=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwiaXNzIjoiaXNzdWVyIiwicGVyc29uSWQiOiI3NWJiM2NjNy1iOTMzLTQ0ZjAtOTNjNi0xNDdiMDgyZmFkYjUiLCJleHAiOjE5MDg4MzUyMDAsImlhdCI6MTQ4ODgxOTYwMCwidXNlcm5hbWUiOiJoZWxsby53b3JsZCJ9.tJoAl_pvq95hK7GKqsp5TU462pLTbmSYZc1fAHzcqWM + local JWT_HS384_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ.SS57j7PEybjbsp3g5W-IhhJHBmG5K-97qvgBKL16xj9ey-uMeEenWjGbB2vVp0kq + local JWT_HS512_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ.xtSU6EWN2LILVsYzJFJpKnRkqjn_3qjz-J2ttNKnhZ60_5YjFeC8io4k8k1u77zlohSWvWMdugD9ZaB3vjJo-w + local JWT_RS256_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwgImxhc3ROYW1lIjoid29ybGQiLCJlbWFpbEFkZHJlc3MiOiJoZWxsb3dvcmxkQGV4YW1wbGUuY29tIiwgInJvbGVzIjpbInRoaXMiLCJ0aGF0IiwidGhlb3RoZXIiXSwgImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwgImV4cCI6MTkwODgzNTIwMCwiaWF0IjoxNDg4ODE5NjAwLCJ1c2VybmFtZSI6ImhlbGxvLndvcmxkIn0.cn5Gb75XL-r7TMsPuqzWoKZ06ZsyF_VZIG0Ohn8uZZFeF8dFUhSrEOYe8WFN6Eon8a8LC0OCI9eNdGiD4m_e9TD1Iz2juqaeos-6yd7SWuODr4YS8KD3cqfXndnLRPzp9PC_UIpATsbqOmxGDrRKvHsQq0TuIXImU3rM_m3kFJFgtoJFHx3KmZUo_Ozkyhhc6Pukikhy6odNAtEyLHP5_tabMXtkeAuIlG8dhjAxef4mJLexYFclG-vl7No5VBU4JrMbfgyxtobcYoE-bDIpmQHywrwo6Li7X0hgHJ17sfS3G2YMHmE-Ij_W2Lf9kf5r2r12DUvg44SLIfM58pCINQ + local JWT_RS256_INVALID=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ._aQmIBL4CVBxU1fNMOHp0kkagFaaX2TvAEenizytwd0 + local JWT_RS384_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzM4NCJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ.H35bTcZRhepWIoa8pKCbUMRuAOkVX9K5hJjc6tPmQwWmTw8lrktsvmMzJg_rgqnJLnAkciSIQw5EDj7fngS5zX2ThyRxrkPuE2Uiyw2Ect-mo9Kg1lrWgnyZCuCgq-Up9HQRAv0160mePlm8Gs4TOY6CPr38zwTcDZsy_Keq93igDQV8WuuWAGICaGd5ZyUOPjjzGShRjTU8Szz7fnpZpTtYRCYVo0pc5yfRWYm0fdn-4AseyGvd8JJ2xfnAEe4kZOkz7X1MLKtL0slKg3m2PH1lD7HwxIawXRTPWxArhJ9dcTNiDUrqtde2juGwOuMD_zTsb2Jj0_rmRb0Q6aljNw + local JWT_RS512_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ.iUupyKypfXJ5aZWfItSW-mOmx9a4C4X7Yr5p5Fk8W75ZhkOq0EeNfstTxx870brhkdPovBhO2LYI44_HoH9XicQNL6JnFprE0r61eJFngbuzlhRQiWpq0xYrazJWc9zB7_GgL2ZCwtw-Ts3G23Q0632wVm6-d7MKvG7RS8aEjN-MuVGdtLglH3forpItmFxw-if40EQsBL7hncN_XNcQTO4KPHkqmlpac_oKXRrLFDIIt2tB6OOpvY4QcpERoxexp4pi2f-JoINnWX_dU5JnIs3ypVJLQPfoJvxg8fsg3zYrOvMYnfsqOCYoHtZGK0O7jyfFmcGo5v2hLT-CpoF3Zw + local JWT_ES256_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ.WFfJXGr5whKHB7arjsTXPTJ6TAsS1LoRxu7Vj2_HrLaIQphWJM6BICf-M3cv52tFzt-XTZb6GxlDgAbHo8z9Zg + local JWT_ES256_INVALID=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ._aQmIBL4CVBxU1fNMOHp0kkagFaaX2TvAEenizytwd0 + local JWT_ES384_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzM4NCJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ._EFxXYOTAfT3gB3xUfgGR2UyXHeRTlDWqA94oZbB0DDa7YPZTEX9T4C_0ylnOFKZ6irGHZA8vxjgXDH3DZKWwBWcZ-XaQ_Q4Ws2J-AEeLqcl7_CS6q9mFo0Y7vUNEn-W + local JWT_ES512_VALID=eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzUxMiJ9.eyJzdWIiOiJzb21lLWxvbmctdXVpZCIsImZpcnN0TmFtZSI6ImhlbGxvIiwibGFzdE5hbWUiOiJ3b3JsZCIsImVtYWlsQWRkcmVzcyI6ImhlbGxvd29ybGRAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJ0aGlzIiwidGhhdCIsInRoZW90aGVyIl0sImlzcyI6Imlzc3VlciIsInBlcnNvbklkIjoiNzViYjNjYzctYjkzMy00NGYwLTkzYzYtMTQ3YjA4MmZhZGI1IiwiZXhwIjoxOTA4ODM1MjAwLCJpYXQiOjE0ODg4MTk2MDAsInVzZXJuYW1lIjoiaGVsbG8ud29ybGQifQ.AFY4gNCtZNYkrTiijDkV4eKIt2UPMIuJBfZIk69jgI8FSGCQyUIMmIVg0fTvbaSiaryXzcjbG5TCm8a9Vu3KFJutAHGrgvZqcdklxx6Fbk3an3r_CH68n_ncwS3SUV58mDjf0OX8jRuNdudU1L5xYNQdodo-fxPIb1oHXfMJ0CmULDR9 + + run_test -n 'when auth disabled, should return 200' \ + -p '/' \ + -c '200' + + run_test -n '[SSL] when auth disabled, should return 200' \ + -s \ + -p '/' \ + -c '200' + + run_test -n 'when auth enabled with default algorithm and no JWT in Authorization header, returns 302' \ + -p '/secure/auth-header/default' \ + -c '302' + + run_test -n '[SSL] when auth enabled with default algorithm and no JWT in Authorization header, returns 302' \ + -s \ + -p '/secure/auth-header/default' \ + -c '302' + + run_test -n 'when auth enabled with default algorithm with no redirect and Authorization header missing Bearer, should return 200' \ + -p '/secure/auth-header/default/no-redirect' \ + -c '200' \ + -x "--header \"Authorization: ${JWT_HS256_VALID}\"" + + run_test -n 'when auth enabled with default algorithm with no redirect and Authorization header with Bearer, should return 200' \ + -p '/secure/auth-header/default/no-redirect' \ + -c '200' \ + -x "--header \"Authorization: Bearer ${JWT_HS256_VALID}\"" + + run_test -n 'when auth enabled with Authorization header with Bearer, should keep header intact' \ + -p '/secure/auth-header/default/proxy-header' \ + -c '200' \ + -r "< Test-Authorization: Bearer ${JWT_HS256_VALID}" \ + -x "--header \"Authorization: Bearer ${JWT_HS256_VALID}\"" + + run_test -n 'when auth enabled with Authorization header with Bearer, lower-case "bearer" should be accepted' \ + -p '/secure/auth-header/default/proxy-header' \ + -c '200' \ + -r "< Test-Authorization: bearer ${JWT_HS256_VALID}" \ + -x "--header \"Authorization: bearer ${JWT_HS256_VALID}\"" + + run_test -n 'when auth enabled with default algorithm and no JWT cookie, returns 302' \ + -p '/secure/cookie/default' \ + -c '302' + + run_test -n 'when auth enabled with default algorithm with no redirect and no JWT cookie, should return 401' \ + -p '/secure/cookie/default/no-redirect' \ + -c '401' + + run_test -n 'when auth enabled with default algorithm and valid JWT cookie, returns 200' \ + -p '/secure/cookie/default' \ + -c '200' \ + -x "--cookie jwt=${JWT_HS256_VALID}" + + run_test -n 'when auth enabled with default algorithm and valid JWT cookie with no sub, returns 200' \ + -p '/secure/cookie/default' \ + -c '200' \ + -x ' --cookie "jwt=${JWT_HS256_MISSING_SUB}"' + + run_test -n 'when auth enabled with default algorithm and valid JWT cookie with no sub when sub validated, returns 302' \ + -p '/secure/cookie/default/validate-sub' \ + -c '302' \ + -x ' --cookie "jwt=${JWT_HS256_MISSING_SUB}"' + + run_test -n 'when auth enabled with default algorithm and valid JWT cookie with no email, returns 200' \ + -p '/secure/cookie/default' \ + -c '200' \ + -x ' --cookie "jwt=${JWT_HS256_MISSING_EMAIL}"' + + run_test -n 'when auth enabled with HS256 algorithm and valid JWT cookie, returns 200' \ + -p '/secure/cookie/hs256' \ + -c '200' \ + -x '--cookie "jwt=${JWT_HS256_VALID}"' + + run_test -n 'when auth enabled with HS384 algorithm and valid JWT cookie, returns 200' \ + -p '/secure/cookie/hs384' \ + -c '200' \ + -x '--cookie "jwt=${JWT_HS384_VALID}"' + + run_test -n 'when auth enabled with HS512 algorithm and valid JWT cookie, returns 200' \ + -p '/secure/cookie/hs512' \ + -c '200' \ + -x '--cookie "jwt=${JWT_HS512_VALID}"' + + run_test -n 'when auth enabled with RS256 algorithm and valid JWT cookie, returns 200' \ + -p '/secure/cookie/rs256' \ + -c '200' \ + -x ' --cookie "jwt=${JWT_RS256_VALID}"' + + run_test -n 'when auth enabled with ES256 algorithm and valid JWT cookie, returns 200' \ + -p '/secure/cookie/es256' \ + -c '200' \ + -x ' --cookie "jwt=${JWT_ES256_VALID}"' + + run_test -n 'when auth enabled with ES384 algorithm and valid JWT cookie, returns 200' \ + -p '/secure/cookie/es384' \ + -c '200' \ + -x ' --cookie "jwt=${JWT_ES384_VALID}"' + + run_test -n 'when auth enabled with ES512 algorithm and valid JWT cookie, returns 200' \ + -p '/secure/cookie/es512' \ + -c '200' \ + -x ' --cookie "jwt=${JWT_ES512_VALID}"' + + run_test -n 'when auth enabled with RS256 algorithm via file and valid JWT in Authorization header, returns 200' \ + -p '/secure/auth-header/rs256/file' \ + -c '200' \ + -x '--header "Authorization: Bearer ${JWT_RS256_VALID}"' + + run_test -n 'when auth enabled with RS256 algorithm via file and invalid JWT in Authorization header, returns 401' \ + -p '/secure/auth-header/rs256/file' \ + -c '302' \ + -x '--header "Authorization: Bearer ${JWT_RS256_INVALID}"' + + run_test -n 'when auth enabled with RS384 algorithm via file and valid JWT in Authorization header, returns 200' \ + -p '/secure/auth-header/rs384/file' \ + -c '200' \ + -x '--header "Authorization: Bearer ${JWT_RS256_VALID}"' + + run_test -n 'when auth enabled with RS512 algorithm via file and valid JWT in Authorization header, returns 200' \ + -p '/secure/auth-header/rs512/file' \ + -c '200' \ + -x '--header "Authorization: Bearer ${JWT_RS256_VALID}"' + + run_test -n 'when auth enabled with ES256 algorithm via file and valid JWT in Authorization header, returns 200' \ + -p '/secure/auth-header/es256/file' \ + -c '200' \ + -x '--header "Authorization: Bearer ${JWT_ES256_VALID}"' + + run_test -n 'when auth enabled with ES256 algorithm via file and invalid JWT in Authorization header, returns 401' \ + -p '/secure/auth-header/es256/file' \ + -c '302' \ + -x '--header "Authorization: Bearer ${JWT_ES256_INVALID}"' + + run_test -n 'when auth enabled with ES384 algorithm via file and valid JWT in Authorization header, returns 200' \ + -p '/secure/auth-header/es384/file' \ + -c '200' \ + -x '--header "Authorization: Bearer ${JWT_ES384_VALID}"' + + run_test -n 'when auth enabled with ES512 algorithm via file and valid JWT in Authorization header, returns 200' \ + -p '/secure/auth-header/es512/file' \ + -c '200' \ + -x '--header "Authorization: Bearer ${JWT_ES512_VALID}"' + + run_test -n 'when auth enabled with HS256 algorithm and valid JWT in custom header without bearer, returns 200' \ + -p '/secure/custom-header/hs256/' \ + -c '200' \ + -x '--header "Auth-Token: ${JWT_HS256_VALID}"' + + run_test -n 'when auth enabled with HS256 algorithm and valid JWT in custom header with bearer, returns 200' \ + -p '/secure/custom-header/hs256/' \ + -c '200' \ + -x '--header "Auth-Token: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts single claim to request variable' \ + -p '/secure/extract-claim/request/sub' \ + -r '< Test: sub=some-long-uuid' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts multiple claims (single directive) to request variable' \ + -p '/secure/extract-claim/request/name-1' \ + -r '< Test: firstName=hello; lastName=world' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts multiple claims (multiple directives) to request variable' \ + -p '/secure/extract-claim/request/name-2' \ + -r '< Test: firstName=hello; lastName=world' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts nested claim to request variable' \ + -p '/secure/extract-claim/request/nested' \ + -r '< Test: username=hello\.world' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts single claim to response variable' \ + -p '/secure/extract-claim/response/sub' \ + -r '< Test: sub=some-long-uuid' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts multiple claims (single directive) to response variable' \ + -p '/secure/extract-claim/response/name-1' \ + -r '< Test: firstName=hello; lastName=world' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts multiple claims (multiple directives) to response variable' \ + -p '/secure/extract-claim/response/name-2' \ + -r '< Test: firstName=hello; lastName=world' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts nested claim to response variable' \ + -p '/secure/extract-claim/response/nested' \ + -r '< Test: username=hello.world' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts single claim to response header' \ + -p '/secure/extract-claim/response/sub' \ + -r '< JWT-sub: some-long-uuid' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts multiple claims (single directive) to response header' \ + -p '/secure/extract-claim/response/name-1' \ + -r '< JWT-firstName: hello' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts multiple claims (multiple directives) to response header' \ + -p '/secure/extract-claim/response/name-2' \ + -r '< JWT-firstName: hello' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts nested claim to response header' \ + -p '/secure/extract-claim/response/nested' \ + -r '< JWT-username: hello\.world' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'tests single claim with if statement' \ + -p '/secure/extract-claim/if/sub' \ + -c 200 \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'tests absence of single claim with if statement' \ + -p '/secure/extract-claim/if/sub' \ + -c 401 \ + -x '--header "Authorization: Bearer ${JWT_HS256_MISSING_SUB}"' + + run_test -n 'extracts single claim to response body' \ + -p '/secure/extract-claim/body/sub' \ + -c 200 \ + -r 'sub: some-long-uuid$' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'extracts multiple claims to response body' \ + -p '/secure/extract-claim/body/multiple' \ + -c 200 \ + -r 'you are: hello world$' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'redirect based on claim' \ + -p '/profile/me' \ + -c 301 \ + -r '< Location: http://nginx:8000/profile/some-long-uuid' \ + -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' + + run_test -n 'returns 302 if auth enabled and no JWT provided' \ + -p '/return-url' \ + -c '302' + + run_test -n 'redirects to login if auth enabled and no JWT provided' \ + -p '/return-url' \ + -r '< Location: https://example\.com/login.*' + + run_test -n 'adds return_url to login URL when redirected to login' \ + -p '/return-url' \ + -r '< Location: https://example\.com/login\?return_url=http://nginx.*' + + run_test -n 'return_url includes port when redirected to login' \ + -p '/return-url' \ + -r "< Location: https://example\.com/login\?return_url=http://nginx:${PORT}/return-url" + + run_test -n 'return_url includes query when redirected to login' \ + -p '/return-url?test=123' \ + -r '< Location: https://example\.com/login\?return_url=http://nginx.*/return-url%3Ftest=123' + + if [[ "${NUM_FAILED}" = '0' ]]; then + printf "\nRan ${NUM_TESTS} tests successfully (skipped ${NUM_SKIPPED}).\n" + return 0 + else + printf "\nRan ${NUM_TESTS} tests: ${GREEN}$((${NUM_TESTS} - ${NUM_FAILED})) passed${NC}; ${RED}${NUM_FAILED} failed${NC}; ${NUM_SKIPPED} skipped\n" + return 1 + fi +} + +if [ "${DEBUG}" != '' ]; then + printf "\n${RED}Some tests will be skipped since DEBUG is set.${NC}\n" +fi + +printf "\n${GRAY}Starting tests using port ${PORT}...${NC}\n" +main