├── version.txt ├── test ├── .gitignore ├── .jshintrc ├── package.json ├── sign └── test_integration.js ├── hosts ├── backend │ ├── .gitignore │ ├── package.json │ ├── Dockerfile │ └── server.js └── proxy │ ├── .gitignore │ ├── default │ ├── Dockerfile │ └── nginx │ │ └── conf │ │ └── nginx.conf │ ├── config-claim_specs-not-table │ ├── Dockerfile │ └── nginx │ │ └── conf │ │ └── nginx.conf │ ├── config-unsupported-claim-spec-type │ ├── Dockerfile │ └── nginx │ │ └── conf │ │ └── nginx.conf │ ├── base64-secret │ ├── Dockerfile │ └── nginx │ │ └── conf │ │ └── nginx.conf │ └── Dockerfile ├── metadata.json ├── scripts ├── common.sh ├── stop_backend.sh ├── stop_proxy_container.sh ├── run_backend.sh ├── build_proxy_base_image.sh ├── run_proxy_container.sh └── build_deps.sh ├── Vagrantfile ├── .gitignore ├── provision-vagrant.sh ├── LICENSE ├── nginx-jwt.lua └── README.md /version.txt: -------------------------------------------------------------------------------- 1 | 1.0.1 2 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /hosts/backend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /hosts/proxy/.gitignore: -------------------------------------------------------------------------------- 1 | **/nginx/lua 2 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "unused": true, 4 | "strict": true, 5 | "curly": true 6 | } 7 | -------------------------------------------------------------------------------- /hosts/proxy/default/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM proxy-base-image 2 | 3 | ENV JWT_SECRET="JWTs are the best!" 4 | 5 | EXPOSE 80 6 | -------------------------------------------------------------------------------- /hosts/proxy/config-claim_specs-not-table/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM proxy-base-image 2 | 3 | ENV JWT_SECRET="JWTs are the best!" 4 | 5 | EXPOSE 80 6 | -------------------------------------------------------------------------------- /hosts/proxy/config-unsupported-claim-spec-type/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM proxy-base-image 2 | 3 | ENV JWT_SECRET="JWTs are the best!" 4 | 5 | EXPOSE 80 6 | -------------------------------------------------------------------------------- /metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "JWT Auth for Nginx", 3 | "type": "sdk", 4 | "tags": [ 5 | "nginx", 6 | "openresty", 7 | "jwt", 8 | "lua" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /hosts/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "express": "^4.12.3", 7 | "morgan": "^1.5.2" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /hosts/backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:0.12.2 2 | 3 | # Bundle app source 4 | COPY . /app 5 | # Install app dependencies 6 | RUN cd /app; npm install 7 | 8 | EXPOSE 5000 9 | CMD ["node", "/app/server.js"] 10 | -------------------------------------------------------------------------------- /hosts/proxy/base64-secret/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM proxy-base-image 2 | 3 | # secret is base-64 encoded and URL-safe 4 | ENV JWT_SECRET="VGhpcyBzZWNyZXQgaXMgc3RvcmVkIGJhc2UtNjQgZW5jb2RlZCBvbiB0aGUgcHJveHkgaG9zdA" 5 | ENV JWT_SECRET_IS_BASE64_ENCODED=true 6 | 7 | EXPOSE 80 8 | -------------------------------------------------------------------------------- /scripts/common.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o pipefail 4 | set -e 5 | 6 | script_dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) 7 | root_dir=$script_dir/.. 8 | hosts_base_dir=$root_dir/hosts 9 | proxy_base_dir=$hosts_base_dir/proxy 10 | 11 | cyan='\033[0;36m' 12 | blue='\033[0;34m' 13 | no_color='\033[0m' 14 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tests", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "test": "mocha ./test_*.js" 7 | }, 8 | "dependencies": { 9 | "chai": "^3.2.0", 10 | "jsonwebtoken": "^5.0.5", 11 | "mocha": "^2.3.2", 12 | "super-request": "^0.1.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /scripts/stop_backend.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o pipefail 4 | set -e 5 | 6 | 7 | script_dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) 8 | . $script_dir/common.sh 9 | 10 | echo -e "${cyan}Stopping the backend container and removing its image...${no_color}" 11 | docker rm -f backend &>/dev/null || true 12 | docker rmi -f backend-image &>/dev/null || true 13 | -------------------------------------------------------------------------------- /scripts/stop_proxy_container.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o pipefail 4 | set -e 5 | 6 | script_dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) 7 | . $script_dir/common.sh 8 | 9 | proxy_name=$1 10 | proxy_dir=$proxy_base_dir/$proxy_name 11 | 12 | echo -e "${cyan}Stopping the '$proxy_name' container and removing the image${no_color}" 13 | docker rm -f "proxy-$proxy_name" &>/dev/null || true 14 | docker rmi -f "proxy-$proxy_name-image" &>/dev/null || true 15 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure(2) do |config| 5 | config.vm.box = "ubuntu/trusty64" 6 | 7 | # copy over handy configuration from host, like Git settings and SSH key 8 | config.vm.provision "file", source: "~/.gitconfig", destination: ".gitconfig" 9 | config.vm.provision "file", source: "~/.ssh/id_rsa", destination: ".ssh/id_rsa" 10 | 11 | # run provisioning script 12 | config.vm.provision :shell, :path => "provision-vagrant.sh" 13 | end 14 | -------------------------------------------------------------------------------- /scripts/run_backend.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o pipefail 4 | set -e 5 | 6 | script_dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) 7 | . $script_dir/common.sh 8 | 9 | # make sure existing image/container is stopped/deleted 10 | $script_dir/stop_backend.sh 11 | 12 | echo -e "${cyan}Building backend image...${no_color}" 13 | docker build -t="backend-image" --force-rm $hosts_base_dir/backend 14 | echo -e "${cyan}Starting backend container...${no_color}" 15 | docker run --name backend -d backend-image 16 | -------------------------------------------------------------------------------- /hosts/proxy/base64-secret/nginx/conf/nginx.conf: -------------------------------------------------------------------------------- 1 | env JWT_SECRET; 2 | env JWT_SECRET_IS_BASE64_ENCODED; 3 | 4 | worker_processes 1; 5 | 6 | events { worker_connections 1024; } 7 | 8 | http { 9 | sendfile on; 10 | lua_package_path '/opt/openresty/nginx/lua/?.lua;;'; 11 | 12 | server { 13 | listen 80; 14 | 15 | location /secure { 16 | access_by_lua ' 17 | local jwt = require("nginx-jwt") 18 | jwt.auth() 19 | '; 20 | 21 | proxy_pass http://backend:5000/secure; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /hosts/proxy/config-claim_specs-not-table/nginx/conf/nginx.conf: -------------------------------------------------------------------------------- 1 | env JWT_SECRET; 2 | 3 | worker_processes 1; 4 | 5 | events { worker_connections 1024; } 6 | 7 | http { 8 | sendfile on; 9 | lua_package_path '/opt/openresty/nginx/lua/?.lua;;'; 10 | 11 | server { 12 | listen 80; 13 | 14 | location /secure/admin { 15 | access_by_lua ' 16 | local jwt = require("nginx-jwt") 17 | jwt.auth("this is not a table, bro") 18 | '; 19 | 20 | proxy_pass http://backend:5000/secure/admin; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Lua sources 2 | luac.out 3 | 4 | # luarocks build files 5 | *.src.rock 6 | *.zip 7 | *.tar.gz 8 | 9 | # Object files 10 | *.o 11 | *.os 12 | *.ko 13 | *.obj 14 | *.elf 15 | 16 | # Precompiled Headers 17 | *.gch 18 | *.pch 19 | 20 | # Libraries 21 | *.lib 22 | *.a 23 | *.la 24 | *.lo 25 | *.def 26 | *.exp 27 | 28 | # Shared objects (inc. Windows DLLs) 29 | *.dll 30 | *.so 31 | *.so.* 32 | *.dylib 33 | 34 | # Executables 35 | *.exe 36 | *.out 37 | *.app 38 | *.i*86 39 | *.x86_64 40 | *.hex 41 | 42 | # npm 43 | npm-debug.log 44 | 45 | # Vagrant 46 | .vagrant/ 47 | 48 | # Other 49 | *.lua.swp 50 | lib/ 51 | nginx-jwt.tar.gz 52 | -------------------------------------------------------------------------------- /hosts/proxy/config-unsupported-claim-spec-type/nginx/conf/nginx.conf: -------------------------------------------------------------------------------- 1 | env JWT_SECRET; 2 | 3 | worker_processes 1; 4 | 5 | events { worker_connections 1024; } 6 | 7 | http { 8 | sendfile on; 9 | lua_package_path '/opt/openresty/nginx/lua/?.lua;;'; 10 | 11 | server { 12 | listen 80; 13 | 14 | location /secure/admin { 15 | access_by_lua ' 16 | local jwt = require("nginx-jwt") 17 | jwt.auth({ 18 | aud={foo="this is not a pattern or a table"}, 19 | roles=function (val) return jwt.table_contains(val, "marketing") end 20 | }) 21 | '; 22 | 23 | proxy_pass http://backend:5000/secure/admin; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/sign: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var jwt = require('jsonwebtoken'); 6 | 7 | function usage() { 8 | console.log('usage: sign PAYLOAD_JSON SECRET'); 9 | } 10 | 11 | var payload = process.argv[2]; 12 | if (!payload) { 13 | console.error('Missing: payload argument'); 14 | usage(); 15 | process.exit(1); 16 | return; 17 | } 18 | try { 19 | payload = JSON.parse(payload); 20 | } 21 | catch (err) { 22 | console.error('Payload must be in JSON format'); 23 | usage(); 24 | process.exit(1); 25 | return; 26 | } 27 | console.log('Payload:', payload); 28 | 29 | var secret = process.argv[3]; 30 | if (!secret) { 31 | console.error('Missing: secret argument'); 32 | usage(); 33 | process.exit(1); 34 | return; 35 | } 36 | console.log('Secret:', secret); 37 | 38 | var token = jwt.sign(payload, secret); 39 | console.log('Token:', token); 40 | -------------------------------------------------------------------------------- /scripts/build_proxy_base_image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o pipefail 4 | set -e 5 | 6 | script_dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) 7 | . $script_dir/common.sh 8 | 9 | # use sha-1 digest of base image Dockerfle as its Docker image tag, so we build a new base image whenever it changes 10 | dockerfile_sha1=$(cat $proxy_base_dir/Dockerfile | openssl sha1 | sed 's/^.* //') 11 | echo -e "${cyan}Required Dockerfile SHA1:${no_color} $dockerfile_sha1" 12 | 13 | echo -e "${cyan}Building base proxy image, if necessary...${no_color}" 14 | image_exists=$(docker images | grep "proxy-base-image\s*$dockerfile_sha1") || true 15 | 16 | if [ -z "$image_exists" ]; then 17 | echo -e "${blue}Building image${no_color}" 18 | docker build -t="proxy-base-image:$dockerfile_sha1" --force-rm $proxy_base_dir 19 | else 20 | echo -e "${blue}Base image already exists${no_color}" 21 | fi 22 | -------------------------------------------------------------------------------- /scripts/run_proxy_container.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o pipefail 4 | set -e 5 | 6 | script_dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) 7 | . $script_dir/common.sh 8 | 9 | proxy_name=$1 10 | proxy_dir=$proxy_base_dir/$proxy_name 11 | 12 | # make sure existing image/container is stopped/deleted 13 | $script_dir/stop_proxy_container.sh $proxy_name 14 | 15 | echo -e "${cyan}Building container and image for the '$proxy_name' proxy (Nginx) host...${no_color}" 16 | 17 | echo -e "${blue}Deploying Lua scripts and depedencies${no_color}" 18 | rm -rf $proxy_dir/nginx/lua 19 | mkdir -p $proxy_dir/nginx/lua 20 | cp $root_dir/nginx-jwt.lua $proxy_dir/nginx/lua 21 | cp -r lib/* $proxy_dir/nginx/lua 22 | 23 | echo -e "${blue}Building the new image${no_color}" 24 | docker build -t="proxy-$proxy_name-image" --force-rm $proxy_dir 25 | docker run --name "proxy-$proxy_name" -d -p 80:80 --link backend:backend "proxy-$proxy_name-image" 26 | -------------------------------------------------------------------------------- /hosts/backend/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var morgan = require('morgan'); 3 | 4 | var app = express(); 5 | app.use(morgan('combined')); 6 | 7 | app.get('/', function (req, res) { 8 | res.send('Backend API root'); 9 | }); 10 | 11 | app.get('/secure', function (req, res) { 12 | console.log('Authorization header:', req.get('Authorization')); 13 | 14 | res.json({ 15 | message: 'This endpoint needs to be secure.' 16 | }); 17 | }); 18 | 19 | app.get('/secure/admin', function (req, res) { 20 | console.log('Authorization header:', req.get('Authorization')); 21 | 22 | res.json({ 23 | message: 'This endpoint needs to be secure for an admin.' 24 | }); 25 | }); 26 | 27 | var server = app.listen(5000, function () { 28 | var host = server.address().address; 29 | var port = server.address().port; 30 | 31 | console.log('App listening at http://%s:%s', host, port); 32 | }); 33 | -------------------------------------------------------------------------------- /hosts/proxy/default/nginx/conf/nginx.conf: -------------------------------------------------------------------------------- 1 | env JWT_SECRET; 2 | 3 | worker_processes 1; 4 | 5 | events { worker_connections 1024; } 6 | 7 | http { 8 | sendfile on; 9 | lua_package_path '/opt/openresty/nginx/lua/?.lua;;'; 10 | 11 | server { 12 | listen 80; 13 | 14 | location / { 15 | proxy_pass http://backend:5000/; 16 | } 17 | 18 | location /secure { 19 | access_by_lua ' 20 | local jwt = require("nginx-jwt") 21 | jwt.auth() 22 | '; 23 | 24 | proxy_pass http://backend:5000/secure; 25 | } 26 | 27 | location /secure/admin { 28 | access_by_lua ' 29 | local jwt = require("nginx-jwt") 30 | jwt.auth({ 31 | aud="^foo:", 32 | roles=function (val) return jwt.table_contains(val, "marketing") end 33 | }) 34 | '; 35 | 36 | proxy_pass http://backend:5000/secure/admin; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /provision-vagrant.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ -e "/etc/vagrant-provisioned" ]; 4 | then 5 | echo "Vagrant provisioning already completed. Skipping..." 6 | exit 0 7 | else 8 | echo "Starting Vagrant provisioning process..." 9 | fi 10 | 11 | host='nginx-jwt' 12 | # Change the hostname so we can easily identify what environment we're on: 13 | echo $host > /etc/hostname 14 | # Update /etc/hosts to match new hostname to avoid "Unable to resolve hostname" issue: 15 | echo '127.0.0.1 $host' >> /etc/hosts 16 | # Use hostname command so that the new hostname takes effect immediately without a restart: 17 | hostname $host 18 | 19 | # Install core components 20 | apt-get update 21 | apt-get install -y make g++ curl git vim nfs-common portmap build-essential libssl-dev 22 | 23 | # Install Node.js 24 | curl --silent --location https://deb.nodesource.com/setup_0.12 | sudo bash - 25 | apt-get install --yes nodejs 26 | 27 | # Install Docker 28 | curl -sSL https://get.docker.com/ubuntu | sh 29 | 30 | # Vim settings: 31 | echo 'syntax on' > /home/vagrant/.vimrc 32 | 33 | touch /etc/vagrant-provisioned 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Auth0, Inc. (http://auth0.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /scripts/build_deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o pipefail 4 | set -e 5 | 6 | script_dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) 7 | . $script_dir/common.sh 8 | 9 | echo -e "${cyan}Fetching Lua depedencies...${no_color}" 10 | load_dependency () { 11 | local target="$1" 12 | local user="$2" 13 | local repo="$3" 14 | local commit="$4" 15 | local required_sha1="$5" 16 | 17 | local actual_sha1=$(cat $target | openssl sha1 | sed 's/^.* //') 18 | 19 | if [ -e "$target" ] && [ "$required_sha1" == "$actual_sha1" ]; then 20 | echo -e "Dependency $target (with SHA-1 digest $required_sha1) already downloaded." 21 | else 22 | curl https://codeload.github.com/$user/$repo/tar.gz/$commit | tar -xz --strip 1 $repo-$commit/lib 23 | fi 24 | } 25 | 26 | load_dependency "lib/resty/jwt.lua" "SkyLothar" "lua-resty-jwt" "612dcf581b5dd2b4168bab67d017c5e23b32bf0a" "cca4f2ea1f49d7c12aecc46eb151cdf63c26294b" 27 | load_dependency "lib/resty/hmac.lua" "jkeys089" "lua-resty-hmac" "67bff3fd6b7ce4f898b4c3deec7a1f6050ff9fc9" "44dffa232bdf20e9cf13fb37c23df089e4ae1ee2" 28 | load_dependency "lib/basexx.lua" "aiq" "basexx" "514f46ceb9a8a867135856abf60aaacfd921d9b9" "da8efedf0d96a79a041eddfe45a6438ea4edf58b" 29 | -------------------------------------------------------------------------------- /hosts/proxy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:14.04.2 2 | 3 | RUN apt-get update \ 4 | && apt-get install -y --no-install-recommends \ 5 | curl perl make build-essential procps \ 6 | libreadline-dev libncurses5-dev libpcre3-dev libssl-dev \ 7 | && rm -rf /var/lib/apt/lists/* 8 | 9 | ENV OPENRESTY_VERSION 1.9.3.1 10 | ENV OPENRESTY_PREFIX /opt/openresty 11 | ENV NGINX_PREFIX /opt/openresty/nginx 12 | ENV VAR_PREFIX /var/nginx 13 | 14 | # NginX prefix is automatically set by OpenResty to $OPENRESTY_PREFIX/nginx 15 | # look for $ngx_prefix in https://github.com/openresty/ngx_openresty/blob/master/util/configure 16 | 17 | RUN cd /root \ 18 | && echo "==> Downloading OpenResty..." \ 19 | && curl -sSL http://openresty.org/download/ngx_openresty-${OPENRESTY_VERSION}.tar.gz | tar -xvz \ 20 | && echo "==> Configuring OpenResty..." \ 21 | && cd ngx_openresty-* \ 22 | && readonly NPROC=$(grep -c ^processor /proc/cpuinfo 2>/dev/null || 1) \ 23 | && echo "using upto $NPROC threads" \ 24 | && ./configure \ 25 | --prefix=$OPENRESTY_PREFIX \ 26 | --http-client-body-temp-path=$VAR_PREFIX/client_body_temp \ 27 | --http-proxy-temp-path=$VAR_PREFIX/proxy_temp \ 28 | --http-log-path=$VAR_PREFIX/access.log \ 29 | --error-log-path=$VAR_PREFIX/error.log \ 30 | --pid-path=$VAR_PREFIX/nginx.pid \ 31 | --lock-path=$VAR_PREFIX/nginx.lock \ 32 | --with-luajit \ 33 | --with-pcre-jit \ 34 | --with-ipv6 \ 35 | --with-http_ssl_module \ 36 | --without-http_ssi_module \ 37 | --without-http_userid_module \ 38 | --without-http_fastcgi_module \ 39 | --without-http_uwsgi_module \ 40 | --without-http_scgi_module \ 41 | --without-http_memcached_module \ 42 | -j${NPROC} \ 43 | && echo "==> Building OpenResty..." \ 44 | && make -j${NPROC} \ 45 | && echo "==> Installing OpenResty..." \ 46 | && make install \ 47 | && echo "==> Finishing..." \ 48 | && ln -sf $NGINX_PREFIX/sbin/nginx /usr/local/bin/nginx \ 49 | && ln -sf $NGINX_PREFIX/sbin/nginx /usr/local/bin/openresty \ 50 | && ln -sf $OPENRESTY_PREFIX/bin/resty /usr/local/bin/resty \ 51 | && ln -sf $OPENRESTY_PREFIX/luajit/bin/luajit-* $OPENRESTY_PREFIX/luajit/bin/lua \ 52 | && ln -sf $OPENRESTY_PREFIX/luajit/bin/luajit-* /usr/local/bin/lua \ 53 | && rm -rf /root/ngx_openresty* 54 | 55 | WORKDIR $NGINX_PREFIX/ 56 | 57 | ONBUILD RUN rm -rf conf/* html/* 58 | ONBUILD COPY nginx $NGINX_PREFIX/ 59 | 60 | CMD ["nginx", "-g", "daemon off; error_log /dev/stderr info;"] 61 | -------------------------------------------------------------------------------- /nginx-jwt.lua: -------------------------------------------------------------------------------- 1 | local jwt = require "resty.jwt" 2 | local cjson = require "cjson" 3 | local basexx = require "basexx" 4 | local secret = os.getenv("JWT_SECRET") 5 | 6 | assert(secret ~= nil, "Environment variable JWT_SECRET not set") 7 | 8 | if os.getenv("JWT_SECRET_IS_BASE64_ENCODED") == 'true' then 9 | -- convert from URL-safe Base64 to Base64 10 | local r = #secret % 4 11 | if r == 2 then 12 | secret = secret .. "==" 13 | elseif r == 3 then 14 | secret = secret .. "=" 15 | end 16 | secret = string.gsub(secret, "-", "+") 17 | secret = string.gsub(secret, "_", "/") 18 | 19 | -- convert from Base64 to UTF-8 string 20 | secret = basexx.from_base64(secret) 21 | end 22 | 23 | local M = {} 24 | 25 | function M.auth(claim_specs) 26 | -- require Authorization request header 27 | local auth_header = ngx.var.http_Authorization 28 | 29 | if auth_header == nil then 30 | ngx.log(ngx.WARN, "No Authorization header") 31 | ngx.exit(ngx.HTTP_UNAUTHORIZED) 32 | end 33 | 34 | ngx.log(ngx.INFO, "Authorization: " .. auth_header) 35 | 36 | -- require Bearer token 37 | local _, _, token = string.find(auth_header, "Bearer%s+(.+)") 38 | 39 | if token == nil then 40 | ngx.log(ngx.WARN, "Missing token") 41 | ngx.exit(ngx.HTTP_UNAUTHORIZED) 42 | end 43 | 44 | ngx.log(ngx.INFO, "Token: " .. token) 45 | 46 | -- require valid JWT 47 | local jwt_obj = jwt:verify(secret, token, 0) 48 | if jwt_obj.verified == false then 49 | ngx.log(ngx.WARN, "Invalid token: ".. jwt_obj.reason) 50 | ngx.exit(ngx.HTTP_UNAUTHORIZED) 51 | end 52 | 53 | ngx.log(ngx.INFO, "JWT: " .. cjson.encode(jwt_obj)) 54 | 55 | -- optionally require specific claims 56 | if claim_specs ~= nil then 57 | --TODO: test 58 | -- make sure they passed a Table 59 | if type(claim_specs) ~= 'table' then 60 | ngx.log(ngx.STDERR, "Configuration error: claim_specs arg must be a table") 61 | ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) 62 | end 63 | 64 | -- process each claim 65 | local blocking_claim = "" 66 | for claim, spec in pairs(claim_specs) do 67 | -- make sure token actually contains the claim 68 | local claim_value = jwt_obj.payload[claim] 69 | if claim_value == nil then 70 | blocking_claim = claim .. " (missing)" 71 | break 72 | end 73 | 74 | local spec_actions = { 75 | -- claim spec is a string (pattern) 76 | ["string"] = function (pattern, val) 77 | return string.match(val, pattern) ~= nil 78 | end, 79 | 80 | -- claim spec is a predicate function 81 | ["function"] = function (func, val) 82 | -- convert truthy to true/false 83 | if func(val) then 84 | return true 85 | else 86 | return false 87 | end 88 | end 89 | } 90 | 91 | local spec_action = spec_actions[type(spec)] 92 | 93 | -- make sure claim spec is a supported type 94 | -- TODO: test 95 | if spec_action == nil then 96 | ngx.log(ngx.STDERR, "Configuration error: claim_specs arg claim '" .. claim .. "' must be a string or a table") 97 | ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) 98 | end 99 | 100 | -- make sure token claim value satisfies the claim spec 101 | if not spec_action(spec, claim_value) then 102 | blocking_claim = claim 103 | break 104 | end 105 | end 106 | 107 | if blocking_claim ~= "" then 108 | ngx.log(ngx.WARN, "User did not satisfy claim: ".. blocking_claim) 109 | ngx.exit(ngx.HTTP_UNAUTHORIZED) 110 | end 111 | end 112 | 113 | -- write the X-Auth-UserId header 114 | ngx.header["X-Auth-UserId"] = jwt_obj.payload.sub 115 | end 116 | 117 | function M.table_contains(table, item) 118 | for _, value in pairs(table) do 119 | if value == item then return true end 120 | end 121 | return false 122 | end 123 | 124 | return M 125 | -------------------------------------------------------------------------------- /test/test_integration.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, before, after */ 2 | 3 | 'use strict'; 4 | 5 | var cp = require('child_process'); 6 | var request = require('super-request'); 7 | var jwt = require('jsonwebtoken'); 8 | var expect = require('chai').expect; 9 | var exec = require('child_process').exec; 10 | 11 | var url = 'http://' + process.env.PROXY_HOST; 12 | 13 | var secret, proxyName; 14 | 15 | function runCommand (command, done) { 16 | exec(command, function (err) { 17 | if (err) { return done(err); } 18 | 19 | // add a small delay so port bindings are ready 20 | setTimeout(done, 100); 21 | }); 22 | } 23 | 24 | function runProxyContainer (name, done) { 25 | console.log(' > Building and starting proxy container: ' + name); 26 | 27 | var command = 'cd ..; ./scripts/run_proxy_container.sh ' + name; 28 | runCommand(command, done); 29 | } 30 | 31 | function stopProxyContainer (name, done) { 32 | console.log(' > Stopping and deleting proxy container: ' + name); 33 | 34 | var command = 'cd ..; ./scripts/stop_proxy_container.sh ' + name; 35 | runCommand(command, done); 36 | } 37 | 38 | describe('proxy', function () { 39 | this.timeout(30000); 40 | 41 | before(function (done) { 42 | console.log(' > Building and starting backend container'); 43 | 44 | var command = 'cd ..; ./scripts/run_backend.sh'; 45 | runCommand(command, done); 46 | }); 47 | 48 | after(function (done) { 49 | console.log(' > Stopping and deleting backend container'); 50 | 51 | var command = 'cd ..; ./scripts/stop_backend.sh'; 52 | runCommand(command, done); 53 | }); 54 | 55 | describe("configured with normal secret", function () { 56 | before(function (done) { 57 | secret = 'JWTs are the best!'; 58 | 59 | proxyName = 'default'; 60 | runProxyContainer(proxyName, done); 61 | }); 62 | 63 | after(function (done) { 64 | stopProxyContainer(proxyName, done); 65 | }); 66 | 67 | describe("GET /", function () { 68 | it("should return 200 with expected proxied response", function () { 69 | return request(url) 70 | .get('/') 71 | .expect(200, "Backend API root") 72 | .end(); 73 | }); 74 | }); 75 | 76 | describe("GET /secure", function () { 77 | it("should return 401 when passing no JWT", function () { 78 | return request(url) 79 | .get('/secure') 80 | .expect(401) 81 | .end(); 82 | }); 83 | 84 | it("should return 401 when passing a bogus JWT", function () { 85 | return request(url) 86 | .get('/secure') 87 | .headers({'Authorization': 'Bearer not-a-valid-jwt'}) 88 | .expect(401) 89 | .end(); 90 | }); 91 | 92 | it("should return a 200 with the expected response header when a valid JWT is passed", function () { 93 | var token = jwt.sign( 94 | { sub: 'foo-user' }, 95 | secret 96 | ); 97 | 98 | return request(url) 99 | .get('/secure') 100 | .headers({'Authorization': 'Bearer ' + token}) 101 | .expect(200) 102 | .expect('Content-Type', /json/) 103 | .expect({ message: 'This endpoint needs to be secure.' }) 104 | .expect('X-Auth-UserId', 'foo-user') 105 | .end(); 106 | }); 107 | }); 108 | 109 | describe("GET /secure/admin", function () { 110 | it("should return 401 when an authenticated user is missing a required claim", function () { 111 | var token = jwt.sign( 112 | // roles claim missing 113 | { sub: 'foo-user', aud: 'foo1:bar' }, 114 | secret); 115 | 116 | return request(url) 117 | .get('/secure/admin') 118 | .headers({'Authorization': 'Bearer ' + token}) 119 | .expect(401) 120 | .end(); 121 | }); 122 | 123 | it("should return 401 when a claim of an authenticated user doesn't pass a 'pattern' claim spec", function () { 124 | var token = jwt.sign( 125 | // aud claim has incorrect value 126 | { sub: 'foo-user', aud: 'foo1:bar', roles: ["sales", "marketing"] }, 127 | secret); 128 | 129 | return request(url) 130 | .get('/secure/admin') 131 | .headers({'Authorization': 'Bearer ' + token}) 132 | .expect(401) 133 | .end(); 134 | }); 135 | 136 | it("should return 401 when a claim of an authenticated user doesn't pass a 'function' claim spec", function () { 137 | var token = jwt.sign( 138 | // roles claim is missing 'marketing' role 139 | { sub: 'foo-user', aud: 'foo:bar', roles: ["sales"] }, 140 | secret); 141 | 142 | return request(url) 143 | .get('/secure/admin') 144 | .headers({'Authorization': 'Bearer ' + token}) 145 | .expect(401) 146 | .end(); 147 | }); 148 | 149 | it("should return 200 when an authenticated user is also authorized by all claims", function () { 150 | var token = jwt.sign( 151 | // everything is good 152 | { sub: 'foo-user', aud: 'foo:bar', roles: ["sales", "marketing"] }, 153 | secret); 154 | 155 | return request(url) 156 | .get('/secure/admin') 157 | .headers({'Authorization': 'Bearer ' + token}) 158 | .expect(200) 159 | .expect('Content-Type', /json/) 160 | .expect({ message: 'This endpoint needs to be secure for an admin.' }) 161 | .expect('X-Auth-UserId', 'foo-user') 162 | .end(); 163 | }); 164 | }); 165 | }); 166 | 167 | describe("configured with URL-safe base-64 encoded secret", function () { 168 | before(function (done) { 169 | secret = 'This secret is stored base-64 encoded on the proxy host'; 170 | 171 | proxyName = 'base64-secret'; 172 | runProxyContainer(proxyName, done); 173 | }); 174 | 175 | after(function (done) { 176 | stopProxyContainer(proxyName, done); 177 | }); 178 | 179 | describe("GET /secure", function () { 180 | it("should return a 200 with the expected response header when a valid JWT is passed", function () { 181 | var token = jwt.sign( 182 | { sub: 'foo-user' }, 183 | secret); 184 | 185 | return request(url) 186 | .get('/secure') 187 | .headers({'Authorization': 'Bearer ' + token}) 188 | .expect(200) 189 | .expect('Content-Type', /json/) 190 | .expect({ message: 'This endpoint needs to be secure.' }) 191 | .expect('X-Auth-UserId', 'foo-user') 192 | .end(); 193 | }); 194 | }); 195 | }); 196 | 197 | describe("configured with claim_specs param that's not a table", function () { 198 | before(function (done) { 199 | secret = 'JWTs are the best!'; 200 | 201 | proxyName = 'config-claim_specs-not-table'; 202 | runProxyContainer(proxyName, done); 203 | }); 204 | 205 | after(function (done) { 206 | stopProxyContainer(proxyName, done); 207 | }); 208 | 209 | describe("GET /secure/admin", function () { 210 | it("should return a 500", function (done) { 211 | var token = jwt.sign( 212 | // everything is good 213 | { sub: 'foo-user', aud: 'foo:bar', roles: ["sales", "marketing"] }, 214 | secret); 215 | 216 | request(url) 217 | .get('/secure/admin') 218 | .headers({'Authorization': 'Bearer ' + token}) 219 | .expect(500) 220 | .end(function (err) { 221 | if (err) { done(err); } 222 | 223 | // check docker logs for expected config error 224 | cp.exec('docker logs proxy-config-claim_specs-not-table', function (err, stdout, stderr) { 225 | if (err) { done(err); } 226 | 227 | expect(stderr).to.have.string( 228 | "Configuration error: claim_specs arg must be a table"); 229 | 230 | done(); 231 | }); 232 | }); 233 | }); 234 | }); 235 | }); 236 | 237 | describe("configured with claim_specs param that contains a spec that's not a pattern (string) or table", function () { 238 | before(function (done) { 239 | secret = 'JWTs are the best!'; 240 | 241 | proxyName = 'config-unsupported-claim-spec-type'; 242 | runProxyContainer(proxyName, done); 243 | }); 244 | 245 | after(function (done) { 246 | stopProxyContainer(proxyName, done); 247 | }); 248 | 249 | describe("GET /secure/admin", function () { 250 | it("should return a 500", function (done) { 251 | var token = jwt.sign( 252 | // everything is good 253 | { sub: 'foo-user', aud: 'foo:bar', roles: ["sales", "marketing"] }, 254 | secret); 255 | 256 | request(url) 257 | .get('/secure/admin') 258 | .headers({'Authorization': 'Bearer ' + token}) 259 | .expect(500) 260 | .end(function (err) { 261 | if (err) { done(err); } 262 | 263 | // check docker logs for expected config error 264 | cp.exec('docker logs proxy-config-unsupported-claim-spec-type', function (err, stdout, stderr) { 265 | if (err) { done(err); } 266 | 267 | expect(stderr).to.have.string( 268 | "Configuration error: claim_specs arg claim 'aud' must be a string or a table"); 269 | 270 | done(); 271 | }); 272 | }); 273 | }); 274 | }); 275 | }); 276 | }); 277 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JWT Auth for Nginx 2 | 3 | **nginx-jwt** is a [Lua](http://www.lua.org/) script for the [Nginx](http://nginx.org/) server (running the [HttpLuaModule](http://wiki.nginx.org/HttpLuaModule)) that will allow you to use Nginx as a reverse proxy in front of your existing set of HTTP services and secure them (authentication/authorization) using a trusted [JSON Web Token (JWT)](http://jwt.io/) in the `Authorization` request header, having to make little or no changes to the backing services themselves. 4 | 5 | ## Contents 6 | 7 | - [Key Features](#key-features) 8 | - [Install](#install) 9 | - [Configuration](#configuration) 10 | - [Usage](#usage) 11 | - [API Reference](#api-reference) 12 | - [Tests](#tests) 13 | - [Packaging](#packaging) 14 | - [Issue Reporting](#issue-reporting) 15 | - [Contributors](#contributors) 16 | - [License](#license) 17 | 18 | ## Key Features 19 | 20 | * Secure an existing HTTP service (ex: REST API) using Nginx reverse-proxy and this script 21 | * Authenticate an HTTP request with the verified identity contained with in a JWT 22 | * Optionally, authorize the same request using helper functions for asserting required JWT claims 23 | 24 | ## Install 25 | 26 | > **IMPORTANT**: **nginx-jwt** is a Lua script that is designed to run on Nginx servers that have the [HttpLuaModule](http://wiki.nginx.org/HttpLuaModule) installed. But ultimately its dependencies require components available in the [OpenResty](http://openresty.org/) distribution of Nginx. Therefore, it is recommended that you use **OpenResty** as your Nginx server, and these instructions make that assumption. 27 | 28 | Install steps: 29 | 30 | 1. Download the latest archive package from [releases](https://github.com/auth0/nginx-jwt/releases). 31 | 1. Extract the archive and deploy its contents to a directory on your Nginx server. 32 | 1. Specify this directory's path using ngx_lua's [lua_package_path](https://github.com/openresty/lua-nginx-module#lua_package_path) directive: 33 | ```lua 34 | # nginx.conf: 35 | 36 | http { 37 | lua_package_path "/path/to/lua/scripts;;"; 38 | ... 39 | } 40 | ``` 41 | 42 | ## Configuration 43 | 44 | > At the moment, `nginx-jwt` only supports symmetric keys (`alg` = `hs256`), which is why you need to configure your server with the shared JWT secret below. 45 | 46 | 1. Export the `JWT_SECRET` environment variable on the Nginx host, setting it equal to your JWT secret. Then expose it to Nginx server: 47 | ```lua 48 | # nginx.conf: 49 | 50 | env JWT_SECRET; 51 | ``` 52 | 1. If your JWT secret is Base64 (URL-safe) encoded, export the `JWT_SECRET_IS_BASE64_ENCODED` environment variable on the Nginx host, setting it equal to `true`. Then expose it to Nginx server: 53 | ```lua 54 | # nginx.conf: 55 | 56 | env JWT_SECRET_IS_BASE64_ENCODED; 57 | ``` 58 | 59 | ## Usage 60 | 61 | Now we can start using the script in reverse-proxy scenarios to secure our backing service. This is done by using the [access_by_lua](https://github.com/openresty/lua-nginx-module#access_by_lua) directive to call the `nginx-jwt` script's [`auth()`](#auth) function before executing any [proxy_* directives](http://nginx.org/en/docs/http/ngx_http_proxy_module.html): 62 | 63 | ```lua 64 | # nginx.conf: 65 | 66 | server { 67 | location /secure_this { 68 | access_by_lua ' 69 | local jwt = require("nginx-jwt") 70 | jwt.auth() 71 | '; 72 | 73 | proxy_pass http://my-backend.com$uri; 74 | } 75 | } 76 | ``` 77 | 78 | If you attempt to cURL the above `/secure_this` endpoint, you're going to get a `401` response from Nginx since it requires a valid JWT to be passed: 79 | 80 | ```bash 81 | curl -i http://your-nginx-server/secure_this 82 | ``` 83 | 84 | ``` 85 | HTTP/1.1 401 Unauthorized 86 | Server: openresty/1.7.7.1 87 | Date: Sun, 03 May 2015 18:05:00 GMT 88 | Content-Type: text/html 89 | Content-Length: 200 90 | Connection: keep-alive 91 | 92 | 93 | 401 Authorization Required 94 | 95 |

401 Authorization Required

96 |
openresty/1.7.7.1
97 | 98 | 99 | ``` 100 | 101 | To create a valid JWT, we've included a handy tool that will generate one given a payload and a secret. The payload must be in JSON format and at a minimum should contain a `sub` (subject) element. The following command will generate a JWT with an arbitrary payload and the specific secret used by the proxy: 102 | 103 | ```bash 104 | test/sign '{"sub": "flynn"}' 'My JWT secret' 105 | ``` 106 | 107 | ``` 108 | Payload: { sub: 'flynn' } 109 | Secret: JWTs are the best! 110 | Token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJwZXRlIiwiaWF0IjoxNDMwNjc3NjYzfQ.Zt4qnQyljbqLvAN7BQSuu14z5PjKcPpZZY85hDFVN3E 111 | ``` 112 | 113 | You can then use the above `Token` (the JWT) and call the Nginx server's `/secure_this` endpoint again: 114 | 115 | ```bash 116 | curl -i http://your-nginx-server/secure_this -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJwZXRlIiwiaWF0IjoxNDMwNjc3NjYzfQ.Zt4qnQyljbqLvAN7BQSuu14z5PjKcPpZZY85hDFVN3E' 117 | ``` 118 | 119 | ``` 120 | HTTP/1.1 200 OK 121 | Server: openresty/1.7.7.1 122 | Date: Sun, 03 May 2015 18:34:18 GMT 123 | Content-Type: text/plain 124 | Content-Length: 47 125 | Connection: keep-alive 126 | X-Auth-UserId: flynn 127 | X-Powered-By: Express 128 | ETag: W/"2f-8fc49de2" 129 | 130 | The reverse-proxied response! 131 | ``` 132 | 133 | In this case the Nginx server has authorized the caller and performed a reverse proxy call to the backing service's endpoint. Notice too that the **nginx-jwt** script has tacked on an extra response header called `X-Auth-UserId` that contains the value passed in the JWT payload's subject. This is just for convenience, but it does help verify that the server does indeed know who you are. 134 | 135 | The `jwt.auth()` function used above can actually do a lot more. See the [API Reference](#api-reference) section for more details. 136 | 137 | ## API Reference 138 | 139 | ### auth 140 | 141 | Syntax: `auth([claim_specs])` 142 | 143 | Authenticates the current request, requiring a JWT bearer token in the `Authorization` request header. Verification uses the value set in the `JWT_SECRET` (and optionally `JWT_SECRET_IS_BASE64_ENCODED`) environment variables. 144 | 145 | If authentication succeeds, then by default the current request is authorized by virtue of a valid user identity. More specific authorization can be accomplished via the optional `claim_specs` parameter. If provided, it must be a Lua [Table](http://www.lua.org/pil/2.5.html) where each key is the name of a desired claim and each value is a [pattern](http://www.lua.org/pil/20.2.html) that can be used to test the actual value of the claim. If your claim value is more complex that what a pattern can handle, you can pass an anonymous function instead that has the signature `function (val)` and returns a truthy value (or just `true`) if `val` is a match. You can also use the [`table_contains`](#table_contains) helper function to easily check for an existing value in an array table. 146 | 147 | For example if we wanted to ensure that the JWT had an `aud` (Audience) claim value that started with `foo:` and a `roles` claim that contained a `marketing` role, then the `claim_specs` parameter might look like this: 148 | 149 | ```lua 150 | # nginx.conf: 151 | 152 | server { 153 | location /secure { 154 | access_by_lua ' 155 | local jwt = require("nginx-jwt") 156 | jwt.auth({ 157 | aud="^foo:", 158 | role=function (val) return jwt.table_contains(val, "marketing") end 159 | }) 160 | '; 161 | 162 | proxy_pass http://my-backend.com$uri; 163 | } 164 | } 165 | ``` 166 | and if our JWT's payload of claims looked something like this, the above `auth()` call would succeed: 167 | 168 | ```json 169 | { 170 | "aud": "foo:user", 171 | "roles": [ "sales", "marketing" ] 172 | } 173 | ``` 174 | 175 | **NOTE:** the **auth** function should be called within the [access_by_lua](https://github.com/openresty/lua-nginx-module#access_by_lua) or [access_by_lua_file](https://github.com/openresty/lua-nginx-module#access_by_lua_file) directive so that it can occur before the Nginx **content** [phase](http://wiki.nginx.org/Phases). 176 | 177 | ### table_contains 178 | 179 | Syntax: `table_contains(table, item)` 180 | 181 | A helper function that checks to see if `table` (a Lua [Table](http://www.lua.org/pil/2.5.html)) contains the specified `item`. If it does, the function returns `true`; otherwise `false`. This is particularly helpful for checking for a value in an array: 182 | 183 | ```lua 184 | array = { "foo", "bar" } 185 | table_contains(array, "foo") --> true 186 | ``` 187 | 188 | ## Tests 189 | 190 | The best way to develop and test the **nginx-jwt** script is to run it in a virtualized development environment. This allows you to run Ngnix separate from your host machine (i.e. your Mac) in a controlled execution environment. It also allows you to easily test the script with any combination of Nginx proxy host configurations and backing services that Nginx will reverse proxy to. 191 | 192 | This repo contains everything you need to do just that. It's set up to run Nginx as well as a simple backend server in individual [Docker](http://www.docker.com) containers. 193 | 194 | ### Prerequisites 195 | 196 | #### Mac OS 197 | 198 | 1. [Docker Toolbox](https://www.docker.com/toolbox) 199 | 1. [Node.js](https://nodejs.org/) 200 | 201 | > **IMPORTANT**: The test scripts expect your **Docker Toolbox** `docker-machine` VM name to be `default` 202 | 203 | #### Ubuntu 204 | 205 | 1. [Docker](https://docs.docker.com/installation/ubuntulinux/) 206 | 1. [Node.js](https://nodejs.org/) 207 | 208 | Besides being able to install Docker and run Docker directly in the host OS, the other different between Ubuntu (and more specifically Linux) and Mac OS is that all Docker commands need to be called using `sudo`. In the examples that follow, a helper script called `build` is used to perform all Docker commands and should therefore be prefixed with `sudo`, like this: 209 | 210 | ```bash 211 | sudo ./build run 212 | ``` 213 | 214 | #### Ubuntu on MacOS (via Vagrant) 215 | 216 | If your host OS is Mac OS but you'd like to test that the build scripts run on Ubuntu, you can use the provided Vagrant scripts to spin up an Ubuntu VM that has all the necessary tools installed. 217 | 218 | First, if you haven't already, install **Vagrant** either by [installing the package](http://www.vagrantup.com/downloads.html) or using [Homebrew](http://sourabhbajaj.com/mac-setup/Vagrant/README.html). 219 | 220 | Then in the repo directory, start the VM: 221 | 222 | ```bash 223 | vagrant up 224 | ``` 225 | 226 | And then SSH into it: 227 | 228 | ```bash 229 | vagrant ssh 230 | ``` 231 | 232 | Once in, you'll need to use git to clone this repo and `cd` into the project: 233 | 234 | ```bash 235 | git clone THIS_REPO_URL 236 | cd nginx-jwt 237 | ``` 238 | 239 | All other tools should be installed. And like with the [Ubuntu](#ubuntu) host OS, you'll need to prefix all calls to the `build` script with `sudo`, like this: 240 | 241 | ```bash 242 | sudo ./build run 243 | ``` 244 | 245 | ### Build and run the default containers 246 | 247 | If you just want to see the **nginx-jwt** script in action, you can run the [`backend`](hosts/backend) container and the [`default`](hosts/proxy/default) proxy (Nginx) container: 248 | 249 | ```bash 250 | ./build run 251 | ``` 252 | 253 | > **NOTE**: On the first run, the above script may take several minutes to download and build all the base Docker images, so go grab a fresh cup of coffee. Successive runs are much faster. 254 | 255 | You can then run cURL commands against the endpoints exposed by the backend through Nginx. The root URL of the proxy is reported back by the script when it is finished. It will look something like this: 256 | 257 | ``` 258 | ... 259 | Proxy: 260 | curl http://192.168.59.103 261 | ``` 262 | 263 | Notice the proxy container (which is running in the Docker Machine VM) is listening on port 80. The actual backend container is not directly accessible via the VM. All calls are configured to reverse-proxy through the Nginx host and the connection between the two is done via [docker container linking](https://docs.docker.com/userguide/dockerlinks/). 264 | 265 | If you issue the above cURL command, you'll hit the [proxy's root (`/`) endpoint](hosts/proxy/default/nginx/conf/nginx.conf#L14), which simply reverse-proxies to the [non-secure backend endpoint](hosts/backend/server.js#L7), which doesn't require any authentication: 266 | 267 | ```bash 268 | curl -i http://192.168.59.103 269 | ``` 270 | 271 | ``` 272 | HTTP/1.1 200 OK 273 | Server: openresty/1.7.7.1 274 | Date: Sun, 03 May 2015 18:05:10 GMT 275 | Content-Type: text/html; charset=utf-8 276 | Content-Length: 16 277 | Connection: keep-alive 278 | X-Powered-By: Express 279 | ETag: W/"10-574c3064" 280 | 281 | Backend API root 282 | ``` 283 | 284 | However, if you attempt to cURL the [proxy's `/secure` endpoint](hosts/proxy/default/nginx/conf/nginx.conf#L18), you're going to get a `401` response from Nginx since it requires a valid JWT: 285 | 286 | ```bash 287 | curl -i http://192.168.59.103/secure 288 | ``` 289 | 290 | ``` 291 | HTTP/1.1 401 Unauthorized 292 | Server: openresty/1.7.7.1 293 | Date: Sun, 03 May 2015 18:05:00 GMT 294 | Content-Type: text/html 295 | Content-Length: 200 296 | Connection: keep-alive 297 | 298 | 299 | 401 Authorization Required 300 | 301 |

401 Authorization Required

302 |
openresty/1.7.7.1
303 | 304 | 305 | ``` 306 | 307 | Just like we showed in the [Usage](#usage) section, we can use the included `sign` tool to generate a JWT and call the Nginx proxy again, this time with a `200` response: 308 | 309 | ```bash 310 | test/sign '{"sub": "flynn"}' 'JWTs are the best!' 311 | ``` 312 | 313 | ``` 314 | Payload: { sub: 'flynn' } 315 | Secret: JWTs are the best! 316 | Token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJwZXRlIiwiaWF0IjoxNDMwNjc3NjYzfQ.Zt4qnQyljbqLvAN7BQSuu14z5PjKcPpZZY85hDFVN3E 317 | ``` 318 | 319 | ```bash 320 | curl -i http://192.168.59.103/secure -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJwZXRlIiwiaWF0IjoxNDMwNjc3NjYzfQ.Zt4qnQyljbqLvAN7BQSuu14z5PjKcPpZZY85hDFVN3E' 321 | ``` 322 | 323 | ``` 324 | HTTP/1.1 200 OK 325 | Server: openresty/1.7.7.1 326 | Date: Sun, 03 May 2015 18:34:18 GMT 327 | Content-Type: application/json; charset=utf-8 328 | Content-Length: 47 329 | Connection: keep-alive 330 | X-Auth-UserId: flynn 331 | X-Powered-By: Express 332 | ETag: W/"2f-8fc49de2" 333 | 334 | {"message":"This endpoint needs to be secure."} 335 | ``` 336 | 337 | The proxy exposes other endpoints, which have different JWT requirements. To see them all, take a look at the default proxy's [`nginx.conf`](hosts/proxy/default/nginx/conf/nginx.conf) file. 338 | 339 | If you want to run the script with one of the [other proxy containers](hosts/proxy), simply pass the name of the desired container. Example: 340 | 341 | ```bash 342 | ./build run base64-secret 343 | ``` 344 | 345 | ### Build the containers and run integration tests 346 | 347 | This script is similar to `run` except it executes all the [integration tests](test/test_integration.js), which end up building and running additional proxy containers to simulate different scenarios. 348 | 349 | ```bash 350 | ./build tests 351 | ``` 352 | 353 | Use this script while developing new features. 354 | 355 | ### Clean everything up 356 | 357 | If you need to simply stop/delete all running Docker containers and remove their associated images, use this command: 358 | 359 | ```bash 360 | ./build clean 361 | ``` 362 | 363 | ### Updating dependencies 364 | 365 | It's always nice to keep dependencies up to date. This library (and the tools used to test it) has three sources of dependencies that should be maintained: Lua dependencies, test script Node.js dependencies, and updates to the proxy base Docker image. 366 | 367 | #### Lua dependencies 368 | 369 | These are the Lua scripts that [this library](nginx-jwt.lua) uses. They are maintained in the [`build_deps.sh`](scripts/build_deps.sh) bash script. 370 | 371 | Since these dependencies don't have any built-in versioning (like npm), we download a specific GitHub commit instead. We also check that a previously downloaded script is current by examining its SHA-1 digest hash. All this is done via the included `load_dependency` bash function. 372 | 373 | If a Lua dependency needs to be updated, find its associated `load_dependency` function call and update its GitHub `commit` and `sha1` parameter values. You can generate the required SHA-1 digest of a new script file using this command: 374 | 375 | ```bash 376 | openssh sha1 NEW_SCRIPT 377 | ``` 378 | 379 | To add a new dependency simply add a new `load_dependency` command to the script. 380 | 381 | #### Test script Node.js dependencies 382 | 383 | All Node.js dependencies (npm packages) for tests are maintained in this [`package.json`](test/package.json) file and should be updated as needed using the `npm` command. 384 | 385 | #### Proxy base Docker image 386 | 387 | The proxy base Docker image may need to be updated periodically, usually to just rev the version of OpenResty that its using. This can be done by modifying the image's [`Dockerfile`](hosts/proxy/Dockerfile). Any change to this file will automatically result in new image builds when the `build` script is run. 388 | 389 | ## Packaging 390 | 391 | When a new version of the script needs to be released, the following should be done: 392 | 393 | > **NOTE**: These steps can only performed by GitHub users with commit access to the project. 394 | 395 | 1. Increment the [Semver](http://semver.org/) version in the [`version.txt`](version.txt) file as needed. 396 | 1. Create a new git tag with the same version value (prefiexed with `v`): 397 | 398 | ```bash 399 | git tag v$(cat version.txt) 400 | ``` 401 | 402 | 1. Push the tag to GitHub. 403 | 1. Create a new GitHub release in [releases](https://github.com/auth0/nginx-jwt/releases) that's associated with the above tag. 404 | 1. Run the following command to create a release package archive and then upload it to the release created above: 405 | 406 | ```bash 407 | ./build package 408 | ``` 409 | 410 | ## Issue Reporting 411 | 412 | If you have found a bug or if you have a feature request, please report them at this repository issues section. Please do not report security vulnerabilities on the public GitHub issue tracker. The [Responsible Disclosure Program](https://auth0.com/whitehat) details the procedure for disclosing security issues. 413 | 414 | ## Contributors 415 | 416 | Check them out [here](https://github.com/auth0/nginx-jwt/graphs/contributors). 417 | 418 | ## License 419 | 420 | This project is licensed under the MIT license. See the [LICENSE](LICENSE) file for more info. 421 | --------------------------------------------------------------------------------