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

339 lines
9.1 KiB
Lua

local constants = require "kong.constants"
local ldap = require "kong.plugins.ldap-auth.ldap"
local kong = kong
local error = error
local decode_base64 = ngx.decode_base64
local tostring = tostring
local re_find = ngx.re.find
local re_match = ngx.re.match
local lower = string.lower
local upper = string.upper
local sub = string.sub
local fmt = string.format
local tcp = ngx.socket.tcp
local sha256_hex = require("kong.tools.sha256").sha256_hex
local AUTHORIZATION = "authorization"
local PROXY_AUTHORIZATION = "proxy-authorization"
local _M = {}
local function retrieve_credentials(authorization_header_value, conf)
local lower_header_type = lower(conf.header_type)
local regex = "^\\s*" .. lower_header_type .. "\\s+"
local from, to, err = re_find(lower(authorization_header_value), regex, "jo")
if err then
kong.log.err("error while find header_type: ", lower_header_type, " in authorization header value")
return nil
end
if not from then
kong.log.info("header_type: ", lower_header_type, " not found in authorization header value")
return nil
end
local username, password
if from == 1 then
local cred = sub(authorization_header_value, to + 1)
local decoded_cred = decode_base64(cred)
local m, err = re_match(decoded_cred, "^(.*?):(.+)$", "jo")
if err then
kong.log.err("error while decoding credentials: ", err)
return nil
end
if type(m) == "table" and #m == 2 then
username = m[1]
password = m[2]
else
kong.log.err("no valid credentials found in authorization header value")
return nil
end
end
return username, password
end
local function ldap_authenticate(given_username, given_password, conf)
local is_authenticated
local err, suppressed_err, ok, _
local sock = tcp()
sock:settimeout(conf.timeout)
local opts
-- keep TLS connections in a separate pool to avoid reusing non-secure
-- connections and vice-versa, because STARTTLS use the same port
if conf.start_tls then
opts = {
pool = conf.ldap_host .. ":" .. conf.ldap_port .. ":starttls"
}
end
ok, err = sock:connect(conf.ldap_host, conf.ldap_port, opts)
if not ok then
kong.log.err("failed to connect to ", conf.ldap_host, ":",
tostring(conf.ldap_port), ": ", err)
return nil, err
end
if conf.start_tls then
-- convert connection to a STARTTLS connection only if it is a new connection
local count, err = sock:getreusedtimes()
if not count then
-- connection was closed, just return instead
return nil, err
end
if count == 0 then
local ok, err = ldap.start_tls(sock)
if not ok then
return nil, err
end
end
end
if conf.start_tls or conf.ldaps then
_, err = sock:sslhandshake(true, conf.ldap_host, conf.verify_ldap_host)
if err ~= nil then
return false, fmt("failed to do SSL handshake with %s:%s: %s",
conf.ldap_host, tostring(conf.ldap_port), err)
end
end
local who = conf.attribute .. "=" .. given_username .. "," .. conf.base_dn
is_authenticated, err = ldap.bind_request(sock, who, given_password)
ok, suppressed_err = sock:setkeepalive(conf.keepalive)
if not ok then
kong.log.err("failed to keepalive to ", conf.ldap_host, ":",
tostring(conf.ldap_port), ": ", suppressed_err)
end
return is_authenticated, err
end
local function cache_key(conf, username, password)
local hash, err = sha256_hex(fmt("%s:%u:%s:%s:%u:%s:%s",
lower(conf.ldap_host),
conf.ldap_port,
conf.base_dn,
conf.attribute,
conf.cache_ttl,
username,
password))
if err then
return nil, err
end
return "ldap_auth_cache:" .. hash
end
local function load_credential(given_username, given_password, conf)
local ok, err = ldap_authenticate(given_username, given_password, conf)
if err ~= nil then
kong.log.err(err)
end
if ok == nil then
return nil
end
if ok == false then
return false
end
local key
key, err = cache_key(conf, given_username, given_password)
if err then
return nil, err
end
return {
id = key,
username = given_username,
password = given_password,
}
end
local function authenticate(conf, given_credentials)
local given_username, given_password = retrieve_credentials(given_credentials, conf)
if given_username == nil then
return false
end
local key, err = cache_key(conf, given_username, given_password)
if err then
return error(err)
end
local credential
credential, err = kong.cache:get(key, {
ttl = conf.cache_ttl,
neg_ttl = conf.cache_ttl
}, load_credential, given_username, given_password, conf)
if err or credential == nil then
return error(err)
end
return credential and credential.password == given_password, credential
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(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.username then
set_header(constants.HEADERS.CREDENTIAL_IDENTIFIER, credential.username)
else
clear_header(constants.HEADERS.CREDENTIAL_IDENTIFIER)
end
if credential then
clear_header(constants.HEADERS.ANONYMOUS)
else
set_header(constants.HEADERS.ANONYMOUS, true)
end
end
local function unauthorized(message, authorization_scheme)
return {
status = 401,
message = message,
headers = { ["WWW-Authenticate"] = authorization_scheme }
}
end
local function do_authentication(conf)
local authorization_value = kong.request.get_header(AUTHORIZATION)
local proxy_authorization_value = kong.request.get_header(PROXY_AUTHORIZATION)
local scheme = conf.header_type
if scheme == "ldap" then
-- ensure backwards compatibility (see GH PR #3656)
-- TODO: provide migration to capitalize older configurations
scheme = upper(scheme)
end
local www_auth_content = conf.realm and fmt('%s realm="%s"', scheme, conf.realm) or scheme
-- If both headers are missing, return 401
if not (authorization_value or proxy_authorization_value) then
return false, unauthorized("Unauthorized", www_auth_content)
end
local is_authorized, credential
if proxy_authorization_value then
is_authorized, credential = authenticate(conf, proxy_authorization_value)
end
if not is_authorized and authorization_value then
is_authorized, credential = authenticate(conf, authorization_value)
end
if not is_authorized then
return false, unauthorized("Unauthorized", www_auth_content)
end
if conf.hide_credentials then
kong.service.request.clear_header(AUTHORIZATION)
kong.service.request.clear_header(PROXY_AUTHORIZATION)
end
set_consumer(nil, 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 _M.execute(conf)
if conf.anonymous then
return logical_OR_authentication(conf)
else
return logical_AND_authentication(conf)
end
end
return _M