335 lines
9.5 KiB
Lua
335 lines
9.5 KiB
Lua
local constants = require "kong.constants"
|
|
local jwt_decoder = require "kong.plugins.jwt.jwt_parser"
|
|
local kong_meta = require "kong.meta"
|
|
|
|
|
|
local fmt = string.format
|
|
local kong = kong
|
|
local type = type
|
|
local error = error
|
|
local ipairs = ipairs
|
|
local pairs = pairs
|
|
local tostring = tostring
|
|
local re_gmatch = ngx.re.gmatch
|
|
|
|
|
|
local JwtHandler = {
|
|
VERSION = kong_meta.version,
|
|
PRIORITY = 1450,
|
|
}
|
|
|
|
|
|
--- Retrieve a JWT in a request.
|
|
-- Checks for the JWT in URI parameters, then in cookies, and finally
|
|
-- in the configured header_names (defaults to `[Authorization]`).
|
|
-- @param conf Plugin configuration
|
|
-- @return token JWT token contained in request (can be a table) or nil
|
|
-- @return err
|
|
local function retrieve_tokens(conf)
|
|
local token_set = {}
|
|
local args = kong.request.get_query()
|
|
for _, v in ipairs(conf.uri_param_names) do
|
|
local token = args[v] -- can be a table
|
|
if token then
|
|
if type(token) == "table" then
|
|
for _, t in ipairs(token) do
|
|
if t ~= "" then
|
|
token_set[t] = true
|
|
end
|
|
end
|
|
|
|
elseif token ~= "" then
|
|
token_set[token] = true
|
|
end
|
|
end
|
|
end
|
|
|
|
local var = ngx.var
|
|
for _, v in ipairs(conf.cookie_names) do
|
|
local cookie = var["cookie_" .. v]
|
|
if cookie and cookie ~= "" then
|
|
token_set[cookie] = true
|
|
end
|
|
end
|
|
|
|
local request_headers = kong.request.get_headers()
|
|
for _, v in ipairs(conf.header_names) do
|
|
local token_header = request_headers[v]
|
|
if token_header then
|
|
if type(token_header) == "table" then
|
|
token_header = token_header[1]
|
|
end
|
|
local iterator, iter_err = re_gmatch(token_header, "\\s*[Bb]earer\\s+(.+)", "jo")
|
|
if not iterator then
|
|
kong.log.err(iter_err)
|
|
break
|
|
end
|
|
|
|
local m, err = iterator()
|
|
if err then
|
|
kong.log.err(err)
|
|
break
|
|
end
|
|
|
|
if m and #m > 0 then
|
|
if m[1] ~= "" then
|
|
token_set[m[1]] = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local tokens_n = 0
|
|
local tokens = {}
|
|
for token, _ in pairs(token_set) do
|
|
tokens_n = tokens_n + 1
|
|
tokens[tokens_n] = token
|
|
end
|
|
|
|
if tokens_n == 0 then
|
|
return nil
|
|
end
|
|
|
|
if tokens_n == 1 then
|
|
return tokens[1]
|
|
end
|
|
|
|
return tokens
|
|
end
|
|
|
|
|
|
local function load_credential(jwt_secret_key)
|
|
local row, err = kong.db.jwt_secrets:select_by_key(jwt_secret_key)
|
|
if err then
|
|
return nil, err
|
|
end
|
|
return row
|
|
end
|
|
|
|
|
|
local function set_consumer(consumer, credential, token)
|
|
kong.client.authenticate(consumer, credential)
|
|
|
|
local set_header = kong.service.request.set_header
|
|
local clear_header = kong.service.request.clear_header
|
|
|
|
if consumer and consumer.id then
|
|
set_header(constants.HEADERS.CONSUMER_ID, consumer.id)
|
|
else
|
|
clear_header(constants.HEADERS.CONSUMER_ID)
|
|
end
|
|
|
|
if consumer and consumer.custom_id then
|
|
set_header(constants.HEADERS.CONSUMER_CUSTOM_ID, consumer.custom_id)
|
|
else
|
|
clear_header(constants.HEADERS.CONSUMER_CUSTOM_ID)
|
|
end
|
|
|
|
if consumer and consumer.username then
|
|
set_header(constants.HEADERS.CONSUMER_USERNAME, consumer.username)
|
|
else
|
|
clear_header(constants.HEADERS.CONSUMER_USERNAME)
|
|
end
|
|
|
|
if credential and credential.key then
|
|
set_header(constants.HEADERS.CREDENTIAL_IDENTIFIER, credential.key)
|
|
else
|
|
clear_header(constants.HEADERS.CREDENTIAL_IDENTIFIER)
|
|
end
|
|
|
|
if credential then
|
|
clear_header(constants.HEADERS.ANONYMOUS)
|
|
else
|
|
set_header(constants.HEADERS.ANONYMOUS, true)
|
|
end
|
|
|
|
kong.ctx.shared.authenticated_jwt_token = token -- TODO: wrap in a PDK function?
|
|
end
|
|
|
|
local function unauthorized(message, www_auth_content, errors)
|
|
return { status = 401, message = message, headers = { ["WWW-Authenticate"] = www_auth_content }, errors = errors }
|
|
end
|
|
|
|
|
|
local function do_authentication(conf)
|
|
local token, err = retrieve_tokens(conf)
|
|
if err then
|
|
return error(err)
|
|
end
|
|
|
|
local www_authenticate_base, www_authenticate_with_error
|
|
if conf.realm then
|
|
www_authenticate_base = fmt('Bearer realm="%s"', conf.realm)
|
|
www_authenticate_with_error = www_authenticate_base .. ', error="invalid_token"'
|
|
|
|
else
|
|
www_authenticate_base = "Bearer"
|
|
www_authenticate_with_error = 'Bearer error="invalid_token"'
|
|
end
|
|
|
|
local token_type = type(token)
|
|
if token_type ~= "string" then
|
|
if token_type == "nil" then
|
|
return false, unauthorized("Unauthorized", www_authenticate_base)
|
|
elseif token_type == "table" then
|
|
return false, unauthorized("Multiple tokens provided", www_authenticate_with_error)
|
|
else
|
|
return false, unauthorized("Unrecognizable token", www_authenticate_with_error)
|
|
end
|
|
end
|
|
|
|
-- Decode token to find out who the consumer is
|
|
local jwt, err = jwt_decoder:new(token)
|
|
if err then
|
|
return false, unauthorized("Bad token; " .. tostring(err), www_authenticate_with_error)
|
|
end
|
|
|
|
local claims = jwt.claims
|
|
local header = jwt.header
|
|
|
|
local jwt_secret_key = claims[conf.key_claim_name] or header[conf.key_claim_name]
|
|
if not jwt_secret_key then
|
|
return false, unauthorized("No mandatory '" .. conf.key_claim_name .. "' in claims", www_authenticate_with_error)
|
|
elseif jwt_secret_key == "" then
|
|
return false, unauthorized("Invalid '" .. conf.key_claim_name .. "' in claims", www_authenticate_with_error)
|
|
end
|
|
|
|
-- Retrieve the secret
|
|
local jwt_secret_cache_key = kong.db.jwt_secrets:cache_key(jwt_secret_key)
|
|
local jwt_secret, err = kong.cache:get(jwt_secret_cache_key, nil,
|
|
load_credential, jwt_secret_key)
|
|
if err then
|
|
return error(err)
|
|
end
|
|
|
|
if not jwt_secret then
|
|
return false, unauthorized("No credentials found for given '" .. conf.key_claim_name .. "'", www_authenticate_with_error)
|
|
end
|
|
|
|
local algorithm = jwt_secret.algorithm or "HS256"
|
|
|
|
-- Verify "alg"
|
|
if jwt.header.alg ~= algorithm then
|
|
return false, unauthorized("Invalid algorithm", www_authenticate_with_error)
|
|
end
|
|
|
|
local is_symmetric_algorithm = algorithm ~= nil and algorithm:sub(1, 2) == "HS"
|
|
local jwt_secret_value
|
|
|
|
if is_symmetric_algorithm and conf.secret_is_base64 then
|
|
jwt_secret_value = jwt:base64_decode(jwt_secret.secret)
|
|
elseif is_symmetric_algorithm then
|
|
jwt_secret_value = jwt_secret.secret
|
|
else
|
|
-- rsa_public_key is either nil or a valid plain text pem file, it can't be base64 decoded.
|
|
-- see #13710
|
|
jwt_secret_value = jwt_secret.rsa_public_key
|
|
end
|
|
|
|
if not jwt_secret_value then
|
|
return false, unauthorized("Invalid key/secret", www_authenticate_with_error)
|
|
end
|
|
|
|
-- Now verify the JWT signature
|
|
if not jwt:verify_signature(jwt_secret_value) then
|
|
return false, unauthorized("Invalid signature", www_authenticate_with_error)
|
|
end
|
|
|
|
-- Verify the JWT registered claims
|
|
local ok_claims, errors = jwt:verify_registered_claims(conf.claims_to_verify)
|
|
if not ok_claims then
|
|
return false, unauthorized(nil, www_authenticate_with_error, errors)
|
|
end
|
|
|
|
-- Verify the JWT registered claims
|
|
if conf.maximum_expiration ~= nil and conf.maximum_expiration > 0 then
|
|
local ok, errors = jwt:check_maximum_expiration(conf.maximum_expiration)
|
|
if not ok then
|
|
return false, unauthorized(nil, www_authenticate_with_error, errors)
|
|
end
|
|
end
|
|
|
|
-- Retrieve the consumer
|
|
local consumer_cache_key = kong.db.consumers:cache_key(jwt_secret.consumer.id)
|
|
local consumer, err = kong.cache:get(consumer_cache_key, nil,
|
|
kong.client.load_consumer,
|
|
jwt_secret.consumer.id, true)
|
|
if err then
|
|
return error(err)
|
|
end
|
|
|
|
-- However this should not happen
|
|
if not consumer then
|
|
return false, {
|
|
status = 401,
|
|
message = fmt("Could not find consumer for '%s=%s'", conf.key_claim_name, jwt_secret_key)
|
|
}
|
|
end
|
|
|
|
set_consumer(consumer, jwt_secret, token)
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
local function set_anonymous_consumer(anonymous)
|
|
local consumer_cache_key = kong.db.consumers:cache_key(anonymous)
|
|
local consumer, err = kong.cache:get(consumer_cache_key, nil,
|
|
kong.client.load_consumer,
|
|
anonymous, true)
|
|
if err then
|
|
return error(err)
|
|
end
|
|
|
|
if not consumer then
|
|
local err_msg = "anonymous consumer " .. anonymous .. " is configured but doesn't exist"
|
|
kong.log.err(err_msg)
|
|
return kong.response.error(500, err_msg)
|
|
end
|
|
|
|
set_consumer(consumer)
|
|
end
|
|
|
|
|
|
--- When conf.anonymous is enabled we are in "logical OR" authentication flow.
|
|
--- Meaning - either anonymous consumer is enabled or there are multiple auth plugins
|
|
--- and we need to passthrough on failed authentication.
|
|
local function logical_OR_authentication(conf)
|
|
if kong.client.get_credential() then
|
|
-- we're already authenticated and in "logical OR" between auth methods -- early exit
|
|
return
|
|
end
|
|
|
|
local ok, _ = do_authentication(conf)
|
|
if not ok then
|
|
set_anonymous_consumer(conf.anonymous)
|
|
end
|
|
end
|
|
|
|
--- When conf.anonymous is not set we are in "logical AND" authentication flow.
|
|
--- Meaning - if this authentication fails the request should not be authorized
|
|
--- even though other auth plugins might have successfully authorized user.
|
|
local function logical_AND_authentication(conf)
|
|
local ok, err = do_authentication(conf)
|
|
if not ok then
|
|
return kong.response.exit(err.status, err.errors or { message = err.message }, err.headers)
|
|
end
|
|
end
|
|
|
|
|
|
function JwtHandler:access(conf)
|
|
-- check if preflight request and whether it should be authenticated
|
|
if not conf.run_on_preflight and kong.request.get_method() == "OPTIONS" then
|
|
return
|
|
end
|
|
|
|
if conf.anonymous then
|
|
return logical_OR_authentication(conf)
|
|
else
|
|
return logical_AND_authentication(conf)
|
|
end
|
|
end
|
|
|
|
|
|
return JwtHandler
|