339 lines
9.1 KiB
Lua
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
|