nginx

Paddy 2015-06-22 Child:ac9c19126939

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