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