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 local cjson = require "cjson"
2 local hmac = require "resty.hmac"
4 local _M = {_VERSION="0.0.1"}
5 local mt = {__index=_M}
8 local function get_raw_part(part_name, jwt_obj)
9 local raw_part = jwt_obj["raw_" .. part_name]
10 if raw_part == nil then
11 local part = jwt_obj[part_name]
13 error({reason="missing part " .. part_name})
15 raw_part = _M:jwt_encode(part)
21 local function parse(token_str, secret, issuer)
23 local raw_header, raw_payload, signature = string.match(
25 '([^%.]+)%.([^%.]+)%.([^%.]+)'
28 raw_header=raw_header,
29 raw_payload=raw_payload,
30 header=_M:jwt_decode(raw_header, true),
31 payload=_M:jwt_decode(raw_payload, true),
38 function _M.jwt_encode(self, ori)
39 if type(ori) == "table" then
40 ori = cjson.encode(ori)
42 return ngx.encode_base64(ori):gsub("+", "-"):gsub("/", "_"):gsub("=", "")
46 function _M.jwt_decode(self, b64_str, json_decode)
47 local reminder = #b64_str % 4
49 b64_str = b64_str .. string.rep("=", 4 - reminder)
51 local data = ngx.decode_base64(b64_str)
53 data = cjson.decode(data)
59 function _M.sign(self, secret_key, jwt_obj)
61 local typ = jwt_obj["header"]["typ"]
63 error({reason="invalid typ: " .. typ})
66 local alg = jwt_obj["header"]["alg"]
68 if alg == "HS256" then
69 hash_alg = hmac.ALGOS.SHA256
70 elseif alg == "HS512" then
71 hash_alg = hmac.ALGOS.SHA512
73 error({reason="unsupported alg: " .. alg})
76 local raw_header = get_raw_part("header", jwt_obj)
77 local raw_payload = get_raw_part("payload", jwt_obj)
79 local message =raw_header .. "." .. raw_payload
81 local hmac_func = hmac:new(secret_key, hash_alg)
82 local signature = _M:jwt_encode(hmac_func:final(message))
83 -- return full jwt string
84 return message .. "." .. signature
88 function _M.verify(self, secret, jwt_str, leeway)
89 local success, ret = pcall(parse, jwt_str)
92 return {verified=false, reason=ret["reason"] or "invalid jwt string"}
95 jwt_obj["verified"] = false
96 local success, ret = pcall(_M.sign, nil, secret, jwt_obj)
99 jwt_obj["reason"] = ret["reason"] or "internal error"
100 elseif jwt_str ~= ret then
102 jwt_obj["reason"] = "signature mismatch: " .. jwt_obj["signature"]
103 elseif leeway ~= nil then
104 local exp = jwt_obj["payload"]["exp"]
105 local nbf = jwt_obj["payload"]["nbf"]
106 local now = ngx.now()
108 if type(exp) == "number" and exp < (now - leeway) then
109 jwt_obj["reason"] = "jwt token expired at: " .. ngx.http_time(exp)
110 elseif type(nbf) == "number" and nbf > (now + leeway) then
111 jwt_obj["reason"] = "jwt token not valid until: " .. ngx.http_time(nbf)
115 if jwt_obj["reason"] == nil then
116 jwt_obj["verified"] = true
117 jwt_obj["reason"] = "everything is awesome~ :p"