diff --git a/.bin/git/hooks/pre-push-build-and-test b/.bin/git/hooks/pre-push-build-and-test index 9cf4682..876ca32 100755 --- a/.bin/git/hooks/pre-push-build-and-test +++ b/.bin/git/hooks/pre-push-build-and-test @@ -4,7 +4,7 @@ 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_runner test) + (cd ${REPO_ROOT_DIR}; ./scripts.sh rebuild_nginx rebuild_test test) else HOOK_NAME=$(basename $0) diff --git a/.github/workflows/make-releases.yml b/.github/workflows/make-releases.yml index 037823b..c77edcb 100644 --- a/.github/workflows/make-releases.yml +++ b/.github/workflows/make-releases.yml @@ -1,45 +1,68 @@ -name: CI +name: Make Releases on: - push: - branches: - - master - paths: - - src/** - pull_request: - branches: - - master - paths: - - src/** 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 versions to build/test against - nginx-version: ['1.20.2', '1.22.1', '1.24.0', '1.25.3'] - - # 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: ['1.12.0', '1.14.0', '1.15.3'] + 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@v3 + uses: actions/checkout@v4 with: - path: 'ngx-http-auth-jwt-module' + 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@v3 + uses: actions/checkout@v4 with: repository: 'akheron/jansson' ref: 'v2.14' @@ -48,14 +71,15 @@ jobs: - name: Build jansson working-directory: ./jansson run: | - cmake . -DJANSSON_BUILD_SHARED_LIBS=1 -DJANSSON_BUILD_DOCS=OFF && \ - make && \ - make check && \ + 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@v3 + uses: actions/checkout@v4 with: repository: 'benmcollins/libjwt' ref: 'v${{matrix.libjwt-version}}' @@ -64,9 +88,10 @@ jobs: - name: Build libjwt working-directory: ./libjwt run: | - autoreconf -i && \ - ./configure && \ - make all && \ + set -e + autoreconf -i + ./configure + make all sudo make install - name: Download NGINX @@ -87,57 +112,71 @@ jobs: BUILD_FLAGS="${BUILD_FLAGS} --with-cc-opt='-DNGX_LINKED_LIST_COOKIES=1'" fi - ./configure --with-compat --add-dynamic-module=../ngx-http-auth-jwt-module ${BUILD_FLAGS} + ./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 + - name: Create Release Archive run: | cp ./nginx/objs/ngx_http_auth_jwt_module.so ./ - tar czf ngx_http_auth_jwt_module_libjwt_${{matrix.libjwt-version}}_nginx_${{matrix.nginx-version}}.tgz 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@v3 + - name: Upload Build Artifact + uses: actions/upload-artifact@v4 with: if-no-files-found: error - name: ngx_http_auth_jwt_module_libjwt_${{matrix.libjwt-version}}_nginx_${{matrix.nginx-version}}.tgz - path: ngx_http_auth_jwt_module_libjwt_${{matrix.libjwt-version}}_nginx_${{matrix.nginx-version}}.tgz + name: ${{steps.meta.outputs.artifact}} + path: ${{steps.meta.outputs.filename}} - update_releases_page: - name: Upload builds to Releases - if: github.event_name != 'pull_request' + release: + name: Create/Update Release needs: + - meta - build runs-on: ubuntu-latest permissions: contents: write steps: - - name: Set up variables + + - name: Set-up Variables id: vars run: | echo "date_now=$(date --rfc-3339=seconds)" >> "${GITHUB_OUTPUT}" - - name: Download build artifacts from previous jobs - uses: actions/download-artifact@v3 + - name: Download Artifacts + uses: actions/download-artifact@v4 with: path: artifacts - - name: Upload builds to Releases + - 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: - allowUpdates: true - artifactErrorsFailBuild: true - artifacts: artifacts/*/* + 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 to the issue tracker. + > Please report any bugs you find. - Build Date: `${{ steps.vars.outputs.date_now }}` - - Commit: ${{ github.sha }} - name: 'Development build: ${{ github.ref_name }}@${{ github.sha }}' + - Commit: `${{ github.sha }}` prerelease: true + allowUpdates: true removeArtifacts: true - tag: dev-build + artifactErrorsFailBuild: true + artifacts: artifacts/* diff --git a/README.md b/README.md index ba3dde1..0fb2ea8 100644 --- a/README.md +++ b/README.md @@ -12,21 +12,22 @@ This module depends on the [JWT C Library](https://github.com/benmcollins/libjwt ## Directives -This module requires several new `nginx.conf` directives, which can be specified at the `http`, `server`, or `location` levels. - -| 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_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. | +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. + +| 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. | ## Algorithms @@ -92,19 +93,19 @@ auth_jwt_validate_sub on; 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_request_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, then use `auth_jwt_extract_response_claims` instead. +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 Request Claims +### 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_request_claims sub; + auth_jwt_extract_var_claims sub; - return 301 /profile/$http_jwt_sub; + return 301 /profile/$jwt_claim_sub; } ``` @@ -191,41 +192,43 @@ Once you save your changes to `.vscode/c_cpp_properties.json`, you should see th ### Building and Testing -The `./scripts.sh` file contains multiple commands to make things easy: +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_nginx` | Starts the NGINX container. | -| `stop_nginx` | Stops the NGINX container. | +| `start` | Starts the NGINX container. | +| `stop` | Stops the NGINX container. | | `cp_bin` | Copies the compiled binaries out of the NGINX container. | -| `build_test_runner` | Builds the images used by the test stack (uses Docker compose). | -| `rebuild_test_runner` | Re-builds the images used by the test stack. | -| `test` | Runs `test.sh` against the NGINX container (uses Docker compose). | +| `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.sh build_module test +./scripts build_module +./scripts test ``` -To build the Docker images, module, start NGINX, and run the tests against, you can simply do: +To build the Docker images, module, start NGINX, and run the tests against it for all versions, you can simply do: ```shell -./scripts.sh all +./scripts all ``` -When you make a change to the module run `./scripts.sh build_module test` to build a fresh module and run the tests. Note that `rebuild_module` is not often needed as `build_module` hashes the module's source files which will cause a cache miss while building the container, causing the module to be rebuilt. +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.sh test` to run the tests. Similar to above, the test sources are hashed and the containers will be rebuilt as needed. +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.sh build_module` only differs from the official NGINX image in two ways: +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.sh 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. +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 @@ -235,20 +238,23 @@ If you'd like to persist logs across test runs, you can configure the log driver ```shell # need to rebuild the test runner with the proper log driver -LOG_DRIVER=journald ./scripts.sh rebuild_test_runner +export LOG_DRIVER=journald + +# rebuild the test images +./scripts rebuild_test # run the tests -./scripts.sh test +./scripts test -# check the logs -journalctl -eu docker CONTAINER_NAME=jwt-nginx-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.sh test +./scripts test # terminal 2 journalctl -fu docker CONTAINER_NAME=jwt-nginx-test diff --git a/resources/nginx.conf b/examples/nginx.conf similarity index 70% rename from resources/nginx.conf rename to examples/nginx.conf index 9b8feab..9795363 100644 --- a/resources/nginx.conf +++ b/examples/nginx.conf @@ -18,6 +18,15 @@ http { '"$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; diff --git a/nginx.dockerfile b/nginx.dockerfile index 21c7460..68c67f0 100644 --- a/nginx.dockerfile +++ b/nginx.dockerfile @@ -1,25 +1,42 @@ -ARG BASE_IMAGE +ARG BASE_IMAGE=${:?required} ARG NGINX_VERSION +ARG LIBJWT_VERSION - -FROM ${BASE_IMAGE} as ngx_http_auth_jwt_builder_base +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 <<` -apt-get update -apt-get install -y curl build-essential + 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 -FROM ngx_http_auth_jwt_builder_base as ngx_http_auth_jwt_builder_module -LABEL stage=ngx_http_auth_jwt_builder -ENV PATH "${PATH}:/etc/nginx" -ENV LD_LIBRARY_PATH=/usr/local/lib -ARG NGINX_VERSION +WORKDIR /root/build/libjansson RUN <<` set -e - apt-get install -y libjwt-dev libjwt0 libjansson-dev libjansson4 libpcre2-dev zlib1g-dev libpcre3-dev - mkdir -p /root/build/ngx-http-auth-jwt-module + 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/ @@ -30,6 +47,7 @@ RUN <<` 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 @@ -90,30 +108,46 @@ RUN <<` ${BUILD_FLAGS} # --with-openssl=/usr/local \ ` + RUN make modules RUN make install -WORKDIR /usr/lib64/nginx/modules -RUN cp /root/build/nginx/objs/ngx_http_auth_jwt_module.so . + +WORKDIR /usr/lib/nginx/modules +RUN mv /root/build/nginx/objs/ngx_http_auth_jwt_module.so . RUN rm -rf /root/build -RUN adduser --system --no-create-home --shell /bin/false --group --disabled-login nginx -RUN mkdir -p /var/cache/nginx /var/log/nginx -WORKDIR /etc/nginx -FROM ngx_http_auth_jwt_builder_module AS ngx_http_auth_jwt_nginx -LABEL maintainer="TeslaGov" email="developers@teslagov.com" -ARG NGINX_VERSION RUN <<` set -e - - apt-get update - apt-get install -y libjansson4 libjwt0 + 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/lib64/nginx/modules/ngx_http_auth_jwt_module.so; +load_module /usr/lib/nginx/modules/ngx_http_auth_jwt_module.so; worker_processes 1; @@ -125,12 +159,17 @@ 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"'; + 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; } ` -ENTRYPOINT ["nginx", "-g", "daemon off;"] + +WORKDIR /var/cache/nginx +RUN chown nginx:nginx . + +WORKDIR / +CMD ["nginx"] diff --git a/openssl.dockerfile b/openssl.dockerfile index 45140cc..9839d29 100644 --- a/openssl.dockerfile +++ b/openssl.dockerfile @@ -1,9 +1,10 @@ -ARG BASE_IMAGE +ARG BASE_IMAGE=debian:bookworm-slim FROM ${BASE_IMAGE} -ARG SRC_DIR=/tmp/openssl-src -ARG OUT_DIR=/usr/local/.openssl -ARG SSL_VERSION +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 @@ -13,8 +14,8 @@ RUN <<` ` WORKDIR ${SRC_DIR} RUN <<` - set -e - curl --silent -O https://www.openssl.org/source/openssl-${SSL_VERSION}.tar.gz + 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 @@ -30,8 +31,8 @@ RUN <<` ldconfig ln -sf ${OUT_DIR}/bin/openssl /usr/bin/openssl - ln -sf ${OUT_DIR}/lib64/libssl.so.3 /lib/x86_64-linux-gnu/libssl.so.3 - ln -sf ${OUT_DIR}/lib64/libcrypto.so.3 /lib/x86_64-linux-gnu/libcrypto.so.3 + 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 +RUN rm -rf ${SRC_DIR} \ No newline at end of file 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/scripts.sh b/scripts.sh deleted file mode 100755 index 421a4cb..0000000 --- a/scripts.sh +++ /dev/null @@ -1,240 +0,0 @@ -#!/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_11='3.0.11' -SSL_VERSION_3_2_1='3.2.1' -SSL_VERSIONS=(${SSL_VERSION_3_2_1}) -SSL_VERSION=${SSL_VERSION:-$SSL_VERSION_3_0_11} - -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_11]="bookworm-slim:openssl-${SSL_VERSION_3_0_11}" -SSL_IMAGE_MAP[$SSL_VERSION_3_2_1]="bookworm-slim:openssl-${SSL_VERSION_3_2_1}" - -# supported NGINX versions -- for binary distribution -NGINX_VERSION_LEGACY_1='1.20.2' -NGINX_VERSION_LEGACY_2='1.22.1' -NGINX_VERSION_STABLE='1.24.0' -NGINX_VERSION_MAINLINE='1.25.4' -NGINX_VERSIONS=(${NGINX_VERSION_LEGACY_1} ${NGINX_VERSION_LEGACY_2} ${NGINX_VERSION_STABLE} ${NGINX_VERSION_MAINLINE}) -NGINX_VERSION=${NGINX_VERSION:-${NGINX_VERSION_STABLE}} - -IMAGE_NAME=${IMAGE_NAME:-nginx-auth-jwt} -FULL_IMAGE_NAME=${ORG_NAME:-teslagov}/${IMAGE_NAME} - -TEST_CONTAINER_NAME_PREFIX="${IMAGE_NAME}-test" - -all() { - build_module - build_test - test_all -} - -verify_and_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 base image for SSL ${SSL_VERSION}...${NC}\n" - docker image build \ - --build-arg BASE_IMAGE=debian:${baseImage} \ - --build-arg SSL_VERSION=${SSL_VERSION} \ - -f openssl.dockerfile \ - -t ${image} . - fi -} - -build_module() { - local dockerArgs=${1:-} - local baseImage=${SSL_IMAGE_MAP[$SSL_VERSION]} - - verify_and_build_base_image - - printf "${MAGENTA}Building module for NGINX ${NGINX_VERSION}...${NC}\n" - docker image build \ - -f nginx.dockerfile \ - -t ${FULL_IMAGE_NAME}:${NGINX_VERSION} \ - --build-arg BASE_IMAGE=${baseImage} \ - --build-arg NGINX_VERSION=${NGINX_VERSION} \ - ${dockerArgs} . - - if [ "$?" -ne 0 ]; then - printf "${RED}✘ Build failed ${NC}\n" - else - printf "${GREEN}✔ Successfully built NGINX module ${NC}\n" - fi -} - -rebuild_module() { - clean_module - build_module --no-cache -} - -clean_module() { - docker rmi -f $(docker images --filter=label=stage=ngx_http_auth_jwt_builder --quiet) 2> /dev/null || true -} - -start_nginx() { - 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} >/dev/null -} - -stop_nginx() { - 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_nginx - 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/lib64/nginx/modules/ngx_http_auth_jwt_module.so \ - usr/lib/x86_64-linux-gnu/libjansson.so.* \ - usr/lib/x86_64-linux-gnu/libjwt.*" | tar -xf - -C ${destDir} &>/dev/null - - if [ $stopContainer ]; then - printf "${MAGENTA}Stopping NGINX container (${IMAGE_NAME})...${NC}\n" - stop_nginx - fi -} - -make_release() { - set -e - - local moduleVersion=${1} - - NGINX_VERSION=${2} - - printf "${MAGENTA}Making release for version ${moduleVersion} for NGINX ${NGINX_VERSION}...${NC}\n" - - rebuild_module - rebuild_test_runner - test - cp_bin - - mkdir -p release - tar -czvf release/ngx_http_auth_jwt_module_${moduleVersion}_nginx_${NGINX_VERSION}.tgz \ - README.md \ - -C bin/usr/lib64/nginx/modules ngx_http_auth_jwt_module.so > /dev/null -} - -# Create releases for the current mainline and stable version, as well as the 2 most recent "legacy" versions. -# See: https://nginx.org/en/download.html -make_releases() { - local moduleVersion=$(git describe --tags --abbrev=0) - - rm -rf release/* - - for v in ${NGINX_VERSIONS[@]}; do - make_release ${moduleVersion} ${v} - 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/docker-compose-test.yml 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 - test - done - done -} - -test() { - build_module - build_test - - printf "${MAGENTA}Running tests...${NC}\n" - docker compose \ - -p ${TEST_CONTAINER_NAME_PREFIX} \ - -f ./test/docker-compose-test.yml up \ - --no-start - - - trap 'docker compose -f ./test/docker-compose-test.yml down' 0 - - test_now -} - -test_now() { - nginxContainerName="${TEST_CONTAINER_NAME_PREFIX}-nginx" - runnerContainerName="${TEST_CONTAINER_NAME_PREFIX}-runner" - - 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 - fi - - docker start -a ${runnerContainerName} - - echo - echo "Tests were executed with the following options:" - echo " SSL Version: ${SSL_VERSION}" - echo " NGINX Version: ${NGINX_VERSION}" -} - -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 - for fn in "$@"; do - ${fn} - done -fi diff --git a/src/ngx_http_auth_jwt_module.c b/src/ngx_http_auth_jwt_module.c index 85a646d..59b84ac 100644 --- a/src/ngx_http_auth_jwt_module.c +++ b/src/ngx_http_auth_jwt_module.c @@ -31,6 +31,7 @@ typedef struct 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; @@ -38,18 +39,28 @@ typedef struct 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 free_jwt_and_redirect(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); @@ -106,6 +117,13 @@ static ngx_command_t auth_jwt_directives[] = { 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, @@ -194,6 +212,7 @@ static void *create_conf(ngx_conf_t *cf) 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; @@ -213,6 +232,7 @@ static char *merge_conf(ngx_conf_t *cf, void *parent, void *child) 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)); @@ -252,6 +272,108 @@ static char *merge_conf(ngx_conf_t *cf, void *parent, void *child) return NGX_CONF_OK; } +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; +} + +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; @@ -295,98 +417,170 @@ static char *merge_extract_response_claims(ngx_conf_t *cf, ngx_command_t *cmd, v return merge_extract_claims(cf, claims); } -static ngx_int_t handle_request(ngx_http_request_t *r) +static auth_jwt_ctx_t *get_or_init_jwt_module_ctx(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf) { - auth_jwt_conf_t *jwtcf = ngx_http_get_module_loc_conf(r, ngx_http_auth_jwt_module); + auth_jwt_ctx_t *ctx = ngx_http_get_module_ctx(r, ngx_http_auth_jwt_module); - if (!jwtcf->enabled) + if (ctx != NULL) { - return NGX_DECLINED; + return ctx; } else { - // pass through options requests without token authentication - if (r->method == NGX_HTTP_OPTIONS) + ctx = ngx_pcalloc(r->pool, sizeof(auth_jwt_ctx_t)); + + if (ctx == NULL) { - return NGX_DECLINED; + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "error allocating jwt module context"); + return ctx; } else { - 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"); - return redirect(r, jwtcf); - } - else + if (jwtcf->extract_var_claims != NULL) { - 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); + ctx->claim_values = ngx_array_create(r->pool, jwtcf->extract_var_claims->nelts, sizeof(ngx_str_t)); - 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"); - return redirect(r, jwtcf); - } - } - else if (algorithm.len == 5 && (ngx_strncmp(algorithm.data, "RS", 2) == 0 || ngx_strncmp(algorithm.data, "ES", 2) == 0)) + if (ctx->claim_values == NULL) { - 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); - return redirect(r, jwtcf); + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "error initializing jwt module context"); + return NULL; } + } - if (jwt_decode(&jwt, jwtPtr, key, keyLength) != 0) - { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "failed to parse JWT"); - return redirect(r, jwtcf); - } + ctx->validation_status = NGX_AGAIN; + ngx_http_set_ctx(r, ctx, ngx_http_auth_jwt_module); - if (validate_alg(jwtcf, jwt) != 0) - { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "invalid algorithm specified"); - return free_jwt_and_redirect(r, jwtcf, jwt); - } - else if (validate_exp(jwtcf, jwt) != 0) - { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the JWT has expired"); - return free_jwt_and_redirect(r, jwtcf, jwt); - } - else if (validate_sub(jwtcf, jwt) != 0) - { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the JWT does not contain a subject"); - return free_jwt_and_redirect(r, jwtcf, jwt); - } - else - { - extract_request_claims(r, jwtcf, jwt); - extract_response_claims(r, jwtcf, jwt); - jwt_free(jwt); + return ctx; + } + } +} - return NGX_OK; - } +// 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) +{ + 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; + } +} + +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; } } @@ -430,6 +624,37 @@ static int validate_sub(auth_jwt_conf_t *jwtcf, jwt_t *jwt) 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 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)) { if (claims != NULL && claims->nelts > 0) @@ -467,16 +692,6 @@ static void extract_response_claims(ngx_http_request_t *r, auth_jwt_conf_t *jwtc extract_claims(r, jwt, jwtcf->extract_response_claims, set_response_header); } -static ngx_int_t free_jwt_and_redirect(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf, jwt_t *jwt) -{ - if (jwt) - { - jwt_free(jwt); - } - - return redirect(r, jwtcf); -} - static ngx_int_t redirect(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf) { if (jwtcf->redirect) @@ -495,11 +710,16 @@ static ngx_int_t redirect(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf) if (r->method == NGX_HTTP_GET) { const int loginlen = jwtcf->loginurl.len; - const char *scheme = (r->connection->ssl) ? "https" : "http"; + 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 *request_uri_var = ngx_http_get_variable(r, &uri_variable_name, uri_variable_hash); + 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; @@ -507,12 +727,12 @@ static ngx_int_t redirect(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf) int return_url_idx; // get the URI - if (request_uri_var && !request_uri_var->not_found && request_uri_var->valid) + 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, request_uri_var->len); - uri.len = request_uri_var->len; - ngx_memcpy(uri.data, request_uri_var->data, request_uri_var->len); + 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 { @@ -520,31 +740,59 @@ static ngx_int_t redirect(ngx_http_request_t *r, auth_jwt_conf_t *jwtcf) 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); - r->headers_out.location->value.len = loginlen + strlen("?return_url=") + strlen(scheme) + strlen("://") + server.len + uri_escaped.len; + // 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); + 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=", strlen("?return_url=")); - return_url_idx += strlen("?return_url="); - ngx_memcpy(return_url + return_url_idx, scheme, strlen(scheme)); + 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, "://", strlen("://")); - return_url_idx += strlen("://"); - ngx_memcpy(return_url + return_url_idx, server.data, server.len); + 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; - ngx_memcpy(return_url + return_url_idx, uri_escaped.data, uri_escaped.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; } @@ -630,7 +878,7 @@ static char *get_jwt(ngx_http_request_t *r, ngx_str_t jwt_location) { static const char *BEARER_PREFIX = "Bearer "; - if (ngx_strncmp(jwtHeaderVal->value.data, BEARER_PREFIX, strlen(BEARER_PREFIX)) == 0) + if (ngx_strncasecmp(jwtHeaderVal->value.data, (u_char *)BEARER_PREFIX, strlen(BEARER_PREFIX)) == 0) { ngx_str_t jwtHeaderValWithoutBearer = jwtHeaderVal->value; diff --git a/test/docker-compose-test.yml b/test/docker-compose-test.yml index 3c0e9be..cc570c5 100644 --- a/test/docker-compose-test.yml +++ b/test/docker-compose-test.yml @@ -1,22 +1,25 @@ -version: '3.3' - services: nginx: - container_name: ${TEST_CONTAINER_NAME_PREFIX}-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} + BASE_IMAGE: ${FULL_IMAGE_NAME}:${NGINX_VERSION:?required} + platform: linux/amd64 logging: driver: ${LOG_DRIVER:-journald} runner: - container_name: ${TEST_CONTAINER_NAME_PREFIX}-runner + container_name: ${TEST_CONTAINER_NAME_PREFIX:?required}-runner build: context: . dockerfile: test-runner.dockerfile - + platforms: + - linux/amd64 + platform: linux/amd64 depends_on: - - nginx \ No newline at end of file + - nginx diff --git a/test/etc/nginx/conf.d/test.conf b/test/etc/nginx/conf.d/test.conf index 229d545..4e5d764 100644 --- a/test/etc/nginx/conf.d/test.conf +++ b/test/etc/nginx/conf.d/test.conf @@ -395,4 +395,54 @@ vXjq39xtcIBRTO1c2zs= 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/test-nginx.dockerfile b/test/test-nginx.dockerfile index c4a6104..f8c323f 100644 --- a/test/test-nginx.dockerfile +++ b/test/test-nginx.dockerfile @@ -1,13 +1,11 @@ ARG BASE_IMAGE -ARG PORT -ARG SSL_PORT -FROM ${BASE_IMAGE} as NGINX +FROM ${BASE_IMAGE:?required} ARG PORT ARG SSL_PORT + COPY etc/ /etc/ -RUN sed -i "s|%{PORT}|${PORT}|" /etc/nginx/conf.d/test.conf -RUN sed -i "s|%{SSL_PORT}|${SSL_PORT}|" /etc/nginx/conf.d/test.conf + COPY <<` /usr/share/nginx/html/index.html Test @@ -16,3 +14,6 @@ COPY <<` /usr/share/nginx/html/index.html ` + +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 index 0aca095..377c40e 100644 --- a/test/test-runner.dockerfile +++ b/test/test-runner.dockerfile @@ -1,16 +1,18 @@ ARG RUNNER_BASE_IMAGE -ARG PORT -ARG SSL_PORT -FROM ${RUNNER_BASE_IMAGE} +FROM ${RUNNER_BASE_IMAGE:?required} ARG PORT ARG SSL_PORT -ENV PORT=${PORT} -ENV SSL_PORT=${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 ${PORT} ${SSL_PORT} + +CMD ["./test.sh"] diff --git a/test/test.sh b/test/test.sh index f54e0de..c726a75 100755 --- a/test/test.sh +++ b/test/test.sh @@ -29,7 +29,7 @@ run_test () { local response= local testNum="${GRAY}${NUM_TESTS}${NC}\t" - while getopts "n:sp:r:c:x:" option; do + while getopts "n:asp:r:c:x:" option; do case $option in n) name=$OPTARG;; @@ -72,8 +72,8 @@ run_test () { fi fi - if [ "${okay}" == '1' ] && [ "${expectedResponseRegex}" != "" ] && ! [[ "${response}" =~ "${expectedResponseRegex}" ]]; then - printf "${RED}${name} -- regex not found in response\n\tPath: ${path}\n\tRegEx: ${expectedResponseRegex}" + 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 @@ -113,8 +113,8 @@ main() { -p '/' \ -c '200' - run_test -s \ - -n '[SSL] when auth disabled, should return 200' \ + run_test -n '[SSL] when auth disabled, should return 200' \ + -s \ -p '/' \ -c '200' @@ -143,6 +143,12 @@ main() { -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' @@ -273,7 +279,7 @@ main() { run_test -n 'extracts nested claim to request variable' \ -p '/secure/extract-claim/request/nested' \ - -r '< Test: username=hello.world' \ + -r '< Test: username=hello\.world' \ -x '--header "Authorization: Bearer ${JWT_HS256_VALID}"' run_test -n 'extracts single claim to response variable' \ @@ -313,9 +319,57 @@ main() { run_test -n 'extracts nested claim to response header' \ -p '/secure/extract-claim/response/nested' \ - -r '< JWT-username: hello.world' \ + -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