472 lines
13 KiB
Lua
472 lines
13 KiB
Lua
-- JWT verification module
|
|
-- Adapted version of x25/luajwt for Kong. It provides various improvements and
|
|
-- an OOP architecture allowing the JWT to be parsed and verified separately,
|
|
-- avoiding multiple parsings.
|
|
--
|
|
-- @see https://github.com/x25/luajwt
|
|
|
|
local json = require "cjson"
|
|
local b64 = require "ngx.base64"
|
|
local buffer = require "string.buffer"
|
|
local openssl_digest = require "resty.openssl.digest"
|
|
local openssl_mac = require "resty.openssl.mac"
|
|
local openssl_pkey = require "resty.openssl.pkey"
|
|
|
|
|
|
local rep = string.rep
|
|
local sub = string.sub
|
|
local find = string.find
|
|
local type = type
|
|
local time = ngx.time
|
|
local pairs = pairs
|
|
local error = error
|
|
local pcall = pcall
|
|
local insert = table.insert
|
|
local unpack = unpack
|
|
local assert = assert
|
|
local tostring = tostring
|
|
local setmetatable = setmetatable
|
|
local getmetatable = getmetatable
|
|
local encode_base64url = b64.encode_base64url
|
|
local decode_base64url = b64.decode_base64url
|
|
|
|
|
|
--- Supported algorithms for signing tokens.
|
|
local alg_sign = {
|
|
HS256 = function(data, key) return openssl_mac.new(key, "HMAC", nil, "sha256"):final(data) end,
|
|
HS384 = function(data, key) return openssl_mac.new(key, "HMAC", nil, "sha384"):final(data) end,
|
|
HS512 = function(data, key) return openssl_mac.new(key, "HMAC", nil, "sha512"):final(data) end,
|
|
RS256 = function(data, key)
|
|
local digest = openssl_digest.new("sha256")
|
|
assert(digest:update(data))
|
|
return assert(openssl_pkey.new(key):sign(digest))
|
|
end,
|
|
RS384 = function(data, key)
|
|
local digest = openssl_digest.new("sha384")
|
|
assert(digest:update(data))
|
|
return assert(openssl_pkey.new(key):sign(digest))
|
|
end,
|
|
RS512 = function(data, key)
|
|
local digest = openssl_digest.new("sha512")
|
|
assert(digest:update(data))
|
|
return assert(openssl_pkey.new(key):sign(digest))
|
|
end,
|
|
ES256 = function(data, key)
|
|
local pkey = openssl_pkey.new(key)
|
|
local sig = assert(pkey:sign(data, "sha256", nil, { ecdsa_use_raw = true }))
|
|
if not sig then
|
|
return nil
|
|
end
|
|
return sig
|
|
end,
|
|
ES384 = function(data, key)
|
|
local pkey = openssl_pkey.new(key)
|
|
local sig = assert(pkey:sign(data, "sha384", nil, { ecdsa_use_raw = true }))
|
|
if not sig then
|
|
return nil
|
|
end
|
|
return sig
|
|
end,
|
|
ES512 = function(data, key)
|
|
local pkey = openssl_pkey.new(key)
|
|
local sig = assert(pkey:sign(data, "sha512", nil, { ecdsa_use_raw = true }))
|
|
if not sig then
|
|
return nil
|
|
end
|
|
return sig
|
|
end,
|
|
|
|
PS256 = function(data, key)
|
|
local pkey = openssl_pkey.new(key)
|
|
local sig = assert(pkey:sign(data, "sha256", openssl_pkey.PADDINGS.RSA_PKCS1_PSS_PADDING))
|
|
if not sig then
|
|
return nil
|
|
end
|
|
return sig
|
|
end,
|
|
PS384 = function(data, key)
|
|
local pkey = openssl_pkey.new(key)
|
|
local sig = assert(pkey:sign(data, "sha384", openssl_pkey.PADDINGS.RSA_PKCS1_PSS_PADDING))
|
|
if not sig then
|
|
return nil
|
|
end
|
|
return sig
|
|
end,
|
|
PS512 = function(data, key)
|
|
local pkey = openssl_pkey.new(key)
|
|
local sig = assert(pkey:sign(data, "sha512", openssl_pkey.PADDINGS.RSA_PKCS1_PSS_PADDING))
|
|
if not sig then
|
|
return nil
|
|
end
|
|
return sig
|
|
end,
|
|
EdDSA = function(data, key)
|
|
local pkey = assert(openssl_pkey.new(key))
|
|
return assert(pkey:sign(data))
|
|
end
|
|
}
|
|
|
|
|
|
--- Supported algorithms for verifying tokens.
|
|
local alg_verify = {
|
|
HS256 = function(data, signature, key) return signature == alg_sign.HS256(data, key) end,
|
|
HS384 = function(data, signature, key) return signature == alg_sign.HS384(data, key) end,
|
|
HS512 = function(data, signature, key) return signature == alg_sign.HS512(data, key) end,
|
|
RS256 = function(data, signature, key)
|
|
local pkey, _ = openssl_pkey.new(key)
|
|
assert(pkey, "Consumer Public Key is Invalid")
|
|
return pkey:verify(signature, data, "sha256")
|
|
end,
|
|
RS384 = function(data, signature, key)
|
|
local pkey, _ = openssl_pkey.new(key)
|
|
assert(pkey, "Consumer Public Key is Invalid")
|
|
return pkey:verify(signature, data, "sha384")
|
|
end,
|
|
RS512 = function(data, signature, key)
|
|
local pkey, _ = openssl_pkey.new(key)
|
|
assert(pkey, "Consumer Public Key is Invalid")
|
|
return pkey:verify(signature, data, "sha512")
|
|
end,
|
|
-- https://www.rfc-editor.org/rfc/rfc7518#section-3.4
|
|
ES256 = function(data, signature, key)
|
|
local pkey, _ = openssl_pkey.new(key)
|
|
assert(pkey, "Consumer Public Key is Invalid")
|
|
-- The ECDSA P-256 SHA-256 digital signature for a JWS is validated as
|
|
-- follows:
|
|
|
|
-- 1. The JWS Signature value MUST be a 64-octet sequence. If it is
|
|
-- not a 64-octet sequence, the validation has failed.
|
|
assert(#signature == 64, "Signature must be 64 bytes.")
|
|
return pkey:verify(signature, data, "sha256", nil, { ecdsa_use_raw = true })
|
|
end,
|
|
ES384 = function(data, signature, key)
|
|
-- Signing and validation with the ECDSA P-384 SHA-384 and ECDSA P-521
|
|
-- SHA-512 algorithms is performed identically to the procedure for
|
|
-- ECDSA P-256 SHA-256 -- just using the corresponding hash algorithms
|
|
-- with correspondingly larger result values. For ECDSA P-384 SHA-384,
|
|
-- R and S will be 384 bits each, resulting in a 96-octet sequence. For
|
|
-- ECDSA P-521 SHA-512, R and S will be 521 bits each, resulting in a
|
|
-- 132-octet sequence.
|
|
local pkey, _ = openssl_pkey.new(key)
|
|
assert(pkey, "Consumer Public Key is Invalid")
|
|
assert(#signature == 96, "Signature must be 96 bytes.")
|
|
return pkey:verify(signature, data, "sha384", nil, { ecdsa_use_raw = true })
|
|
end,
|
|
|
|
ES512 = function(data, signature, key)
|
|
-- Signing and validation with the ECDSA P-384 SHA-384 and ECDSA P-521
|
|
-- SHA-512 algorithms is performed identically to the procedure for
|
|
-- ECDSA P-256 SHA-256 -- just using the corresponding hash algorithms
|
|
-- with correspondingly larger result values. For ECDSA P-384 SHA-384,
|
|
-- R and S will be 384 bits each, resulting in a 96-octet sequence. For
|
|
-- ECDSA P-521 SHA-512, R and S will be 521 bits each, resulting in a
|
|
-- 132-octet sequence.
|
|
local pkey, _ = openssl_pkey.new(key)
|
|
assert(pkey, "Consumer Public Key is Invalid")
|
|
assert(#signature == 132, "Signature must be 132 bytes.")
|
|
return pkey:verify(signature, data, "sha512", nil, { ecdsa_use_raw = true })
|
|
end,
|
|
|
|
PS256 = function(data, signature, key)
|
|
local pkey, _ = openssl_pkey.new(key)
|
|
assert(pkey, "Consumer Public Key is Invalid")
|
|
assert(#signature == 256, "Signature must be 256 bytes")
|
|
return pkey:verify(signature, data, "sha256", openssl_pkey.PADDINGS.RSA_PKCS1_PSS_PADDING)
|
|
end,
|
|
PS384 = function(data, signature, key)
|
|
local pkey, _ = openssl_pkey.new(key)
|
|
assert(pkey, "Consumer Public Key is Invalid")
|
|
assert(#signature == 256, "Signature must be 256 bytes")
|
|
return pkey:verify(signature, data, "sha384", openssl_pkey.PADDINGS.RSA_PKCS1_PSS_PADDING)
|
|
end,
|
|
PS512 = function(data, signature, key)
|
|
local pkey, _ = openssl_pkey.new(key)
|
|
assert(pkey, "Consumer Public Key is Invalid")
|
|
assert(#signature == 256, "Signature must be 256 bytes")
|
|
return pkey:verify(signature, data, "sha512", openssl_pkey.PADDINGS.RSA_PKCS1_PSS_PADDING)
|
|
end,
|
|
EdDSA = function(data, signature, key)
|
|
-- Support of EdDSA alg typ according to RFC 8037
|
|
-- https://www.rfc-editor.org/rfc/rfc8037
|
|
local pkey, _ = openssl_pkey.new(key)
|
|
assert(pkey, "Consumer Public Key is Invalid")
|
|
return pkey:verify(signature, data)
|
|
end
|
|
}
|
|
|
|
|
|
--- base 64 encoding
|
|
-- @param input String to base64 encode
|
|
-- @return Base64 encoded string
|
|
local function base64_encode(input)
|
|
local result = encode_base64url(input)
|
|
return result
|
|
end
|
|
|
|
|
|
--- base 64 decode
|
|
-- @param input String to base64 decode
|
|
-- @return Base64 decoded string
|
|
local function base64_decode(input)
|
|
local remainder = #input % 4
|
|
|
|
if remainder > 0 then
|
|
local padlen = 4 - remainder
|
|
input = input .. rep("=", padlen)
|
|
end
|
|
|
|
return decode_base64url(input)
|
|
end
|
|
|
|
|
|
--- Tokenize a string by delimiter
|
|
-- Used to separate the header, claims and signature part of a JWT
|
|
-- @param str String to tokenize
|
|
-- @param div Delimiter
|
|
-- @param len Number of parts to retrieve
|
|
-- @return A table of strings
|
|
local function tokenize(str, div, len)
|
|
local result, idx, pos = {}, 1, 0
|
|
|
|
local iter = function()
|
|
return find(str, div, pos, true)
|
|
end
|
|
|
|
for st, sp in iter do
|
|
result[idx] = sub(str, pos, st - 1)
|
|
idx = idx + 1
|
|
pos = sp + 1
|
|
len = len - 1
|
|
if len <= 1 then
|
|
break
|
|
end
|
|
end
|
|
|
|
result[idx] = sub(str, pos)
|
|
return result
|
|
end
|
|
|
|
|
|
--- Parse a JWT
|
|
-- Parse a JWT and validate header values.
|
|
-- @param token JWT to parse
|
|
-- @return A table containing base64 and decoded headers, claims and signature
|
|
local function decode_token(token)
|
|
-- Get b64 parts
|
|
local header_64, claims_64, signature_64 = unpack(tokenize(token, ".", 3))
|
|
|
|
-- Decode JSON
|
|
local ok, header, claims, signature = pcall(function()
|
|
return json.decode(base64_decode(header_64)),
|
|
json.decode(base64_decode(claims_64)),
|
|
base64_decode(signature_64)
|
|
end)
|
|
if not ok then
|
|
return nil, "invalid JSON"
|
|
end
|
|
|
|
if not header.alg or type(header.alg) ~= "string" or not alg_verify[header.alg] then
|
|
return nil, "invalid alg"
|
|
end
|
|
|
|
if not claims then
|
|
return nil, "invalid claims"
|
|
end
|
|
|
|
if not signature then
|
|
return nil, "invalid signature"
|
|
end
|
|
|
|
return {
|
|
token = token,
|
|
header_64 = header_64,
|
|
claims_64 = claims_64,
|
|
signature_64 = signature_64,
|
|
header = header,
|
|
claims = claims,
|
|
signature = signature
|
|
}
|
|
end
|
|
|
|
|
|
-- For test purposes
|
|
local function encode_token(data, key, alg, header)
|
|
if type(data) ~= "table" then
|
|
error("Argument #1 must be table", 2)
|
|
end
|
|
|
|
if type(key) ~= "string" then
|
|
error("Argument #2 must be string", 2)
|
|
end
|
|
|
|
if header and type(header) ~= "table" then
|
|
error("Argument #4 must be a table", 2)
|
|
end
|
|
|
|
alg = alg or "HS256"
|
|
if not alg_sign[alg] then
|
|
error("Algorithm not supported", 2)
|
|
end
|
|
|
|
local header = header or { typ = "JWT", alg = alg }
|
|
local buf = buffer.new()
|
|
|
|
buf:put(base64_encode(json.encode(header))):put(".")
|
|
:put(base64_encode(json.encode(data)))
|
|
|
|
local signature = alg_sign[alg](buf:tostring(), key)
|
|
|
|
buf:put(".")
|
|
:put(base64_encode(signature))
|
|
|
|
return buf:get()
|
|
end
|
|
|
|
|
|
local err_list_mt = {}
|
|
|
|
|
|
local function add_error(errors, k, v)
|
|
if not errors then
|
|
errors = {}
|
|
end
|
|
|
|
if errors and errors[k] then
|
|
if getmetatable(errors[k]) ~= err_list_mt then
|
|
errors[k] = setmetatable({errors[k]}, err_list_mt)
|
|
end
|
|
|
|
insert(errors[k], v)
|
|
else
|
|
errors[k] = v
|
|
end
|
|
|
|
return errors
|
|
end
|
|
|
|
|
|
--[[
|
|
|
|
JWT public interface
|
|
|
|
]]--
|
|
|
|
|
|
local _M = {}
|
|
|
|
|
|
_M.__index = _M
|
|
|
|
|
|
--- Instantiate a JWT parser
|
|
-- Parse a JWT and instantiate a JWT parser for further operations
|
|
-- Return errors instead of an instance if any encountered
|
|
-- @param token JWT to parse
|
|
-- @return JWT parser
|
|
-- @return error if any
|
|
function _M:new(token)
|
|
if type(token) ~= "string" then
|
|
error("Token must be a string, got " .. tostring(token), 2)
|
|
end
|
|
|
|
local token, err = decode_token(token)
|
|
if err then
|
|
return nil, err
|
|
end
|
|
|
|
return setmetatable(token, _M)
|
|
end
|
|
|
|
|
|
--- Verify a JWT signature
|
|
-- Verify the current JWT signature against a given key
|
|
-- @param key Key against which to verify the signature
|
|
-- @return A boolean indicating if the signature if verified or not
|
|
function _M:verify_signature(key)
|
|
return alg_verify[self.header.alg](self.header_64 .. "." .. self.claims_64, self.signature, key)
|
|
end
|
|
|
|
|
|
function _M:base64_decode(input)
|
|
return base64_decode(input)
|
|
end
|
|
|
|
|
|
--- Registered claims according to RFC 7519 Section 4.1
|
|
local registered_claims = {
|
|
nbf = {
|
|
type = "number",
|
|
check = function(nbf)
|
|
if nbf > time() then
|
|
return "token not valid yet"
|
|
end
|
|
end
|
|
},
|
|
exp = {
|
|
type = "number",
|
|
check = function(exp)
|
|
if exp <= time() then
|
|
return "token expired"
|
|
end
|
|
end
|
|
}
|
|
}
|
|
|
|
|
|
--- Verify registered claims (according to RFC 7519 Section 4.1)
|
|
-- Claims are verified by type and a check.
|
|
-- @param claims_to_verify A list of claims to verify.
|
|
-- @return A boolean indicating true if no errors zere found
|
|
-- @return A list of errors
|
|
function _M:verify_registered_claims(claims_to_verify)
|
|
if not claims_to_verify then
|
|
claims_to_verify = {}
|
|
end
|
|
|
|
local errors
|
|
local claim
|
|
local claim_rules
|
|
|
|
for _, claim_name in pairs(claims_to_verify) do
|
|
claim = self.claims[claim_name]
|
|
claim_rules = registered_claims[claim_name]
|
|
|
|
if type(claim) ~= claim_rules.type then
|
|
errors = add_error(errors, claim_name, "must be a " .. claim_rules.type)
|
|
|
|
else
|
|
local check_err = claim_rules.check(claim)
|
|
if check_err then
|
|
errors = add_error(errors, claim_name, check_err)
|
|
end
|
|
end
|
|
end
|
|
|
|
return errors == nil, errors
|
|
end
|
|
|
|
|
|
--- Check that the maximum allowed expiration is not reached
|
|
-- @param maximum_expiration of the claim
|
|
-- @return A Boolean indicating true if the claim has reached the maximum
|
|
-- allowed expiration time
|
|
-- @return error if any
|
|
function _M:check_maximum_expiration(maximum_expiration)
|
|
if maximum_expiration <= 0 then
|
|
return true
|
|
end
|
|
|
|
local exp = self.claims.exp
|
|
if exp == nil or exp - time() > maximum_expiration then
|
|
return false, { exp = "exceeds maximum allowed expiration" }
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
_M.encode = encode_token
|
|
|
|
|
|
return _M
|