239 lines
6.8 KiB
Lua
239 lines
6.8 KiB
Lua
local crypto = require "kong.plugins.basic-auth.crypto"
|
|
local constants = require "kong.constants"
|
|
|
|
|
|
local crypto_hash = crypto.hash
|
|
local decode_base64 = ngx.decode_base64
|
|
local re_gmatch = ngx.re.gmatch
|
|
local re_match = ngx.re.match
|
|
local error = error
|
|
local kong = kong
|
|
|
|
|
|
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 _M = {}
|
|
|
|
|
|
-- Fast lookup for credential retrieval depending on the type of the authentication
|
|
--
|
|
-- All methods must respect:
|
|
--
|
|
-- @param request ngx request object
|
|
-- @param {table} conf Plugin config
|
|
-- @return {string} public_key
|
|
-- @return {string} private_key
|
|
local function retrieve_credentials(header_name, conf, header)
|
|
local username, password
|
|
local authorization_header = header or kong.request.get_header(header_name)
|
|
|
|
if authorization_header then
|
|
local iterator, iter_err = re_gmatch(authorization_header, "\\s*[Bb]asic\\s*(.+)", "oj")
|
|
if not iterator then
|
|
kong.log.err(iter_err)
|
|
return
|
|
end
|
|
|
|
local m, err = iterator()
|
|
if err then
|
|
kong.log.err(err)
|
|
return
|
|
end
|
|
|
|
if m and m[1] then
|
|
local decoded_basic = decode_base64(m[1])
|
|
if decoded_basic then
|
|
local basic_parts, err = re_match(decoded_basic, "([^:]+):(.*)", "oj")
|
|
if err then
|
|
kong.log.err(err)
|
|
return
|
|
end
|
|
|
|
if not basic_parts then
|
|
kong.log.err("header has unrecognized format")
|
|
return
|
|
end
|
|
|
|
username = basic_parts[1]
|
|
password = basic_parts[2]
|
|
end
|
|
end
|
|
end
|
|
|
|
if conf.hide_credentials then
|
|
kong.service.request.clear_header(header_name)
|
|
end
|
|
|
|
return username, password
|
|
end
|
|
|
|
|
|
--- Validate a credential in the Authorization header against one fetched from the database.
|
|
-- @param credential The retrieved credential from the username passed in the request
|
|
-- @param given_password The password as given in the Authorization header
|
|
-- @return Success of authentication
|
|
local function validate_credentials(credential, given_password)
|
|
local digest, err = crypto_hash(credential.consumer.id, given_password)
|
|
if err then
|
|
kong.log.err(err)
|
|
end
|
|
|
|
return credential.password == digest
|
|
end
|
|
|
|
|
|
local function load_credential_into_memory(username)
|
|
local credential, err = kong.db.basicauth_credentials:select_by_username(username)
|
|
if err then
|
|
return nil, err
|
|
end
|
|
return credential
|
|
end
|
|
|
|
|
|
local function load_credential_from_db(username)
|
|
if not username then
|
|
return
|
|
end
|
|
|
|
local credential_cache_key = kong.db.basicauth_credentials:cache_key(username)
|
|
local credential, err = kong.cache:get(credential_cache_key, nil,
|
|
load_credential_into_memory,
|
|
username)
|
|
if err then
|
|
return error(err)
|
|
end
|
|
|
|
return 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(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.username then
|
|
set_header(HEADERS_CREDENTIAL_IDENTIFIER, credential.username)
|
|
else
|
|
clear_header(HEADERS_CREDENTIAL_IDENTIFIER)
|
|
end
|
|
|
|
if credential then
|
|
clear_header(HEADERS_ANONYMOUS)
|
|
else
|
|
set_header(HEADERS_ANONYMOUS, true)
|
|
end
|
|
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)
|
|
local www_authenticate = "Basic realm=\"" .. conf.realm .. "\""
|
|
local authorization = kong.request.get_header("authorization")
|
|
local proxy_authorization = kong.request.get_header("proxy-authorization")
|
|
|
|
-- If both headers are missing, return 401
|
|
if not (authorization or proxy_authorization) then
|
|
return false, unauthorized("Unauthorized", www_authenticate)
|
|
end
|
|
|
|
local credential
|
|
local given_username, given_password = retrieve_credentials("proxy-authorization", conf, proxy_authorization)
|
|
if given_username and given_password then
|
|
credential = load_credential_from_db(given_username)
|
|
end
|
|
|
|
-- Try with the authorization header
|
|
if not credential then
|
|
given_username, given_password = retrieve_credentials("authorization", conf, authorization)
|
|
if given_username and given_password then
|
|
credential = load_credential_from_db(given_username)
|
|
else
|
|
return false, unauthorized("Unauthorized", www_authenticate)
|
|
end
|
|
end
|
|
|
|
if not credential or not validate_credentials(credential, given_password) then
|
|
return false, unauthorized("Unauthorized", www_authenticate)
|
|
end
|
|
|
|
-- Retrieve consumer
|
|
local consumer_cache_key = kong.db.consumers:cache_key(credential.consumer.id)
|
|
local consumer, err = kong.cache:get(consumer_cache_key, nil,
|
|
kong.client.load_consumer,
|
|
credential.consumer.id)
|
|
if err then
|
|
return error(err)
|
|
end
|
|
|
|
set_consumer(consumer, credential)
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
function _M.execute(conf)
|
|
if conf.anonymous and kong.client.get_credential() then
|
|
-- we're already authenticated, and we're configured for using anonymous,
|
|
-- hence we're in a logical OR between auth methods and we're already done.
|
|
return
|
|
end
|
|
|
|
local ok, err = do_authentication(conf)
|
|
if not ok then
|
|
if conf.anonymous then
|
|
-- get anonymous user
|
|
local consumer_cache_key = kong.db.consumers:cache_key(conf.anonymous)
|
|
local consumer, err = kong.cache:get(consumer_cache_key, nil,
|
|
kong.client.load_consumer,
|
|
conf.anonymous, true)
|
|
if err then
|
|
return error(err)
|
|
end
|
|
|
|
if not consumer then
|
|
local err_msg = "anonymous consumer " .. conf.anonymous .. " is configured but doesn't exist"
|
|
kong.log.err(err_msg)
|
|
return kong.response.error(500, err_msg)
|
|
end
|
|
|
|
set_consumer(consumer)
|
|
|
|
else
|
|
return kong.response.error(err.status, err.message, err.headers)
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
return _M
|