Files
2026-07-13 12:32:21 +08:00

277 lines
7.3 KiB
Lua

local constants = require "kong.constants"
local kong_meta = require "kong.meta"
local kong = kong
local type = type
local error = error
local ipairs = ipairs
local tostring = tostring
local fmt = string.format
local HEADERS_CONSUMER_ID = constants.HEADERS.CONSUMER_ID
local HEADERS_CONSUMER_CUSTOM_ID = constants.HEADERS.CONSUMER_CUSTOM_ID
local HEADERS_CONSUMER_USERNAME = constants.HEADERS.CONSUMER_USERNAME
local HEADERS_CREDENTIAL_IDENTIFIER = constants.HEADERS.CREDENTIAL_IDENTIFIER
local HEADERS_ANONYMOUS = constants.HEADERS.ANONYMOUS
local KeyAuthHandler = {
VERSION = kong_meta.version,
PRIORITY = 1250,
}
local EMPTY = {}
local ERR_DUPLICATE_API_KEY = "Duplicate API key found"
local ERR_NO_API_KEY = "No API key found in request"
local ERR_INVALID_AUTH_CRED = "Unauthorized"
local ERR_INVALID_PLUGIN_CONF = "Invalid plugin configuration"
local ERR_UNEXPECTED = "An unexpected error occurred"
local function load_credential(key)
local cred, err = kong.db.keyauth_credentials:select_by_key(key)
if not cred then
return nil, err
end
if cred.ttl == 0 then
kong.log.debug("key expired")
return nil
end
return cred, nil, cred.ttl
end
local function set_consumer(consumer, credential)
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(HEADERS_CONSUMER_ID, consumer.id)
else
clear_header(HEADERS_CONSUMER_ID)
end
if consumer and consumer.custom_id then
set_header(HEADERS_CONSUMER_CUSTOM_ID, consumer.custom_id)
else
clear_header(HEADERS_CONSUMER_CUSTOM_ID)
end
if consumer and consumer.username then
set_header(HEADERS_CONSUMER_USERNAME, consumer.username)
else
clear_header(HEADERS_CONSUMER_USERNAME)
end
if credential and credential.id then
set_header(HEADERS_CREDENTIAL_IDENTIFIER, credential.id)
else
clear_header(HEADERS_CREDENTIAL_IDENTIFIER)
end
if credential then
clear_header(HEADERS_ANONYMOUS)
else
set_header(HEADERS_ANONYMOUS, true)
end
end
local function get_body()
local body, err = kong.request.get_body()
if err then
kong.log.info("Cannot process request body: ", err)
return EMPTY
end
return body
end
local function server_error(message)
return { status = 500, message = message }
end
local function unauthorized(message, www_auth_content)
return { status = 401, message = message, headers = { ["WWW-Authenticate"] = www_auth_content } }
end
local function do_authentication(conf)
if type(conf.key_names) ~= "table" then
kong.log.err("no conf.key_names set, aborting plugin execution")
return nil, server_error(ERR_INVALID_PLUGIN_CONF)
end
local www_auth_content = conf.realm and fmt('Key realm="%s"', conf.realm) or 'Key'
local headers = kong.request.get_headers()
local query = kong.request.get_query()
local key
local body
-- search in headers & querystring
for _, name in ipairs(conf.key_names) do
local v
if conf.key_in_header then
v = headers[name]
end
if not v and conf.key_in_query then
-- search in querystring
v = query[name]
end
-- search the body, if we asked to
if not v and conf.key_in_body then
if not body then
body = get_body()
end
v = body[name]
end
if type(v) == "string" then
key = v
if conf.hide_credentials then
kong.service.request.clear_query_arg(name)
kong.service.request.clear_header(name)
if conf.key_in_body then
if not body then
body = get_body()
end
if body ~= EMPTY then
if body then
body[name] = nil
end
kong.service.request.set_body(body)
end
end
end
break
elseif type(v) == "table" then
-- duplicate API key
return nil, unauthorized(ERR_DUPLICATE_API_KEY, www_auth_content)
end
end
-- this request is missing an API key, HTTP 401
if not key or key == "" then
return nil, unauthorized(ERR_NO_API_KEY, www_auth_content)
end
-- retrieve our consumer linked to this API key
local cache = kong.cache
local credential_cache_key = kong.db.keyauth_credentials:cache_key(key)
-- hit_level be 1 if stale value is propelled into L1 cache; so set a minimal `resurrect_ttl`
local credential, err, hit_level = cache:get(credential_cache_key, { resurrect_ttl = 0.001 }, load_credential,
key)
if err then
return error(err)
end
kong.log.debug("credential hit_level: ", tostring(hit_level))
-- no credential in DB, for this key, it is invalid, HTTP 401
if not credential or hit_level == 4 then
return nil, unauthorized(ERR_INVALID_AUTH_CRED, www_auth_content)
end
-----------------------------------------
-- Success, this request is authenticated
-----------------------------------------
-- retrieve the consumer linked to this API key, to set appropriate headers
local consumer_cache_key, consumer
consumer_cache_key = kong.db.consumers:cache_key(credential.consumer.id)
consumer, err = cache:get(consumer_cache_key, nil,
kong.client.load_consumer,
credential.consumer.id)
if err then
kong.log.err(err)
return nil, server_error(ERR_UNEXPECTED)
end
set_consumer(consumer, credential)
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.error(err.status, err.message, err.headers)
end
end
function KeyAuthHandler: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 KeyAuthHandler