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/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 3c31403..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,138 +0,0 @@ -name: CI - -on: - push: - branches: - - 'master' - paths: - - src/** - pull_request: - branches: - - 'master' - paths: - - src/** - workflow_dispatch: - -jobs: - build: - name: "NGINX: ${{ matrix.nginx-version }}; libjwt: ${{ matrix.libjwt-version }}" - strategy: - matrix: - # Each nginx version to build 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 compilles against each version prior to a breaking change and the latest release - libjwt-version: ['1.12.0', '1.14.0', '1.15.3'] - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v3 - with: - path: 'ngx-http-auth-jwt-module' - - - name: Download jansson - uses: actions/checkout@v3 - with: - repository: 'akheron/jansson' - ref: 'v2.14' - path: 'jansson' - - - name: Build jansson - working-directory: ./jansson - run: | - cmake . -DJANSSON_BUILD_SHARED_LIBS=1 -DJANSSON_BUILD_DOCS=OFF && \ - make && \ - make check && \ - sudo make install - - - name: Download libjwt - uses: actions/checkout@v3 - with: - repository: 'benmcollins/libjwt' - ref: 'v${{matrix.libjwt-version}}' - path: 'libjwt' - - - name: Build libjwt - working-directory: ./libjwt - run: | - 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: Run configure - 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 --add-dynamic-module=../ngx-http-auth-jwt-module ${BUILD_FLAGS} - - - name: Run make - working-directory: ./nginx - run: make modules - - - 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 - - - name: Upload build artifact - uses: actions/upload-artifact@v3 - 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 - - update_releases_page: - name: Upload builds to Releases - if: github.event_name != 'pull_request' - needs: - - 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 build artifacts from previous jobs - uses: actions/download-artifact@v3 - with: - path: artifacts - - - name: Upload builds to Releases - uses: ncipollo/release-action@v1 - with: - allowUpdates: true - artifactErrorsFailBuild: true - artifacts: artifacts/*/* - 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. - - - Build Date: `${{ steps.vars.outputs.date_now }}` - - Commit: ${{ github.sha }} - name: 'Development build: ${{ github.ref_name }}@${{ github.sha }}' - prerelease: true - removeArtifacts: true - tag: dev-build 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/Dockerfile b/Dockerfile deleted file mode 100644 index 4f2db13..0000000 --- a/Dockerfile +++ /dev/null @@ -1,59 +0,0 @@ -ARG NGINX_VERSION -ARG SOURCE_HASH - - -FROM debian:bullseye-slim as ngx_http_auth_jwt_builder_base -LABEL stage=ngx_http_auth_jwt_builder -RUN <<` -apt-get update -apt-get install -y curl build-essential -` - - -FROM ngx_http_auth_jwt_builder_base as ngx_http_auth_jwt_builder_module -LABEL stage=ngx_http_auth_jwt_builder -ENV LD_LIBRARY_PATH=/usr/local/lib -ARG NGINX_VERSION -RUN <<` -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 -` -WORKDIR /root/build/ngx-http-auth-jwt-module -ARG SOURCE_HASH -RUN echo "Source Hash: ${SOURCE_HASH}" -ADD config ./ -ADD src/*.h src/*.c ./src/ -WORKDIR /root/build -RUN <<` -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 <<` -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 --with-compat --add-dynamic-module=../ngx-http-auth-jwt-module ${BUILD_FLAGS} -make modules -` - - -FROM nginx:${NGINX_VERSION} AS ngx_http_auth_jwt_builder_nginx -LABEL stage= -RUN rm /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh /etc/nginx/conf.d/default.conf -RUN <<` -apt-get update -apt-get -y install libjansson4 libjwt0 -cd /etc/nginx -sed -ri '/pid\s+\/var\/run\/nginx\.pid;$/a load_module \/usr\/lib64\/nginx\/modules\/ngx_http_auth_jwt_module\.so;' nginx.conf -` -LABEL maintainer="TeslaGov" email="developers@teslagov.com" -COPY --from=ngx_http_auth_jwt_builder_module /root/build/nginx/objs/ngx_http_auth_jwt_module.so /usr/lib64/nginx/modules/ 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 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/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 e3fb254..0000000 --- a/scripts.sh +++ /dev/null @@ -1,181 +0,0 @@ -#!/bin/bash -eu - -BLUE='\033[0;34m' -GREEN='\033[0;32m' -RED='\033[0;31m' -NC='\033[0m' - -# supported NGINX versions -- for binary distribution -NGINX_VERSION_MAINLINE='1.25.4' -NGINX_VERSION_STABLE='1.24.0' -NGINX_VERSION_LEGACY_1='1.22.1' -NGINX_VERSION_LEGACY_2='1.20.2' - -export ORG_NAME=${ORG_NAME:-teslagov} -export IMAGE_NAME=${IMAGE_NAME:-jwt-nginx} -export FULL_IMAGE_NAME=${ORG_NAME}/${IMAGE_NAME} -export CONTAINER_NAME_PREFIX=${CONTAINER_NAME_PREFIX:-jwt-nginx-test} -export NGINX_VERSION=${NGINX_VERSION:-${NGINX_VERSION_STABLE}} - -all() { - build_module - build_test_runner - test -} - -build_module() { - local dockerArgs=${1:-} - local sourceHash=$(get_hash config src/*) - - printf "${BLUE}Pulling images...${NC}\n" - docker image pull debian:bullseye-slim - docker image pull nginx:${NGINX_VERSION} - - printf "${BLUE}Building module for NGINX ${NGINX_VERSION}...${NC}\n" - docker image build -t ${FULL_IMAGE_NAME}:latest -t ${FULL_IMAGE_NAME}:${NGINX_VERSION} ${dockerArgs} \ - --build-arg NGINX_VERSION=${NGINX_VERSION} \ - --build-arg SOURCE_HASH=${sourceHash} . - - 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 "${BLUE}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 "${BLUE}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 "${BLUE}Stopping NGINX container (${IMAGE_NAME})...${NC}\n" - stop_nginx - fi -} - -make_release() { - set -e - - local moduleVersion=${1} - - NGINX_VERSION=${2} - - printf "${BLUE}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) - local nginxVersions=(${NGINX_VERSION_MAINLINE} ${NGINX_VERSION_STABLE} ${NGINX_VERSION_LEGACY_1} ${NGINX_VERSION_LEGACY_2}) - - rm -rf release/* - - for v in ${nginxVersions[@]}; do - make_release ${moduleVersion} ${v} - done -} - - -build_test_runner() { - local dockerArgs=${1:-} - local configHash=$(get_hash $(find test -type f -not -name 'test.sh' -not -name '*.yml' -not -name 'Dockerfile*')) - local sourceHash=$(get_hash test/test.sh) - local port=$(get_port) - - printf "${BLUE}Building test runner using port ${port}...${NC}\n" - docker compose -f ./test/docker-compose-test.yml build ${dockerArgs} \ - --build-arg CONFIG_HASH=${configHash}\ - --build-arg SOURCE_HASH=${sourceHash} \ - --build-arg PORT=${port} -} - -rebuild_test_runner() { - build_test_runner --no-cache -} - -test() { - build_test_runner - - printf "${BLUE}Running tests...${NC}\n" - docker compose -f ./test/docker-compose-test.yml up --no-start - docker start ${CONTAINER_NAME_PREFIX} - - if [ "$(docker container inspect -f '{{.State.Running}}' ${CONTAINER_NAME_PREFIX})" != "true" ]; then - printf "${RED}Failed to start NGINX test container. See logs below:\n" - docker logs ${CONTAINER_NAME_PREFIX} - printf "${NC}\n" - else - test_now - fi - - docker compose -f ./test/docker-compose-test.yml down -} - -test_now() { - docker start -a ${CONTAINER_NAME_PREFIX}-runner -} - -get_hash() { - sha1sum $@ | sed -E 's|\s+|:|' | tr '\n' ' ' | sha1sum | head -c 40 -} - -get_port() { - for p in $(seq 8000 8100); 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 edef14a..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; + ctx->claim_values = ngx_array_create(r->pool, jwtcf->extract_var_claims->nelts, sizeof(ngx_str_t)); - if (algorithm.len == 0 || (algorithm.len == 5 && ngx_strncmp(algorithm.data, "HS", 2) == 0)) + if (ctx->claim_values == NULL) { - 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"); - return redirect(r, jwtcf); - } - } - else if (algorithm.len == 5 && ngx_strncmp(algorithm.data, "RS", 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); - 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; } } @@ -394,7 +588,7 @@ static int validate_alg(auth_jwt_conf_t *jwtcf, jwt_t *jwt) { const jwt_alg_t alg = jwt_get_alg(jwt); - 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) + 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; } @@ -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,10 +878,10 @@ 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; - + jwtHeaderValWithoutBearer.data += strlen(BEARER_PREFIX); jwtHeaderValWithoutBearer.len -= strlen(BEARER_PREFIX); diff --git a/test/Dockerfile-test-nginx b/test/Dockerfile-test-nginx deleted file mode 100644 index b70ca9e..0000000 --- a/test/Dockerfile-test-nginx +++ /dev/null @@ -1,12 +0,0 @@ -ARG BASE_IMAGE -ARG CONFIG_HASH -ARG PORT - -FROM ${BASE_IMAGE} as NGINX -ARG CONFIG_HASH -ARG PORT -RUN echo "Config Hash: ${CONFIG_HASH}" -COPY /docker-entrypoint.d/* /docker-entrypoint.d/ -COPY /etc/nginx/conf.d/test.conf /etc/nginx/conf.d/test.conf -COPY /etc/nginx/rsa_key_2048-pub.pem /etc/nginx/rsa-key.conf -RUN sed -i "s|%{PORT}|${PORT}|" /etc/nginx/conf.d/test.conf diff --git a/test/Dockerfile-test-runner b/test/Dockerfile-test-runner deleted file mode 100644 index c8cbff2..0000000 --- a/test/Dockerfile-test-runner +++ /dev/null @@ -1,13 +0,0 @@ -ARG SOURCE_HASH -ARG PORT - -FROM alpine:3.7 AS test-base -RUN apk add curl bash - -FROM test-base AS test -ARG SOURCE_HASH -ARG PORT -ENV PORT=${PORT} -RUN echo "Source Hash: ${SOURCE_HASH}" -COPY test.sh . -CMD ./test.sh ${PORT} diff --git a/test/docker-compose-test.yml b/test/docker-compose-test.yml index eff2460..cc570c5 100644 --- a/test/docker-compose-test.yml +++ b/test/docker-compose-test.yml @@ -1,25 +1,25 @@ -version: '3.3' - services: nginx: - container_name: ${CONTAINER_NAME_PREFIX} + container_name: ${TEST_CONTAINER_NAME_PREFIX:?required}-nginx build: context: . - dockerfile: Dockerfile-test-nginx + dockerfile: test-nginx.dockerfile + platforms: + - linux/amd64 args: - BASE_IMAGE: ${FULL_IMAGE_NAME}:${NGINX_VERSION:-latest} - command: [nginx-debug, '-g', 'daemon off;'] + BASE_IMAGE: ${FULL_IMAGE_NAME}:${NGINX_VERSION:?required} + platform: linux/amd64 logging: driver: ${LOG_DRIVER:-journald} runner: - container_name: ${CONTAINER_NAME_PREFIX}-runner + container_name: ${TEST_CONTAINER_NAME_PREFIX:?required}-runner build: context: . - dockerfile: Dockerfile-test-runner - environment: - BASE_IMAGE: ${FULL_IMAGE_NAME}:${NGINX_VERSION:-latest} - + dockerfile: test-runner.dockerfile + platforms: + - linux/amd64 + platform: linux/amd64 depends_on: - - nginx \ No newline at end of file + - nginx diff --git a/test/docker-entrypoint.d/10-nginx-test.sh b/test/docker-entrypoint.d/10-nginx-test.sh deleted file mode 100755 index 0bf8791..0000000 --- a/test/docker-entrypoint.d/10-nginx-test.sh +++ /dev/null @@ -1 +0,0 @@ -nginx -t \ No newline at end of file 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 index 00c990b..4e5d764 100644 --- a/test/etc/nginx/conf.d/test.conf +++ b/test/etc/nginx/conf.d/test.conf @@ -3,7 +3,13 @@ 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"; @@ -72,6 +78,51 @@ server { 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; @@ -119,13 +170,55 @@ BwIDAQAB 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.conf"; + auth_jwt_keyfile_path "/etc/nginx/rsa_key_2048-pub.pem"; alias /usr/share/nginx/html/; try_files index.html =404; @@ -137,7 +230,7 @@ BwIDAQAB auth_jwt_location HEADER=Authorization; auth_jwt_algorithm RS384; auth_jwt_use_keyfile on; - auth_jwt_keyfile_path "/etc/nginx/rsa-key.conf"; + auth_jwt_keyfile_path "/etc/nginx/rsa_key_2048-pub.pem"; alias /usr/share/nginx/html/; try_files index.html =404; @@ -149,7 +242,43 @@ BwIDAQAB auth_jwt_location HEADER=Authorization; auth_jwt_algorithm RS512; auth_jwt_use_keyfile on; - auth_jwt_keyfile_path "/etc/nginx/rsa-key.conf"; + 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; @@ -266,5 +395,54 @@ BwIDAQAB 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/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/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 + +