nginx

Paddy 2015-06-22

0:68478c1bddde Go to Latest

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.

History
     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