nginx
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.
| 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 |