nginx

Paddy 2015-06-30 Parent:68478c1bddde

1:ac9c19126939 Go to Latest

nginx/jwt-lib/resty/jwt.lua

Make nginx kubernetes-ready. We had to update to use a ubuntu-based image to build nginx into, because (and I kid you not) alpine linux straight-up ignores your resolv.conf file, meaning any attempt to use it with kubernetes DNS is doomed to fail. Who thought this was a good idea? So we're using a bloated image instead. Oh well. We also are running a wrapper script instead of nginx directly, so we can inject the JWT_SECRET environment variable based on a kubernetes secret file. We define the secret file (using a placeholder secret, obvs) so that future-Paddy can remember what the hell it looks like, when he inevitably loses the file and needs to sin up a new cluster. Or whatever. Finally, we updated the token expiration error message to be in an errors array, as God (and our API conventions) intended.

History
paddy@0 1 local cjson = require "cjson"
paddy@0 2 local hmac = require "resty.hmac"
paddy@0 3
paddy@0 4 local _M = {_VERSION="0.0.1"}
paddy@0 5 local mt = {__index=_M}
paddy@0 6
paddy@0 7
paddy@0 8 local function get_raw_part(part_name, jwt_obj)
paddy@0 9 local raw_part = jwt_obj["raw_" .. part_name]
paddy@0 10 if raw_part == nil then
paddy@0 11 local part = jwt_obj[part_name]
paddy@0 12 if part == nil then
paddy@0 13 error({reason="missing part " .. part_name})
paddy@0 14 end
paddy@0 15 raw_part = _M:jwt_encode(part)
paddy@0 16 end
paddy@0 17 return raw_part
paddy@0 18 end
paddy@0 19
paddy@0 20
paddy@0 21 local function parse(token_str, secret, issuer)
paddy@0 22 local basic_jwt = {}
paddy@0 23 local raw_header, raw_payload, signature = string.match(
paddy@0 24 token_str,
paddy@0 25 '([^%.]+)%.([^%.]+)%.([^%.]+)'
paddy@0 26 )
paddy@0 27 local basic_jwt = {
paddy@0 28 raw_header=raw_header,
paddy@0 29 raw_payload=raw_payload,
paddy@0 30 header=_M:jwt_decode(raw_header, true),
paddy@0 31 payload=_M:jwt_decode(raw_payload, true),
paddy@0 32 signature=signature
paddy@0 33 }
paddy@0 34 return basic_jwt
paddy@0 35 end
paddy@0 36
paddy@0 37
paddy@0 38 function _M.jwt_encode(self, ori)
paddy@0 39 if type(ori) == "table" then
paddy@0 40 ori = cjson.encode(ori)
paddy@0 41 end
paddy@0 42 return ngx.encode_base64(ori):gsub("+", "-"):gsub("/", "_"):gsub("=", "")
paddy@0 43 end
paddy@0 44
paddy@0 45
paddy@0 46 function _M.jwt_decode(self, b64_str, json_decode)
paddy@0 47 local reminder = #b64_str % 4
paddy@0 48 if reminder > 0 then
paddy@0 49 b64_str = b64_str .. string.rep("=", 4 - reminder)
paddy@0 50 end
paddy@0 51 local data = ngx.decode_base64(b64_str)
paddy@0 52 if json_decode then
paddy@0 53 data = cjson.decode(data)
paddy@0 54 end
paddy@0 55 return data
paddy@0 56 end
paddy@0 57
paddy@0 58
paddy@0 59 function _M.sign(self, secret_key, jwt_obj)
paddy@0 60 -- header typ check
paddy@0 61 local typ = jwt_obj["header"]["typ"]
paddy@0 62 if typ ~= "JWT" then
paddy@0 63 error({reason="invalid typ: " .. typ})
paddy@0 64 end
paddy@0 65 -- header alg check
paddy@0 66 local alg = jwt_obj["header"]["alg"]
paddy@0 67 local hash_alg = nil
paddy@0 68 if alg == "HS256" then
paddy@0 69 hash_alg = hmac.ALGOS.SHA256
paddy@0 70 elseif alg == "HS512" then
paddy@0 71 hash_alg = hmac.ALGOS.SHA512
paddy@0 72 else
paddy@0 73 error({reason="unsupported alg: " .. alg})
paddy@0 74 end
paddy@0 75 -- assemble jwt parts
paddy@0 76 local raw_header = get_raw_part("header", jwt_obj)
paddy@0 77 local raw_payload = get_raw_part("payload", jwt_obj)
paddy@0 78
paddy@0 79 local message =raw_header .. "." .. raw_payload
paddy@0 80 -- cal signature
paddy@0 81 local hmac_func = hmac:new(secret_key, hash_alg)
paddy@0 82 local signature = _M:jwt_encode(hmac_func:final(message))
paddy@0 83 -- return full jwt string
paddy@0 84 return message .. "." .. signature
paddy@0 85 end
paddy@0 86
paddy@0 87
paddy@0 88 function _M.verify(self, secret, jwt_str, leeway)
paddy@0 89 local success, ret = pcall(parse, jwt_str)
paddy@0 90 local jwt_obj = ret
paddy@0 91 if not success then
paddy@0 92 return {verified=false, reason=ret["reason"] or "invalid jwt string"}
paddy@0 93 end
paddy@0 94
paddy@0 95 jwt_obj["verified"] = false
paddy@0 96 local success, ret = pcall(_M.sign, nil, secret, jwt_obj)
paddy@0 97 if not success then
paddy@0 98 -- syntax check
paddy@0 99 jwt_obj["reason"] = ret["reason"] or "internal error"
paddy@0 100 elseif jwt_str ~= ret then
paddy@0 101 -- signature check
paddy@0 102 jwt_obj["reason"] = "signature mismatch: " .. jwt_obj["signature"]
paddy@0 103 elseif leeway ~= nil then
paddy@0 104 local exp = jwt_obj["payload"]["exp"]
paddy@0 105 local nbf = jwt_obj["payload"]["nbf"]
paddy@0 106 local now = ngx.now()
paddy@0 107
paddy@0 108 if type(exp) == "number" and exp < (now - leeway) then
paddy@0 109 jwt_obj["reason"] = "jwt token expired at: " .. ngx.http_time(exp)
paddy@0 110 elseif type(nbf) == "number" and nbf > (now + leeway) then
paddy@0 111 jwt_obj["reason"] = "jwt token not valid until: " .. ngx.http_time(nbf)
paddy@0 112 end
paddy@0 113 end
paddy@0 114
paddy@0 115 if jwt_obj["reason"] == nil then
paddy@0 116 jwt_obj["verified"] = true
paddy@0 117 jwt_obj["reason"] = "everything is awesome~ :p"
paddy@0 118 end
paddy@0 119 return jwt_obj
paddy@0 120 end
paddy@0 121
paddy@0 122 return _M