nginx
nginx/jwt-lib/resty/jwt.lua
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.
1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/jwt-lib/resty/jwt.lua Mon Jun 22 00:42:40 2015 -0400 1.3 @@ -0,0 +1,122 @@ 1.4 +local cjson = require "cjson" 1.5 +local hmac = require "resty.hmac" 1.6 + 1.7 +local _M = {_VERSION="0.0.1"} 1.8 +local mt = {__index=_M} 1.9 + 1.10 + 1.11 +local function get_raw_part(part_name, jwt_obj) 1.12 + local raw_part = jwt_obj["raw_" .. part_name] 1.13 + if raw_part == nil then 1.14 + local part = jwt_obj[part_name] 1.15 + if part == nil then 1.16 + error({reason="missing part " .. part_name}) 1.17 + end 1.18 + raw_part = _M:jwt_encode(part) 1.19 + end 1.20 + return raw_part 1.21 +end 1.22 + 1.23 + 1.24 +local function parse(token_str, secret, issuer) 1.25 + local basic_jwt = {} 1.26 + local raw_header, raw_payload, signature = string.match( 1.27 + token_str, 1.28 + '([^%.]+)%.([^%.]+)%.([^%.]+)' 1.29 + ) 1.30 + local basic_jwt = { 1.31 + raw_header=raw_header, 1.32 + raw_payload=raw_payload, 1.33 + header=_M:jwt_decode(raw_header, true), 1.34 + payload=_M:jwt_decode(raw_payload, true), 1.35 + signature=signature 1.36 + } 1.37 + return basic_jwt 1.38 +end 1.39 + 1.40 + 1.41 +function _M.jwt_encode(self, ori) 1.42 + if type(ori) == "table" then 1.43 + ori = cjson.encode(ori) 1.44 + end 1.45 + return ngx.encode_base64(ori):gsub("+", "-"):gsub("/", "_"):gsub("=", "") 1.46 +end 1.47 + 1.48 + 1.49 +function _M.jwt_decode(self, b64_str, json_decode) 1.50 + local reminder = #b64_str % 4 1.51 + if reminder > 0 then 1.52 + b64_str = b64_str .. string.rep("=", 4 - reminder) 1.53 + end 1.54 + local data = ngx.decode_base64(b64_str) 1.55 + if json_decode then 1.56 + data = cjson.decode(data) 1.57 + end 1.58 + return data 1.59 +end 1.60 + 1.61 + 1.62 +function _M.sign(self, secret_key, jwt_obj) 1.63 + -- header typ check 1.64 + local typ = jwt_obj["header"]["typ"] 1.65 + if typ ~= "JWT" then 1.66 + error({reason="invalid typ: " .. typ}) 1.67 + end 1.68 + -- header alg check 1.69 + local alg = jwt_obj["header"]["alg"] 1.70 + local hash_alg = nil 1.71 + if alg == "HS256" then 1.72 + hash_alg = hmac.ALGOS.SHA256 1.73 + elseif alg == "HS512" then 1.74 + hash_alg = hmac.ALGOS.SHA512 1.75 + else 1.76 + error({reason="unsupported alg: " .. alg}) 1.77 + end 1.78 + -- assemble jwt parts 1.79 + local raw_header = get_raw_part("header", jwt_obj) 1.80 + local raw_payload = get_raw_part("payload", jwt_obj) 1.81 + 1.82 + local message =raw_header .. "." .. raw_payload 1.83 + -- cal signature 1.84 + local hmac_func = hmac:new(secret_key, hash_alg) 1.85 + local signature = _M:jwt_encode(hmac_func:final(message)) 1.86 + -- return full jwt string 1.87 + return message .. "." .. signature 1.88 +end 1.89 + 1.90 + 1.91 +function _M.verify(self, secret, jwt_str, leeway) 1.92 + local success, ret = pcall(parse, jwt_str) 1.93 + local jwt_obj = ret 1.94 + if not success then 1.95 + return {verified=false, reason=ret["reason"] or "invalid jwt string"} 1.96 + end 1.97 + 1.98 + jwt_obj["verified"] = false 1.99 + local success, ret = pcall(_M.sign, nil, secret, jwt_obj) 1.100 + if not success then 1.101 + -- syntax check 1.102 + jwt_obj["reason"] = ret["reason"] or "internal error" 1.103 + elseif jwt_str ~= ret then 1.104 + -- signature check 1.105 + jwt_obj["reason"] = "signature mismatch: " .. jwt_obj["signature"] 1.106 + elseif leeway ~= nil then 1.107 + local exp = jwt_obj["payload"]["exp"] 1.108 + local nbf = jwt_obj["payload"]["nbf"] 1.109 + local now = ngx.now() 1.110 + 1.111 + if type(exp) == "number" and exp < (now - leeway) then 1.112 + jwt_obj["reason"] = "jwt token expired at: " .. ngx.http_time(exp) 1.113 + elseif type(nbf) == "number" and nbf > (now + leeway) then 1.114 + jwt_obj["reason"] = "jwt token not valid until: " .. ngx.http_time(nbf) 1.115 + end 1.116 + end 1.117 + 1.118 + if jwt_obj["reason"] == nil then 1.119 + jwt_obj["verified"] = true 1.120 + jwt_obj["reason"] = "everything is awesome~ :p" 1.121 + end 1.122 + return jwt_obj 1.123 +end 1.124 + 1.125 +return _M