nginx
0:68478c1bddde Browse Files
First basic pass at JWT auth. Mostly just a fork of https://github.com/ficusio/openresty, with a few twists: * We've narrowed down some of the configuration options, and we're passing more headers (essentially exposing all the data in the JWT as headers). * We no longer automatically return a 401 unauthorized if the JWT verification fails; we just don't assign it the headers. The consuming service can decide whether or not they want to accept the request. * We automatically fail the verification of a JWT if the token has expired in the last minute (or shouldn't be used for the next minute). If the token has expired, we return a 401 that our clients can catch and use a refresh token automatically from. If the token can't be used for another minute, we quietly just refuse to add auth headers to the request.
Dockerfile README.md jwt-lib/basexx.lua jwt-lib/resty/hmac.lua jwt-lib/resty/jwt.lua nginx-jwt.lua
1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/Dockerfile Mon Jun 22 00:42:40 2015 -0400 1.3 @@ -0,0 +1,70 @@ 1.4 +FROM alpine:3.1 1.5 + 1.6 +ENV OPENRESTY_VERSION 1.7.10.1 1.7 +ENV OPENRESTY_PREFIX /opt/secondbit 1.8 +ENV NGINX_PREFIX /opt/secondbit/nginx 1.9 +ENV VAR_PREFIX /var/nginx 1.10 + 1.11 +# NginX prefix is automatically set by OpenResty to $OPENRESTY_PREFIX/nginx 1.12 +# look for $ngx_prefix in https://github.com/openresty/ngx_openresty/blob/master/util/configure 1.13 + 1.14 +ADD nginx-jwt.lua $OPENRESTY_PREFIX/lualib/nginx-jwt.lua 1.15 +ADD jwt-lib/basexx.lua $OPENRESTY_PREFIX/lualib/basexx.lua 1.16 +ADD jwt-lib/resty/hmac.lua $OPENRESTY_PREFIX/lualib/resty/hmac.lua 1.17 +ADD jwt-lib/resty/jwt.lua $OPENRESTY_PREFIX/lualib/resty/jwt.lua 1.18 + 1.19 +RUN echo "==> Installing dependencies..." \ 1.20 + && apk update \ 1.21 + && apk add make gcc musl-dev \ 1.22 + pcre-dev openssl-dev zlib-dev ncurses-dev readline-dev \ 1.23 + curl perl \ 1.24 + && mkdir -p /root/ngx_openresty \ 1.25 + && cd /root/ngx_openresty \ 1.26 + && echo "==> Downloading OpenResty..." \ 1.27 + && curl -sSL http://openresty.org/download/ngx_openresty-${OPENRESTY_VERSION}.tar.gz | tar -xvz \ 1.28 + && cd ngx_openresty-* \ 1.29 + && echo "==> Configuring OpenResty..." \ 1.30 + && readonly NPROC=$(grep -c ^processor /proc/cpuinfo 2>/dev/null || 1) \ 1.31 + && echo "using upto $NPROC threads" \ 1.32 + && ./configure \ 1.33 + --prefix=$OPENRESTY_PREFIX \ 1.34 + --http-client-body-temp-path=$VAR_PREFIX/client_body_temp \ 1.35 + --http-proxy-temp-path=$VAR_PREFIX/proxy_temp \ 1.36 + --http-log-path=$VAR_PREFIX/access.log \ 1.37 + --error-log-path=$VAR_PREFIX/error.log \ 1.38 + --pid-path=$VAR_PREFIX/nginx.pid \ 1.39 + --lock-path=$VAR_PREFIX/nginx.lock \ 1.40 + --with-luajit \ 1.41 + --with-pcre-jit \ 1.42 + --with-ipv6 \ 1.43 + --with-http_ssl_module \ 1.44 + --without-http_ssi_module \ 1.45 + --without-http_userid_module \ 1.46 + --without-http_fastcgi_module \ 1.47 + --without-http_uwsgi_module \ 1.48 + --without-http_scgi_module \ 1.49 + --without-http_memcached_module \ 1.50 + -j${NPROC} \ 1.51 + && echo "==> Building OpenResty..." \ 1.52 + && make -j${NPROC} \ 1.53 + && echo "==> Installing OpenResty..." \ 1.54 + && make install \ 1.55 + && echo "==> Finishing..." \ 1.56 + && ln -sf $NGINX_PREFIX/sbin/nginx /usr/local/bin/nginx \ 1.57 + && ln -sf $NGINX_PREFIX/sbin/nginx /usr/local/bin/openresty \ 1.58 + && ln -sf $OPENRESTY_PREFIX/bin/resty /usr/local/bin/resty \ 1.59 + && ln -sf $OPENRESTY_PREFIX/luajit/bin/luajit-* $OPENRESTY_PREFIX/luajit/bin/lua \ 1.60 + && ln -sf $OPENRESTY_PREFIX/luajit/bin/luajit-* /usr/local/bin/lua \ 1.61 + && apk del \ 1.62 + make gcc musl-dev pcre-dev openssl-dev zlib-dev ncurses-dev readline-dev curl perl \ 1.63 + && apk add \ 1.64 + libpcrecpp libpcre16 libpcre32 openssl libssl1.0 pcre libgcc libstdc++ \ 1.65 + && rm -rf /var/cache/apk/* \ 1.66 + && rm -rf /root/ngx_openresty 1.67 + 1.68 +WORKDIR $NGINX_PREFIX/ 1.69 + 1.70 +ONBUILD RUN rm -rf conf/* html/* 1.71 +ONBUILD COPY nginx $NGINX_PREFIX/ 1.72 + 1.73 +CMD ["nginx", "-g", "daemon off; error_log /dev/stderr info;"]
2.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 2.2 +++ b/README.md Mon Jun 22 00:42:40 2015 -0400 2.3 @@ -0,0 +1,71 @@ 2.4 +# OpenResty Docker image 2.5 + 2.6 +This is a fork of https://github.com/ficusio/openresty's alpine flavour. 2.7 + 2.8 +### Paths & config 2.9 + 2.10 +NginX is configured with `/opt/secondbit/nginx` [prefix path](http://nginx.org/en/docs/configure.html), which means that, by default, it loads configuration from `/opt/secondbit/nginx/conf/nginx.conf` file. The default HTML root path is `/opt/secondbit/nginx/html/`. 2.11 + 2.12 +OpenResty bundle includes several useful Lua modules located in `/opt/secondbit/lualib/` directory. This directory is already present in Lua package path, so you don't need to specify it in NginX `lua_package_path` directive. 2.13 + 2.14 +The Lua NginX module is built with LuaJIT 2.1, which is also available as stand-alone `lua` binary. 2.15 + 2.16 +### `ONBUILD` hook 2.17 + 2.18 +This image uses [`ONBUILD` hook](https://docs.docker.com/reference/builder/#onbuild) that automatically copies all files and subdirectories from the `nginx/` directory located at the root of Docker build context (i.e. next to your `Dockerfile`) into `/opt/secondbit/nginx/`. The minimal configuration needed to get NginX running is the following: 2.19 + 2.20 +``` 2.21 +project_root/ 2.22 + ├ nginx/ # all subdirs/files will be copied to /opt/secondbit/nginx/ 2.23 + | └ conf/ 2.24 + | └ nginx.conf # your NginX configuration file 2.25 + └ Dockerfile 2.26 +``` 2.27 + 2.28 +Dockerfile: 2.29 + 2.30 +``` 2.31 +FROM secondbit/nginx:latest 2.32 +EXPOSE 8080 2.33 +``` 2.34 + 2.35 +### Command-line parameters 2.36 + 2.37 +NginX is launched with the `nginx -g 'daemon off; error\_log /dev/stderr info; access\_log /dev/stdout;'` command. This means that you should not specify the `daemon` directive in your `nginx.conf` file, because it will lead to NginX config check error (duplicate directive). 2.38 + 2.39 +No-daemon mode is needed to allow host OS' service manager, like `systemd`, or [Docker itself](https://docs.docker.com/reference/commandline/cli/#restart-policies) to detect that NginX has exited and restart the container. Otherwise in-container service manager would be required. 2.40 + 2.41 +Error log and access log are redirected to `stderr` and `stdout` respectively to simplify debugging and log collection with tools like [progrium/logspout](https://github.com/progrium/logspout). 2.42 + 2.43 +If you wish to run it with different command-line options, you can add `CMD` directive to your Dockerfile. It will override the command provided in this image. Another option is to pass a command to `docker run` directly: 2.44 + 2.45 +```text 2.46 +$ docker run --rm -it --name test secondbit/nginx bash 2.47 +root@06823698db68:/opt/secondbit/nginx $ ls -l 2.48 +total 12 2.49 +drwxr-xr-x 2 root root 4096 Feb 1 14:48 conf 2.50 +drwxr-xr-x 2 root root 4096 Feb 1 14:48 html 2.51 +drwxr-xr-x 2 root root 4096 Feb 1 14:48 sbin 2.52 +``` 2.53 + 2.54 +### Usage during development 2.55 + 2.56 +To avoid rebuilding your Docker image after each modification of Lua code or NginX config, you can add a simple script that mounts config/content directories to appropriate locations and starts NginX: 2.57 + 2.58 +```bash 2.59 +#!/usr/bin/env bash 2.60 + 2.61 +exec docker run --rm -it \ 2.62 + --name my-app-dev \ 2.63 + -v "$(pwd)/nginx/conf":/opt/secondbit/nginx/conf \ 2.64 + -v "$(pwd)/nginx/lualib":/opt/secondbit/nginx/lualib \ 2.65 + -p 8080:8080 \ 2.66 + secondbit/nginx:latest "$@" 2.67 + 2.68 +# you may add more -v options to mount another directories, e.g. nginx/html/ 2.69 + 2.70 +# do not do -v "$(pwd)/nginx":/opt/secondbit/nginx because it will hide 2.71 +# the NginX binary located at /opt/secondbit/nginx/sbin/nginx 2.72 +``` 2.73 + 2.74 +Place it next to your `Dockerfile`, make executable and use during development. You may also want to temporarily disable [Lua code cache](https://github.com/openresty/lua-nginx-module#lua_code_cache) to allow testing code modifications without re-starting NginX.
3.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 3.2 +++ b/jwt-lib/basexx.lua Mon Jun 22 00:42:40 2015 -0400 3.3 @@ -0,0 +1,167 @@ 3.4 +------------------------------------------------------------------------------------------------------------------------ 3.5 +-- util functions 3.6 +------------------------------------------------------------------------------------------------------------------------ 3.7 + 3.8 +function divide_string( str, max, fillChar ) 3.9 + fillChar = fillChar or "" 3.10 + local result = {} 3.11 + 3.12 + local start = 1 3.13 + for i = 1, #str do 3.14 + if i % max == 0 then 3.15 + table.insert( result, str:sub( start, i ) ) 3.16 + start = i + 1 3.17 + elseif i == #str then 3.18 + table.insert( result, str:sub( start, i ) ) 3.19 + end 3.20 + end 3.21 + 3.22 + return result 3.23 +end 3.24 + 3.25 +function number_to_bit( num, length ) 3.26 + local bits = {} 3.27 + 3.28 + while num > 0 do 3.29 + local rest = math.fmod( num, 2 ) 3.30 + table.insert( bits, rest ) 3.31 + num = ( num - rest ) / 2 3.32 + end 3.33 + 3.34 + while #bits < length do 3.35 + table.insert( bits, "0" ) 3.36 + end 3.37 + 3.38 + return string.reverse( table.concat( bits ) ) 3.39 +end 3.40 + 3.41 +------------------------------------------------------------------------------------------------------------------------ 3.42 + 3.43 +local basexx = {} 3.44 + 3.45 +------------------------------------------------------------------------------------------------------------------------ 3.46 +-- base2(bitfield) decode and encode function 3.47 +------------------------------------------------------------------------------------------------------------------------ 3.48 + 3.49 +local bitMap = { o = "0", i = "1", l = "1" } 3.50 + 3.51 +function basexx.from_bit( str ) 3.52 + str = string.lower( str ) 3.53 + str = str:gsub( '[ilo]', function( c ) return bitMap[ c ] end ) 3.54 + return ( str:gsub( '........', function ( cc ) 3.55 + return string.char( tonumber( cc, 2 ) ) 3.56 + end ) ) 3.57 +end 3.58 + 3.59 +function basexx.to_bit( str ) 3.60 + return ( str:gsub( '.', function ( c ) 3.61 + local byte = string.byte( c ) 3.62 + local bits = {} 3.63 + for i = 1,8 do 3.64 + table.insert( bits, byte % 2 ) 3.65 + byte = math.floor( byte / 2 ) 3.66 + end 3.67 + return table.concat( bits ):reverse() 3.68 + end ) ) 3.69 +end 3.70 + 3.71 +------------------------------------------------------------------------------------------------------------------------ 3.72 +-- base16(hex) decode and encode function 3.73 +------------------------------------------------------------------------------------------------------------------------ 3.74 + 3.75 +function basexx.from_hex( str ) 3.76 + return ( str:gsub( '..', function ( cc ) 3.77 + return string.char( tonumber( cc, 16 ) ) 3.78 + end ) ) 3.79 +end 3.80 + 3.81 +function basexx.to_hex( str ) 3.82 + return ( str:gsub( '.', function ( c ) 3.83 + return string.format('%02X', string.byte( c ) ) 3.84 + end ) ) 3.85 +end 3.86 + 3.87 +------------------------------------------------------------------------------------------------------------------------ 3.88 +-- generic function to decode and encode base32/base64 3.89 +------------------------------------------------------------------------------------------------------------------------ 3.90 + 3.91 +local function from_basexx( str, alphabet, bits ) 3.92 + local result = {} 3.93 + for i = 1, #str do 3.94 + local c = string.sub( str, i, i ) 3.95 + if c ~= '=' then 3.96 + local index = string.find( alphabet, c ) 3.97 + table.insert( result, number_to_bit( index - 1, bits ) ) 3.98 + end 3.99 + end 3.100 + 3.101 + local value = table.concat( result ) 3.102 + local pad = #value % 8 3.103 + return basexx.from_bit( string.sub( value, 1, #value - pad ) ) 3.104 +end 3.105 + 3.106 +local function to_basexx( str, alphabet, bits, pad ) 3.107 + local bitString = basexx.to_bit( str ) 3.108 + 3.109 + local chunks = divide_string( bitString, bits ) 3.110 + local result = {} 3.111 + for key,value in ipairs( chunks ) do 3.112 + if ( #value < bits ) then 3.113 + value = value .. string.rep( '0', bits - #value ) 3.114 + end 3.115 + local pos = tonumber( value, 2 ) + 1 3.116 + table.insert( result, alphabet:sub( pos, pos ) ) 3.117 + end 3.118 + 3.119 + table.insert( result, pad ) 3.120 + return table.concat( result ) 3.121 +end 3.122 + 3.123 +------------------------------------------------------------------------------------------------------------------------ 3.124 +-- rfc 3548: http://www.rfc-editor.org/rfc/rfc3548.txt 3.125 +------------------------------------------------------------------------------------------------------------------------ 3.126 + 3.127 +local base32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" 3.128 + 3.129 +function basexx.from_base32( str ) 3.130 + return from_basexx( string.upper( str ), base32Alphabet, 5 ) 3.131 +end 3.132 + 3.133 +function basexx.to_base32( str ) 3.134 + return to_basexx( str, base32Alphabet, 5, ({ '', '======', '====', '===', '=' })[ #str % 5 + 1 ] ) 3.135 +end 3.136 + 3.137 +------------------------------------------------------------------------------------------------------------------------ 3.138 +-- crockford: http://www.crockford.com/wrmg/base32.html 3.139 +------------------------------------------------------------------------------------------------------------------------ 3.140 + 3.141 +local crockfordAlphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" 3.142 +local crockfordMap = { O = "0", I = "1", L = "1", U = "V" } 3.143 + 3.144 +function basexx.from_crockford( str ) 3.145 + str = string.upper( str ) 3.146 + str = str:gsub( '[ILOU]', function( c ) return crockfordMap[ c ] end ) 3.147 + return from_basexx( str, crockfordAlphabet, 5 ) 3.148 +end 3.149 + 3.150 +function basexx.to_crockford( str ) 3.151 + return to_basexx( str, crockfordAlphabet, 5, "" ) 3.152 +end 3.153 + 3.154 +------------------------------------------------------------------------------------------------------------------------ 3.155 +-- base64 decode and encode function 3.156 +------------------------------------------------------------------------------------------------------------------------ 3.157 + 3.158 +local base64Alphabet='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' 3.159 + 3.160 +function basexx.from_base64( str ) 3.161 + return from_basexx( str, base64Alphabet, 6 ) 3.162 +end 3.163 + 3.164 +function basexx.to_base64( str ) 3.165 + return to_basexx( str, base64Alphabet, 6, ({ '', '==', '=' })[ #str % 3 + 1 ] ) 3.166 +end 3.167 + 3.168 +------------------------------------------------------------------------------------------------------------------------ 3.169 + 3.170 +return basexx
4.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 4.2 +++ b/jwt-lib/resty/hmac.lua Mon Jun 22 00:42:40 2015 -0400 4.3 @@ -0,0 +1,135 @@ 4.4 + 4.5 +local str_util = require "resty.string" 4.6 +local ffi = require "ffi" 4.7 +local ffi_new = ffi.new 4.8 +local ffi_str = ffi.string 4.9 +local ffi_gc = ffi.gc 4.10 +local C = ffi.C 4.11 +local setmetatable = setmetatable 4.12 +local error = error 4.13 + 4.14 + 4.15 +local _M = { _VERSION = '0.01' } 4.16 + 4.17 +local mt = { __index = _M } 4.18 + 4.19 + 4.20 +ffi.cdef[[ 4.21 +typedef struct engine_st ENGINE; 4.22 +typedef struct evp_pkey_ctx_st EVP_PKEY_CTX; 4.23 +typedef struct env_md_ctx_st EVP_MD_CTX; 4.24 +typedef struct env_md_st EVP_MD; 4.25 + 4.26 +struct env_md_ctx_st 4.27 + { 4.28 + const EVP_MD *digest; 4.29 + ENGINE *engine; 4.30 + unsigned long flags; 4.31 + void *md_data; 4.32 + EVP_PKEY_CTX *pctx; 4.33 + int (*update)(EVP_MD_CTX *ctx,const void *data,size_t count); 4.34 + }; 4.35 + 4.36 +struct env_md_st 4.37 + { 4.38 + int type; 4.39 + int pkey_type; 4.40 + int md_size; 4.41 + unsigned long flags; 4.42 + int (*init)(EVP_MD_CTX *ctx); 4.43 + int (*update)(EVP_MD_CTX *ctx,const void *data,size_t count); 4.44 + int (*final)(EVP_MD_CTX *ctx,unsigned char *md); 4.45 + int (*copy)(EVP_MD_CTX *to,const EVP_MD_CTX *from); 4.46 + int (*cleanup)(EVP_MD_CTX *ctx); 4.47 + 4.48 + int (*sign)(int type, const unsigned char *m, unsigned int m_length, unsigned char *sigret, unsigned int *siglen, void *key); 4.49 + int (*verify)(int type, const unsigned char *m, unsigned int m_length, const unsigned char *sigbuf, unsigned int siglen, void *key); 4.50 + int required_pkey_type[5]; 4.51 + int block_size; 4.52 + int ctx_size; 4.53 + int (*md_ctrl)(EVP_MD_CTX *ctx, int cmd, int p1, void *p2); 4.54 + }; 4.55 + 4.56 +typedef struct hmac_ctx_st 4.57 + { 4.58 + const EVP_MD *md; 4.59 + EVP_MD_CTX md_ctx; 4.60 + EVP_MD_CTX i_ctx; 4.61 + EVP_MD_CTX o_ctx; 4.62 + unsigned int key_length; 4.63 + unsigned char key[128]; 4.64 + } HMAC_CTX; 4.65 + 4.66 +void HMAC_CTX_init(HMAC_CTX *ctx); 4.67 +void HMAC_CTX_cleanup(HMAC_CTX *ctx); 4.68 + 4.69 +int HMAC_Init_ex(HMAC_CTX *ctx, const void *key, int len,const EVP_MD *md, ENGINE *impl); 4.70 +int HMAC_Update(HMAC_CTX *ctx, const unsigned char *data, size_t len); 4.71 +int HMAC_Final(HMAC_CTX *ctx, unsigned char *md, unsigned int *len); 4.72 + 4.73 +const EVP_MD *EVP_md5(void); 4.74 +const EVP_MD *EVP_sha1(void); 4.75 +const EVP_MD *EVP_sha256(void); 4.76 +const EVP_MD *EVP_sha512(void); 4.77 +]] 4.78 + 4.79 +local buf = ffi_new("unsigned char[64]") 4.80 +local res_len = ffi_new("unsigned int[1]") 4.81 +local ctx_ptr_type = ffi.typeof("HMAC_CTX[1]") 4.82 +local hashes = { 4.83 + MD5 = C.EVP_md5(), 4.84 + SHA1 = C.EVP_sha1(), 4.85 + SHA256 = C.EVP_sha256(), 4.86 + SHA512 = C.EVP_sha512() 4.87 +} 4.88 + 4.89 + 4.90 +_M.ALGOS = hashes 4.91 + 4.92 + 4.93 +function _M.new(self, key, hash_algo) 4.94 + local ctx = ffi_new(ctx_ptr_type) 4.95 + 4.96 + C.HMAC_CTX_init(ctx) 4.97 + 4.98 + local _hash_algo = hash_algo or hashes.md5 4.99 + 4.100 + if C.HMAC_Init_ex(ctx, key, #key, _hash_algo, nil) == 0 then 4.101 + return nil 4.102 + end 4.103 + 4.104 + ffi_gc(ctx, C.HMAC_CTX_cleanup) 4.105 + 4.106 + return setmetatable({ _ctx = ctx }, mt) 4.107 +end 4.108 + 4.109 + 4.110 +function _M.update(self, s) 4.111 + return C.HMAC_Update(self._ctx, s, #s) == 1 4.112 +end 4.113 + 4.114 + 4.115 +function _M.final(self, s, hex_output) 4.116 + 4.117 + if s ~= nil then 4.118 + if C.HMAC_Update(self._ctx, s, #s) == 0 then 4.119 + return nil 4.120 + end 4.121 + end 4.122 + 4.123 + if C.HMAC_Final(self._ctx, buf, res_len) == 1 then 4.124 + if hex_output == true then 4.125 + return str_util.to_hex(ffi_str(buf, res_len[0])) 4.126 + end 4.127 + return ffi_str(buf, res_len[0]) 4.128 + end 4.129 + 4.130 + return nil 4.131 +end 4.132 + 4.133 + 4.134 +function _M.reset(self) 4.135 + return C.HMAC_Init_ex(self._ctx, nil, 0, nil, nil) == 1 4.136 +end 4.137 + 4.138 +return _M
5.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 5.2 +++ b/jwt-lib/resty/jwt.lua Mon Jun 22 00:42:40 2015 -0400 5.3 @@ -0,0 +1,122 @@ 5.4 +local cjson = require "cjson" 5.5 +local hmac = require "resty.hmac" 5.6 + 5.7 +local _M = {_VERSION="0.0.1"} 5.8 +local mt = {__index=_M} 5.9 + 5.10 + 5.11 +local function get_raw_part(part_name, jwt_obj) 5.12 + local raw_part = jwt_obj["raw_" .. part_name] 5.13 + if raw_part == nil then 5.14 + local part = jwt_obj[part_name] 5.15 + if part == nil then 5.16 + error({reason="missing part " .. part_name}) 5.17 + end 5.18 + raw_part = _M:jwt_encode(part) 5.19 + end 5.20 + return raw_part 5.21 +end 5.22 + 5.23 + 5.24 +local function parse(token_str, secret, issuer) 5.25 + local basic_jwt = {} 5.26 + local raw_header, raw_payload, signature = string.match( 5.27 + token_str, 5.28 + '([^%.]+)%.([^%.]+)%.([^%.]+)' 5.29 + ) 5.30 + local basic_jwt = { 5.31 + raw_header=raw_header, 5.32 + raw_payload=raw_payload, 5.33 + header=_M:jwt_decode(raw_header, true), 5.34 + payload=_M:jwt_decode(raw_payload, true), 5.35 + signature=signature 5.36 + } 5.37 + return basic_jwt 5.38 +end 5.39 + 5.40 + 5.41 +function _M.jwt_encode(self, ori) 5.42 + if type(ori) == "table" then 5.43 + ori = cjson.encode(ori) 5.44 + end 5.45 + return ngx.encode_base64(ori):gsub("+", "-"):gsub("/", "_"):gsub("=", "") 5.46 +end 5.47 + 5.48 + 5.49 +function _M.jwt_decode(self, b64_str, json_decode) 5.50 + local reminder = #b64_str % 4 5.51 + if reminder > 0 then 5.52 + b64_str = b64_str .. string.rep("=", 4 - reminder) 5.53 + end 5.54 + local data = ngx.decode_base64(b64_str) 5.55 + if json_decode then 5.56 + data = cjson.decode(data) 5.57 + end 5.58 + return data 5.59 +end 5.60 + 5.61 + 5.62 +function _M.sign(self, secret_key, jwt_obj) 5.63 + -- header typ check 5.64 + local typ = jwt_obj["header"]["typ"] 5.65 + if typ ~= "JWT" then 5.66 + error({reason="invalid typ: " .. typ}) 5.67 + end 5.68 + -- header alg check 5.69 + local alg = jwt_obj["header"]["alg"] 5.70 + local hash_alg = nil 5.71 + if alg == "HS256" then 5.72 + hash_alg = hmac.ALGOS.SHA256 5.73 + elseif alg == "HS512" then 5.74 + hash_alg = hmac.ALGOS.SHA512 5.75 + else 5.76 + error({reason="unsupported alg: " .. alg}) 5.77 + end 5.78 + -- assemble jwt parts 5.79 + local raw_header = get_raw_part("header", jwt_obj) 5.80 + local raw_payload = get_raw_part("payload", jwt_obj) 5.81 + 5.82 + local message =raw_header .. "." .. raw_payload 5.83 + -- cal signature 5.84 + local hmac_func = hmac:new(secret_key, hash_alg) 5.85 + local signature = _M:jwt_encode(hmac_func:final(message)) 5.86 + -- return full jwt string 5.87 + return message .. "." .. signature 5.88 +end 5.89 + 5.90 + 5.91 +function _M.verify(self, secret, jwt_str, leeway) 5.92 + local success, ret = pcall(parse, jwt_str) 5.93 + local jwt_obj = ret 5.94 + if not success then 5.95 + return {verified=false, reason=ret["reason"] or "invalid jwt string"} 5.96 + end 5.97 + 5.98 + jwt_obj["verified"] = false 5.99 + local success, ret = pcall(_M.sign, nil, secret, jwt_obj) 5.100 + if not success then 5.101 + -- syntax check 5.102 + jwt_obj["reason"] = ret["reason"] or "internal error" 5.103 + elseif jwt_str ~= ret then 5.104 + -- signature check 5.105 + jwt_obj["reason"] = "signature mismatch: " .. jwt_obj["signature"] 5.106 + elseif leeway ~= nil then 5.107 + local exp = jwt_obj["payload"]["exp"] 5.108 + local nbf = jwt_obj["payload"]["nbf"] 5.109 + local now = ngx.now() 5.110 + 5.111 + if type(exp) == "number" and exp < (now - leeway) then 5.112 + jwt_obj["reason"] = "jwt token expired at: " .. ngx.http_time(exp) 5.113 + elseif type(nbf) == "number" and nbf > (now + leeway) then 5.114 + jwt_obj["reason"] = "jwt token not valid until: " .. ngx.http_time(nbf) 5.115 + end 5.116 + end 5.117 + 5.118 + if jwt_obj["reason"] == nil then 5.119 + jwt_obj["verified"] = true 5.120 + jwt_obj["reason"] = "everything is awesome~ :p" 5.121 + end 5.122 + return jwt_obj 5.123 +end 5.124 + 5.125 +return _M
6.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 6.2 +++ b/nginx-jwt.lua Mon Jun 22 00:42:40 2015 -0400 6.3 @@ -0,0 +1,86 @@ 6.4 +local jwt = require "resty.jwt" 6.5 +local cjson = require "cjson" 6.6 +local basexx = require "basexx" 6.7 +local secret = os.getenv("JWT_SECRET") 6.8 + 6.9 +assert(secret ~= nil, "Environment variable JWT_SECRET not set") 6.10 + 6.11 +if os.getenv("JWT_SECRET_IS_BASE64_ENCODED") == 'true' then 6.12 + -- convert from URL-safe Base64 to Base64 6.13 + local r = #secret % 4 6.14 + if r == 2 then 6.15 + secret = secret .. "==" 6.16 + elseif r == 3 then 6.17 + secret = secret .. "=" 6.18 + end 6.19 + secret = string.gsub(secret, "-", "+") 6.20 + secret = string.gsub(secret, "_", "/") 6.21 + 6.22 + -- convert from Base64 to UTF-8 string 6.23 + secret = basexx.from_base64(secret) 6.24 +end 6.25 + 6.26 +local M = {} 6.27 + 6.28 +function M.auth(claim_specs) 6.29 + -- strip our headers to avoid spoofing 6.30 + ngx.req.clear_header("User-Id") 6.31 + ngx.req.clear_header("Scopes") 6.32 + ngx.req.clear_header("JWT-Scope") 6.33 + ngx.req.clear_header("JWT-Issuer") 6.34 + ngx.req.clear_header("JWT-Subject") 6.35 + ngx.req.clear_header("JWT-Expiration-Time") 6.36 + ngx.req.clear_header("JWT-Not-Before") 6.37 + ngx.req.clear_header("JWT-Issued-At") 6.38 + 6.39 + -- require Authorization request header 6.40 + local auth_header = ngx.var.http_Authorization 6.41 + 6.42 + if auth_header == nil then 6.43 + ngx.log(ngx.INFO, "No Authorization header") 6.44 + -- ngx.exit(ngx.HTTP_UNAUTHORIZED) 6.45 + return 6.46 + else 6.47 + ngx.log(ngx.INFO, "Authorization: " .. auth_header) 6.48 + 6.49 + -- require Bearer token 6.50 + local _, _, token = string.find(auth_header, "Bearer%s+(.+)") 6.51 + 6.52 + if token == nil then 6.53 + ngx.log(ngx.INFO, "Missing token") 6.54 + -- ngx.exit(ngx.HTTP_UNAUTHORIZED) 6.55 + return 6.56 + else 6.57 + ngx.log(ngx.INFO, "Token: " .. token) 6.58 + 6.59 + -- require valid JWT 6.60 + local jwt_obj = jwt:verify(secret, token, 60) 6.61 + if jwt_obj.verified == false then 6.62 + if string.find(jwt_obj.reason, "expired at") ~= nil then 6.63 + ngx.status = ngx.HTTP_UNAUTHORIZED 6.64 + ngx.say('{"error": "access_denied", "header": "authorization"}') 6.65 + return ngx.exit(ngx.HTTP_UNAUTHORIZED) 6.66 + else 6.67 + ngx.log(ngx.WARN, "Invalid token: ".. jwt_obj.reason) 6.68 + -- ngx.exit(ngx.HTTP_UNAUTHORIZED) 6.69 + end 6.70 + return 6.71 + else 6.72 + ngx.log(ngx.INFO, "JWT: " .. cjson.encode(jwt_obj)) 6.73 + 6.74 + -- write the headers 6.75 + ngx.req.set_header("User-Id", jwt_obj.payload.sub) 6.76 + ngx.req.set_header("Scopes", jwt_obj.payload.scope) 6.77 + ngx.req.set_header("JWT-Scope", jwt_obj.payload.scope) 6.78 + ngx.req.set_header("JWT-Issuer", jwt_obj.payload.iss) 6.79 + ngx.req.set_header("JWT-Subject", jwt_obj.payload.sub) 6.80 + ngx.req.set_header("JWT-Expiration-Time", jwt_obj.payload.exp) 6.81 + ngx.req.set_header("JWT-Not-Before", jwt_obj.payload.nbf) 6.82 + ngx.req.set_header("JWT-Issued-At", jwt_obj.payload.iat) 6.83 + return 6.84 + end 6.85 + end 6.86 + end 6.87 +end 6.88 + 6.89 +return M