apisix/plugins/hmac-auth.lua (294 lines of code) (raw):
--
-- Licensed to the Apache Software Foundation (ASF) under one or more
-- contributor license agreements. See the NOTICE file distributed with
-- this work for additional information regarding copyright ownership.
-- The ASF licenses this file to You under the Apache License, Version 2.0
-- (the "License"); you may not use this file except in compliance with
-- the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
local ngx = ngx
local abs = math.abs
local ngx_time = ngx.time
local ngx_re = require("ngx.re")
local ipairs = ipairs
local hmac_sha1 = ngx.hmac_sha1
local core = require("apisix.core")
local hmac = require("resty.hmac")
local consumer = require("apisix.consumer")
local ngx_decode_base64 = ngx.decode_base64
local ngx_encode_base64 = ngx.encode_base64
local plugin_name = "hmac-auth"
local ALLOWED_ALGORITHMS = {"hmac-sha1", "hmac-sha256", "hmac-sha512"}
local resty_sha256 = require("resty.sha256")
local schema_def = require("apisix.schema_def")
local auth_utils = require("apisix.utils.auth")
local schema = {
type = "object",
title = "work with route or service object",
properties = {
allowed_algorithms = {
type = "array",
minItems = 1,
items = {
type = "string",
enum = ALLOWED_ALGORITHMS
},
default = ALLOWED_ALGORITHMS,
},
clock_skew = {
type = "integer",
default = 300,
minimum = 1
},
signed_headers = {
type = "array",
items = {
type = "string",
minLength = 1,
maxLength = 50,
}
},
validate_request_body = {
type = "boolean",
title = "A boolean value telling the plugin to enable body validation",
default = false,
},
hide_credentials = {type = "boolean", default = false},
anonymous_consumer = schema_def.anonymous_consumer_schema,
},
}
local consumer_schema = {
type = "object",
title = "work with consumer object",
properties = {
key_id = {type = "string", minLength = 1, maxLength = 256},
secret_key = {type = "string", minLength = 1, maxLength = 256},
},
encrypt_fields = {"secret_key"},
required = {"key_id", "secret_key"},
}
local _M = {
version = 0.1,
priority = 2530,
type = 'auth',
name = plugin_name,
schema = schema,
consumer_schema = consumer_schema
}
local hmac_funcs = {
["hmac-sha1"] = function(secret_key, message)
return hmac_sha1(secret_key, message)
end,
["hmac-sha256"] = function(secret_key, message)
return hmac:new(secret_key, hmac.ALGOS.SHA256):final(message)
end,
["hmac-sha512"] = function(secret_key, message)
return hmac:new(secret_key, hmac.ALGOS.SHA512):final(message)
end,
}
local function array_to_map(arr)
local map = core.table.new(0, #arr)
for _, v in ipairs(arr) do
map[v] = true
end
return map
end
function _M.check_schema(conf, schema_type)
core.log.info("input conf: ", core.json.delay_encode(conf))
if schema_type == core.schema.TYPE_CONSUMER then
return core.schema.check(consumer_schema, conf)
else
return core.schema.check(schema, conf)
end
end
local function get_consumer(key_id)
if not key_id then
return nil, "missing key_id"
end
local cur_consumer, _, err = consumer.find_consumer(plugin_name, "key_id", key_id)
if not cur_consumer then
return nil, err or "Invalid key_id"
end
core.log.info("consumer: ", core.json.delay_encode(consumer, true))
return cur_consumer
end
local function generate_signature(ctx, secret_key, params)
local uri = ctx.var.request_uri
local request_method = core.request.get_method()
if uri == "" then
uri = "/"
end
local signing_string_items = {
params.keyId,
}
if params.headers then
for _, h in ipairs(params.headers) do
local canonical_header = core.request.header(ctx, h)
if not canonical_header then
if h == "@request-target" then
local request_target = request_method .. " " .. uri
core.table.insert(signing_string_items, request_target)
core.log.info("canonical_header name:", core.json.delay_encode(h))
core.log.info("canonical_header value: ",
core.json.delay_encode(request_target))
end
else
core.table.insert(signing_string_items,
h .. ": " .. canonical_header)
core.log.info("canonical_header name:", core.json.delay_encode(h))
core.log.info("canonical_header value: ",
core.json.delay_encode(canonical_header))
end
end
end
local signing_string = core.table.concat(signing_string_items, "\n") .. "\n"
return hmac_funcs[params.algorithm](secret_key, signing_string)
end
local function sha256(key)
local hash = resty_sha256:new()
hash:update(key)
local digest = hash:final()
return digest
end
local function validate(ctx, conf, params)
if not params then
return nil
end
if not params.keyId or not params.signature then
return nil, "keyId or signature missing"
end
if not params.algorithm then
return nil, "algorithm missing"
end
local consumer, err = get_consumer(params.keyId)
if err then
return nil, err
end
local consumer_conf = consumer.auth_conf
local found_algorithm = false
-- check supported algorithm used
if not conf.allowed_algorithms then
conf.allowed_algorithms = ALLOWED_ALGORITHMS
end
for _, algo in ipairs(conf.allowed_algorithms) do
if algo == params.algorithm then
found_algorithm = true
break
end
end
if not found_algorithm then
return nil, "Invalid algorithm"
end
core.log.info("clock_skew: ", conf.clock_skew)
if conf.clock_skew and conf.clock_skew > 0 then
if not params.date then
return nil, "Date header missing. failed to validate clock skew"
end
local time = ngx.parse_http_time(params.date)
core.log.info("params.date: ", params.date, " time: ", time)
if not time then
return nil, "Invalid GMT format time"
end
local diff = abs(ngx_time() - time)
if diff > conf.clock_skew then
return nil, "Clock skew exceeded"
end
end
-- validate headers
-- All headers passed in route conf.signed_headers must be used in signing(params.headers)
if conf.signed_headers and #conf.signed_headers >= 1 then
if not params.headers then
return nil, "headers missing"
end
local params_headers_map = array_to_map(params.headers)
if params_headers_map then
for _, header in ipairs(conf.signed_headers) do
if not params_headers_map[header] then
return nil, [[expected header "]] .. header .. [[" missing in signing]]
end
end
end
end
local secret_key = consumer_conf and consumer_conf.secret_key
local request_signature = ngx_decode_base64(params.signature)
local generated_signature = generate_signature(ctx, secret_key, params)
if request_signature ~= generated_signature then
return nil, "Invalid signature"
end
local validate_request_body = conf.validate_request_body
if validate_request_body then
local digest_header = params.body_digest
if not digest_header then
return nil, "Invalid digest"
end
local req_body, err = core.request.get_body()
if err then
return nil, err
end
req_body = req_body or ""
local digest_created = "SHA-256" .. "=" ..
ngx_encode_base64(sha256(req_body))
if digest_created ~= digest_header then
return nil, "Invalid digest"
end
end
return consumer
end
local function retrieve_hmac_fields(ctx)
local hmac_params = {}
local auth_string = core.request.header(ctx, "Authorization")
if not auth_string then
return nil, "missing Authorization header"
end
if not core.string.has_prefix(auth_string, "Signature") then
return nil, "Authorization header does not start with 'Signature'"
end
local signature_fields = auth_string:sub(10):gmatch('[^,]+')
for field in signature_fields do
local key, value = field:match('%s*(%w+)="(.-)"')
if key and value then
if key == "keyId" or key == "algorithm" or key == "signature" then
hmac_params[key] = value
elseif key == "headers" then
hmac_params.headers = ngx_re.split(value, " ")
end
end
end
-- will be required to check clock skew
if core.request.header(ctx, "Date") then
hmac_params.date = core.request.header(ctx, "Date")
end
if core.request.header(ctx, "Digest") then
hmac_params.body_digest = core.request.header(ctx, "Digest")
end
return hmac_params
end
local function find_consumer(conf, ctx)
local params,err = retrieve_hmac_fields(ctx)
if err then
if not auth_utils.is_running_under_multi_auth(ctx) then
core.log.warn("client request can't be validated: ", err)
end
return nil, nil, "client request can't be validated: " .. err
end
local validated_consumer, err = validate(ctx, conf, params)
if not validated_consumer then
err = "client request can't be validated: " .. (err or "Invalid signature")
if auth_utils.is_running_under_multi_auth(ctx) then
return nil, nil, err
end
core.log.warn(err)
return nil, nil, "client request can't be validated"
end
local consumers_conf = consumer.consumers_conf(plugin_name)
return validated_consumer, consumers_conf, err
end
function _M.rewrite(conf, ctx)
local cur_consumer, consumers_conf, err = find_consumer(conf, ctx)
if not cur_consumer then
if not conf.anonymous_consumer then
return 401, { message = err }
end
cur_consumer, consumers_conf, err = consumer.get_anonymous_consumer(conf.anonymous_consumer)
if not cur_consumer then
if auth_utils.is_running_under_multi_auth(ctx) then
return 401, err
end
core.log.error(err)
return 401, { message = "Invalid user authorization" }
end
end
if conf.hide_credentials then
core.request.set_header("Authorization", nil)
end
consumer.attach_consumer(ctx, cur_consumer, consumers_conf)
end
return _M