414 lines
12 KiB
Lua
414 lines
12 KiB
Lua
local constants = require "kong.constants"
|
|
local openssl_mac = require "resty.openssl.mac"
|
|
|
|
|
|
local sha256_base64 = require("kong.tools.sha256").sha256_base64
|
|
local splitn = require("kong.tools.string").splitn
|
|
|
|
|
|
local ngx = ngx
|
|
local kong = kong
|
|
local error = error
|
|
local time = ngx.time
|
|
local abs = math.abs
|
|
local decode_base64 = ngx.decode_base64
|
|
local parse_time = ngx.parse_http_time
|
|
local re_gmatch = ngx.re.gmatch
|
|
local hmac_sha1 = ngx.hmac_sha1
|
|
local ipairs = ipairs
|
|
local fmt = string.format
|
|
local string_lower = string.lower
|
|
local kong_request = kong.request
|
|
local kong_client = kong.client
|
|
local kong_service_request = kong.service.request
|
|
|
|
|
|
local AUTHORIZATION = "authorization"
|
|
local PROXY_AUTHORIZATION = "proxy-authorization"
|
|
local DATE = "date"
|
|
local X_DATE = "x-date"
|
|
local DIGEST = "digest"
|
|
local SIGNATURE_NOT_VALID = "HMAC signature cannot be verified"
|
|
local SIGNATURE_NOT_SAME = "HMAC signature does not match"
|
|
|
|
|
|
local hmac = {
|
|
["hmac-sha1"] = function(secret, data)
|
|
return hmac_sha1(secret, data)
|
|
end,
|
|
["hmac-sha256"] = function(secret, data)
|
|
return openssl_mac.new(secret, "HMAC", nil, "sha256"):final(data)
|
|
end,
|
|
["hmac-sha384"] = function(secret, data)
|
|
return openssl_mac.new(secret, "HMAC", nil, "sha384"):final(data)
|
|
end,
|
|
["hmac-sha512"] = function(secret, data)
|
|
return openssl_mac.new(secret, "HMAC", nil, "sha512"):final(data)
|
|
end,
|
|
}
|
|
|
|
|
|
local function list_as_set(list)
|
|
local set = kong.table.new(0, #list)
|
|
for _, v in ipairs(list) do
|
|
set[v] = true
|
|
end
|
|
|
|
return set
|
|
end
|
|
|
|
|
|
local function validate_params(params, conf)
|
|
-- check username and signature are present
|
|
if not params.username or not params.signature then
|
|
return nil, "username or signature missing"
|
|
end
|
|
|
|
-- check enforced headers are present
|
|
if conf.enforce_headers and #conf.enforce_headers >= 1 then
|
|
local enforced_header_set = list_as_set(conf.enforce_headers)
|
|
|
|
if params.hmac_headers then
|
|
for _, header in ipairs(params.hmac_headers) do
|
|
enforced_header_set[header] = nil
|
|
end
|
|
end
|
|
|
|
for _, header in ipairs(conf.enforce_headers) do
|
|
if enforced_header_set[header] then
|
|
return nil, "enforced header not used for signature creation"
|
|
end
|
|
end
|
|
end
|
|
|
|
-- check supported alorithm used
|
|
for _, algo in ipairs(conf.algorithms) do
|
|
if algo == params.algorithm then
|
|
return true
|
|
end
|
|
end
|
|
|
|
return nil, fmt("algorithm %s not supported", params.algorithm)
|
|
end
|
|
|
|
|
|
local function retrieve_hmac_fields(authorization_header)
|
|
local hmac_params = {}
|
|
|
|
-- parse the header to retrieve hamc parameters
|
|
if authorization_header then
|
|
local iterator, iter_err = re_gmatch(authorization_header,
|
|
"\\s*[Hh]mac\\s*username=\"(.+)\"," ..
|
|
"\\s*algorithm=\"(.+)\",\\s*header" ..
|
|
"s=\"(.+)\",\\s*signature=\"(.+)\"",
|
|
"jo")
|
|
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 >= 4 then
|
|
hmac_params.username = m[1]
|
|
hmac_params.algorithm = m[2]
|
|
hmac_params.hmac_headers = splitn(m[3], " ")
|
|
hmac_params.signature = m[4]
|
|
end
|
|
end
|
|
|
|
return hmac_params
|
|
end
|
|
|
|
|
|
-- plugin assumes the request parameters being used for creating
|
|
-- signature by client are not changed by core or any other plugin
|
|
local function create_hash(request_uri, hmac_params)
|
|
local signing_string = ""
|
|
local hmac_headers = hmac_params.hmac_headers
|
|
|
|
local count = #hmac_headers
|
|
for i = 1, count do
|
|
local header = hmac_headers[i]
|
|
local header_value = kong.request.get_header(header)
|
|
|
|
if not header_value then
|
|
if header == "@request-target" then
|
|
local request_target = string_lower(kong.request.get_method()) .. " " .. request_uri
|
|
signing_string = signing_string .. header .. ": " .. request_target
|
|
|
|
elseif header == "request-line" then
|
|
-- request-line in hmac headers list
|
|
local request_line = fmt("%s %s HTTP/%.01f",
|
|
kong_request.get_method(),
|
|
request_uri,
|
|
assert(kong_request.get_http_version()))
|
|
signing_string = signing_string .. request_line
|
|
|
|
else
|
|
signing_string = signing_string .. header .. ":"
|
|
end
|
|
|
|
else
|
|
signing_string = signing_string .. header .. ":" .. " " .. header_value
|
|
end
|
|
|
|
if i < count then
|
|
signing_string = signing_string .. "\n"
|
|
end
|
|
end
|
|
|
|
return hmac[hmac_params.algorithm](hmac_params.secret, signing_string)
|
|
end
|
|
|
|
|
|
local function validate_signature(hmac_params)
|
|
local signature_1 = create_hash(kong_request.get_path_with_query(), hmac_params)
|
|
local signature_2 = decode_base64(hmac_params.signature)
|
|
return signature_1 == signature_2
|
|
end
|
|
|
|
|
|
local function load_credential_into_memory(username)
|
|
local key, err = kong.db.hmacauth_credentials:select_by_username(username)
|
|
if err then
|
|
return nil, err
|
|
end
|
|
return key
|
|
end
|
|
|
|
|
|
local function load_credential(username)
|
|
local credential, err
|
|
if username then
|
|
local credential_cache_key = kong.db.hmacauth_credentials:cache_key(username)
|
|
credential, err = kong.cache:get(credential_cache_key, nil,
|
|
load_credential_into_memory,
|
|
username)
|
|
end
|
|
|
|
if err then
|
|
return error(err)
|
|
end
|
|
|
|
return credential
|
|
end
|
|
|
|
|
|
local function validate_clock_skew(date_header_name, allowed_clock_skew)
|
|
local date = kong_request.get_header(date_header_name)
|
|
if not date then
|
|
return false
|
|
end
|
|
|
|
local requestTime = parse_time(date)
|
|
if not requestTime then
|
|
return false
|
|
end
|
|
|
|
local skew = abs(time() - requestTime)
|
|
if skew > allowed_clock_skew then
|
|
return false
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
local function validate_body()
|
|
local body, err = kong_request.get_raw_body()
|
|
if err then
|
|
kong.log.debug(err)
|
|
return false
|
|
end
|
|
|
|
local digest_received = kong_request.get_header(DIGEST)
|
|
if not digest_received then
|
|
-- if there is no digest and no body, it is ok
|
|
return body == ""
|
|
end
|
|
|
|
local digest_created = "SHA-256=" .. sha256_base64(body or '')
|
|
|
|
return digest_created == digest_received
|
|
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, www_auth_content)
|
|
return { status = 401, message = message, headers = { ["WWW-Authenticate"] = www_auth_content } }
|
|
end
|
|
|
|
|
|
local function do_authentication(conf)
|
|
local authorization = kong_request.get_header(AUTHORIZATION)
|
|
local proxy_authorization = kong_request.get_header(PROXY_AUTHORIZATION)
|
|
local www_auth_content = conf.realm and fmt('hmac realm="%s"', conf.realm) or 'hmac'
|
|
|
|
-- If both headers are missing, return 401
|
|
if not (authorization or proxy_authorization) then
|
|
return false, unauthorized("Unauthorized", www_auth_content)
|
|
end
|
|
|
|
-- validate clock skew
|
|
if not (validate_clock_skew(X_DATE, conf.clock_skew) or
|
|
validate_clock_skew(DATE, conf.clock_skew)) then
|
|
return false, unauthorized(
|
|
"HMAC signature cannot be verified, a valid date or " ..
|
|
"x-date header is required for HMAC Authentication",
|
|
www_auth_content
|
|
)
|
|
end
|
|
|
|
-- retrieve hmac parameter from Proxy-Authorization header
|
|
local hmac_params = retrieve_hmac_fields(proxy_authorization)
|
|
|
|
-- Try with the authorization header
|
|
if not hmac_params.username then
|
|
hmac_params = retrieve_hmac_fields(authorization)
|
|
if hmac_params and conf.hide_credentials then
|
|
kong_service_request.clear_header(AUTHORIZATION)
|
|
end
|
|
|
|
elseif conf.hide_credentials then
|
|
kong_service_request.clear_header(PROXY_AUTHORIZATION)
|
|
end
|
|
|
|
local ok, err = validate_params(hmac_params, conf)
|
|
if not ok then
|
|
kong.log.debug(err)
|
|
return false, unauthorized(SIGNATURE_NOT_VALID, www_auth_content)
|
|
end
|
|
|
|
-- validate signature
|
|
local credential = load_credential(hmac_params.username)
|
|
if not credential then
|
|
kong.log.debug("failed to retrieve credential for ", hmac_params.username)
|
|
return false, unauthorized(SIGNATURE_NOT_VALID, www_auth_content)
|
|
end
|
|
|
|
hmac_params.secret = credential.secret
|
|
|
|
if not validate_signature(hmac_params) then
|
|
return false, unauthorized(SIGNATURE_NOT_SAME, www_auth_content)
|
|
end
|
|
|
|
-- If request body validation is enabled, then verify digest.
|
|
if conf.validate_request_body and not validate_body() then
|
|
kong.log.debug("digest validation failed")
|
|
return false, unauthorized(SIGNATURE_NOT_SAME, www_auth_content)
|
|
end
|
|
|
|
-- Retrieve consumer
|
|
local consumer_cache_key, consumer
|
|
consumer_cache_key = kong.db.consumers:cache_key(credential.consumer.id)
|
|
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
|
|
|
|
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
|
|
|
|
|
|
local _M = {}
|
|
|
|
function _M.execute(conf)
|
|
|
|
if conf.anonymous then
|
|
return logical_OR_authentication(conf)
|
|
else
|
|
return logical_AND_authentication(conf)
|
|
end
|
|
end
|
|
|
|
|
|
return _M
|