This is an automated email from the ASF dual-hosted git repository. wenming pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/apisix.git
The following commit(s) were added to refs/heads/master by this push: new 84ce7ba feat: add AK/SK(HMAC) auth plugin. (#2192) 84ce7ba is described below commit 84ce7ba781affbf93c136ea5d3e2e12460ebe4ed Author: nic-chen <33000667+nic-c...@users.noreply.github.com> AuthorDate: Wed Sep 16 14:27:56 2020 +0800 feat: add AK/SK(HMAC) auth plugin. (#2192) --- apisix/core/table.lua | 1 + apisix/plugins/hmac-auth.lua | 303 +++++++++++++++++++++ conf/config-default.yaml | 1 + doc/plugins/hmac-auth.md | 151 +++++++++++ doc/zh-cn/plugins/hmac-auth.md | 152 +++++++++++ t/APISIX.pm | 12 + t/admin/plugins.t | 2 +- t/debug/debug-mode.t | 1 + t/plugin/custom_hmac_auth.t | 364 +++++++++++++++++++++++++ t/plugin/hmac-auth.t | 596 +++++++++++++++++++++++++++++++++++++++++ 10 files changed, 1582 insertions(+), 1 deletion(-) diff --git a/apisix/core/table.lua b/apisix/core/table.lua index 5c84164..4ad92ca 100644 --- a/apisix/core/table.lua +++ b/apisix/core/table.lua @@ -32,6 +32,7 @@ local _M = { nkeys = nkeys, insert = table.insert, concat = table.concat, + sort = table.sort, clone = require("table.clone"), isarray = require("table.isarray"), } diff --git a/apisix/plugins/hmac-auth.lua b/apisix/plugins/hmac-auth.lua new file mode 100644 index 0000000..ad578c4 --- /dev/null +++ b/apisix/plugins/hmac-auth.lua @@ -0,0 +1,303 @@ +-- +-- 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 type = type +local select = select +local abs = math.abs +local ngx_time = ngx.time +local ngx_re = require("ngx.re") +local ngx_req = ngx.req +local pairs = pairs +local ipairs = ipairs +local hmac_sha1 = ngx.hmac_sha1 +local escape_uri = ngx.escape_uri +local core = require("apisix.core") +local hmac = require("resty.hmac") +local consumer = require("apisix.consumer") +local ngx_decode_base64 = ngx.decode_base64 + +local SIGNATURE_KEY = "X-HMAC-SIGNATURE" +local ALGORITHM_KEY = "X-HMAC-ALGORITHM" +local TIMESTAMP_KEY = "X-HMAC-TIMESTAMP" +local ACCESS_KEY = "X-HMAC-ACCESS-KEY" +local plugin_name = "hmac-auth" + +local schema = { + type = "object", + oneOf = { + { + title = "work with consumer object", + properties = { + access_key = {type = "string", minLength = 1, maxLength = 256}, + secret_key = {type = "string", minLength = 1, maxLength = 256}, + algorithm = { + type = "string", + enum = {"hmac-sha1", "hmac-sha256", "hmac-sha512"}, + default = "hmac-sha256" + }, + clock_skew = { + type = "integer", + default = 300 + } + }, + required = {"access_key", "secret_key"}, + additionalProperties = false, + }, + { + title = "work with route or service object", + properties = {}, + additionalProperties = false, + } + } +} + +local _M = { + version = 0.1, + priority = 2530, + type = 'auth', + name = plugin_name, + schema = 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 try_attr(t, ...) + local tbl = t + local count = select('#', ...) + for i = 1, count do + local attr = select(i, ...) + tbl = tbl[attr] + if type(tbl) ~= "table" then + return false + end + end + + return true +end + + +local create_consumer_cache +do + local consumer_ids = {} + + function create_consumer_cache(consumers) + core.table.clear(consumer_ids) + + for _, consumer in ipairs(consumers.nodes) do + core.log.info("consumer node: ", core.json.delay_encode(consumer)) + consumer_ids[consumer.auth_conf.access_key] = consumer + end + + return consumer_ids + end + +end -- do + + +function _M.check_schema(conf) + core.log.info("input conf: ", core.json.delay_encode(conf)) + + return core.schema.check(schema, conf) +end + + +local function get_consumer(access_key) + if not access_key then + return nil, {message = "missing access key"} + end + + local consumer_conf = consumer.plugin(plugin_name) + if not consumer_conf then + return nil, {message = "Missing related consumer"} + end + + local consumers = core.lrucache.plugin(plugin_name, "consumers_key", + consumer_conf.conf_version, + create_consumer_cache, consumer_conf) + + local consumer = consumers[access_key] + if not consumer then + return nil, {message = "Invalid access key"} + end + core.log.info("consumer: ", core.json.delay_encode(consumer)) + + return consumer +end + + +local function generate_signature(ctx, secret_key, params) + local canonical_uri = ctx.var.uri + local canonical_query_string = "" + local request_method = ngx_req.get_method() + local args = ngx_req.get_uri_args() + + if canonical_uri == "" then + canonical_uri = "/" + end + + if type(args) == "table" then + local keys = {} + local query_tab = {} + + for k, v in pairs(args) do + core.table.insert(keys, k) + end + core.table.sort(keys) + + for _, key in pairs(keys) do + local param = args[key] + if type(param) == "table" then + for _, val in pairs(param) do + core.table.insert(query_tab, escape_uri(key) .. "=" .. escape_uri(val)) + end + else + core.table.insert(query_tab, escape_uri(key) .. "=" .. escape_uri(param)) + end + end + canonical_query_string = core.table.concat(query_tab, "&") + end + + local req_body = core.request.get_body() + req_body = req_body or "" + + local signing_string = request_method .. canonical_uri + .. canonical_query_string .. req_body + .. params.access_key .. params.timestamp + .. secret_key + + return hmac_funcs[params.algorithm](secret_key, signing_string) +end + + +local function validate(ctx, params) + if not params.access_key or not params.signature then + return nil, {message = "access key or signature missing"} + end + + local consumer, err = get_consumer(params.access_key) + if err then + return nil, err + end + + local conf = consumer.auth_conf + if conf.algorithm ~= params.algorithm then + return nil, {message = "algorithm " .. params.algorithm .. " not supported"} + end + + core.log.info("conf.clock_skew: ", conf.clock_skew) + if conf.clock_skew and conf.clock_skew > 0 then + local diff = abs(ngx_time() - params.timestamp) + core.log.info("conf.diff: ", diff) + if diff > conf.clock_skew then + return nil, {message = "Invalid timestamp"} + end + end + + local secret_key = conf and conf.secret_key + local request_signature = ngx_decode_base64(params.signature) + local generated_signature = generate_signature(ctx, secret_key, params) + + core.log.info("request_signature: ", request_signature, + " generated_signature: ", generated_signature) + + if request_signature ~= generated_signature then + return nil, {message = "Invalid signature"} + end + + return consumer +end + +local function get_params(ctx) + local params = {} + local local_conf = core.config.local_conf() + local signature_key = SIGNATURE_KEY + local algorithm_key = ALGORITHM_KEY + local timestamp_key = TIMESTAMP_KEY + local access_key = ACCESS_KEY + + if try_attr(local_conf, "plugin_attr", "hmac-auth") then + local attr = local_conf.plugin_attr["hmac-auth"] + signature_key = attr.signature_key or signature_key + algorithm_key = attr.algorithm_key or algorithm_key + timestamp_key = attr.timestamp_key or timestamp_key + access_key = attr.access_key or access_key + end + + local ak = core.request.header(ctx, access_key) + local signature = core.request.header(ctx, signature_key) + local algorithm = core.request.header(ctx, algorithm_key) + local timestamp = core.request.header(ctx, timestamp_key) + core.log.info("signature_key: ", signature_key) + + -- get params from header `Authorization` + if not ak then + local auth_string = core.request.header(ctx, "Authorization") + if not auth_string then + return params + end + + local auth_data = ngx_re.split(auth_string, "#") + core.log.info("auth_string: ", auth_string, " #auth_data: ", + #auth_data, " auth_data: ", core.json.delay_encode(auth_data)) + if #auth_data == 5 and auth_data[1] == "hmac-auth-v1" then + ak = auth_data[2] + signature = auth_data[3] + algorithm = auth_data[4] + timestamp = auth_data[5] + end + end + + params.access_key = ak + params.algorithm = algorithm + params.signature = signature + params.timestamp = timestamp or 0 + + return params +end + + +function _M.rewrite(conf, ctx) + local params = get_params(ctx) + local validated_consumer, err = validate(ctx, params) + if err then + return 401, err + end + + if not validated_consumer then + return 401, {message = "Invalid signature"} + end + + local consumer_conf = consumer.plugin(plugin_name) + ctx.consumer = validated_consumer + ctx.consumer_id = validated_consumer.consumer_id + ctx.consumer_ver = consumer_conf.conf_version + core.log.info("hit hmac-auth rewrite") +end + + +return _M diff --git a/conf/config-default.yaml b/conf/config-default.yaml index f6a4710..6e09012 100644 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -191,6 +191,7 @@ plugins: # plugin list - proxy-cache - proxy-mirror - request-id + - hmac-auth stream_plugins: - mqtt-proxy diff --git a/doc/plugins/hmac-auth.md b/doc/plugins/hmac-auth.md new file mode 100644 index 0000000..af3c671 --- /dev/null +++ b/doc/plugins/hmac-auth.md @@ -0,0 +1,151 @@ +<!-- +# +# 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. +# +--> + +- [中文](../zh-cn/plugins/hmac-auth.md) + +# Summary +- [**Name**](#name) +- [**Attributes**](#attributes) +- [**How To Enable**](#how-to-enable) +- [**Test Plugin**](#test-plugin) +- [**Disable Plugin**](#disable-plugin) + + +## Name + +`hmac-auth` is an authentication plugin that need to work with `consumer`. Add HMAC Authentication to a `service` or `route`. + +The `consumer` then adds its key to request header to verify its request. + +## Attributes + +|Name |Requirement |Default |Description| +|--------- |--------|-----------|-----------| +| access_key | required | none |Different `consumer` objects should have different values, and it should be unique. If different consumers use the same `access_key`, a request matching exception will occur| +| secret_key | required | none |Use as a pair with `access_key`| +| algorithm | optional| hmac-sha256 |Encryption algorithm. support `hmac-sha1`, `hmac-sha256` and `hmac-sha512`| +| clock_skew | optional | 300 |The clock skew allowed by the signature in seconds. For example, if the time is allowed to skew by 10 seconds, then it should be set to `10`. especially, `0` means not checking timestamp.| + +## How To Enable + +1. set a consumer and config the value of the `hmac-auth` option + +```shell +curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "username": "jack", + "plugins": { + "hmac-auth": { + "access_key": "user-key", + "secret_key": "my-secret-key", + "clock_skew": 10 + } + } +}' +``` + +2. add a Route or add a Service , and enable the `hmac-auth` plugin + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "uri": "/index.html", + "plugins": { + "hmac-auth": {} + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "39.97.63.215:80": 1 + } + } +}' +``` + +## Test Plugin + +### generate signature: +The calculation formula of the signature is `signature = HMAC-SHAx-HEX(secret_key, signning_string)`. From the formula, it can be seen that in order to obtain the signature, two parameters, `SECRET_KEY` and `SIGNNING_STRING`, are required. Where secret_key is configured by the corresponding consumer, the calculation formula of `SIGNNING_STRING` is `signning_string = HTTP Method + HTTP URI + canonical_query_string + HTTP BODY + ACCESS_KEY + TIMESTAMP + SECRET_KEY` + +1. **HTTP Method** : Refers to the GET, PUT, POST and other request methods defined in the HTTP protocol, and must be in all uppercase. +2. **HTTP URI** : `HTTP URI` requirements must start with "/", those that do not start with "/" need to be added, and the empty path is "/". +3. **canonical_query_string** :`canonical_query_string` is the result of encoding the `query` in the URL (`query` is the string "key1 = valve1 & key2 = valve2" after the "?" in the URL). + +> The coding steps are as follows: + +* Extract the `query` item in the URL, that is, the string "key1 = valve1 & key2 = valve2" after the "?" in the URL. +* Split the `query` into several items according to the & separator, each item is in the form of key=value or only key. +* Encoding each item after disassembly is divided into the following two situations. + + * When the item has only key, the conversion formula is UriEncode(key) + "=". + * When the item is in the form of key=value, the conversion formula is in the form of UriEncode(key) + "=" + UriEncode(value). Here value can be an empty string. + * After converting each item, sort by key in lexicographic order (ASCII code from small to large), and connect them with the & symbol to generate the corresponding canonical_query_string. + +### Use the generated signature to try the request + +**Note: ACCESS_KEY, SIGNATURE, ALGORITHM, TIMESTAMP respectively represent the corresponding variables** + +* The signature information is put together in the request header `Authorization` field: + +```shell +$ curl http://127.0.0.1:9080/index.html -H 'Authorization: hmac-auth-v1# + ACCESS_KEY + # + base64_encode(SIGNATURE) + # + ALGORITHM + # + TIMESTAMP' -i +HTTP/1.1 200 OK +Content-Type: text/html +Content-Length: 13175 +... +Accept-Ranges: bytes + +<!DOCTYPE html> +<html lang="cn"> +... +``` + +* The signature information is separately placed in the request header: + +```shell +$ curl http://127.0.0.1:9080/index.html -H 'X-HMAC-SIGNATURE: base64_encode(SIGNATURE)' -H 'X-HMAC-ALGORITHM: ALGORITHM' -H 'X-HMAC-TIMESTAMP: TIMESTAMP' -H 'X-HMAC-ACCESS-KEY: ACCESS_KEY' -i +HTTP/1.1 200 OK +Content-Type: text/html +Content-Length: 13175 +... +Accept-Ranges: bytes + +<!DOCTYPE html> +<html lang="cn"> +``` + +## Disable Plugin + +When you want to disable the `hmac-auth` plugin, it is very simple, +you can delete the corresponding json configuration in the plugin configuration, +no need to restart the service, it will take effect immediately: + +```shell +$ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "uri": "/index.html", + "plugins": {}, + "upstream": { + "type": "roundrobin", + "nodes": { + "39.97.63.215:80": 1 + } + } +}' +``` diff --git a/doc/zh-cn/plugins/hmac-auth.md b/doc/zh-cn/plugins/hmac-auth.md new file mode 100644 index 0000000..37305ee --- /dev/null +++ b/doc/zh-cn/plugins/hmac-auth.md @@ -0,0 +1,152 @@ +<!-- +# +# 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. +# +--> + +- [English](../../plugins/hmac-auth.md) + +# 目录 +- [**名字**](#名字) +- [**属性**](#属性) +- [**如何启用**](#如何启用) +- [**测试插件**](#测试插件) +- [**禁用插件**](#禁用插件) + + +## 名字 + +`hmac-auth` 是一个认证插件,它需要与 `consumer` 一起配合才能工作。 + +添加 HMAC Authentication 到一个 `service` 或 `route`。 然后 `consumer` 将其签名添加到请求头以验证其请求。 + +## 属性 + +|属性名 |是否可选 | 默认值 |描述| +|--------- |--------|-----------|-----------| +| `access_key` | 必须 | 无 | 不同的 `consumer` 对象应有不同的值,它应当是唯一的。不同 consumer 使用了相同的 `access_key` ,将会出现请求匹配异常。| +| `secret_key`| 必须 | 无 | 与 `access_key` 配对使用。| +| `algorithm` | 可选 | hmac-sha256 | 加密算法。目前支持 `hmac-sha1`, `hmac-sha256` 和 `hmac-sha512`。| +| `clock_skew`| 可选 | 300 | 签名允许的时间偏移,以秒为单位的计时。比如允许时间偏移 10 秒钟,那么就应设置为 `10`。特别地,`0` 表示不对 `timestamp` 进行检查。| + +## 如何启用 + +1. 创建一个 consumer 对象,并设置插件 `hmac-auth` 的值。 + +```shell +curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "username": "jack", + "plugins": { + "hmac-auth": { + "access_key": "user-key", + "secret_key": "my-secret-key", + "clock_skew": 10 + } + } +}' +``` + +2. 创建 Route 或 Service 对象,并开启 `hmac-auth` 插件。 + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "uri": "/index.html", + "plugins": { + "hmac-auth": {} + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "39.97.63.215:80": 1 + } + } +}' +``` + +## 测试插件 + +### 签名生成公式 + +签名的计算公式为 `signature = HMAC-SHAx-HEX(secret_key, signning_string)`,从公式可以看出,想要获得签名需要得到 `secret_key` 和 `signning_string` 两个参数。其中 `secret_key` 为对应 consumer 所配置的, `signning_string` 的计算公式为 `signning_string = HTTP Method + HTTP URI + canonical_query_string + HTTP BODY + access_key + timestamp + secret_key` + +1. **HTTP Method**:指 HTTP 协议中定义的 GET、PUT、POST 等请求方法,必须使用全大写的形式。 +2. **HTTP URI**:要求必须以“/”开头,不以“/”开头的需要补充上,空路径为“/”。 +3. **canonical_query_string**:是对于 URL 中的 query( query 即 URL 中 ? 后面的 key1=valve1&key2=valve2 字符串)进行编码后的结果。 + +> canonical_query_string 编码步骤如下: + +* 提取 URL 中的 query 项,即 URL 中 ? 后面的 key1=valve1&key2=valve2 字符串。 +* 将 query 根据&分隔符拆开成若干项,每一项是 key=value 或者只有 key 的形式。 +* 对拆开后的每一项进行编码处理,分以下两种情况: + * 当该项只有 key 时,转换公式为 url_encode(key) + "=" 的形式。 + * 当该项是 key=value 的形式时,转换公式为 url_encode(key) + "=" + url_encode(value) 的形式。这里 value 可以是空字符串。 + * 将每一项转换后,以 key 按照字典顺序( ASCII 码由小到大)排序,并使用 & 符号连接起来,生成相应的 canonical_query_string 。 + + + +### 使用生成好的签名进行请求尝试 + +**注: ACCESS_KEY,SIGNATURE,ALGORITHM,TIMESTAMP 分别代表对应的变量** + +* 签名信息拼一起放到请求头 `Authorization` 字段中: + +```shell +$ curl http://127.0.0.1:9080/index.html -H 'Authorization: hmac-auth-v1# + ACCESS_KEY + # + base64_encode(SIGNATURE) + # + ALGORITHM + # + TIMESTAMP' -i +HTTP/1.1 200 OK +Content-Type: text/html +Content-Length: 13175 +... +Accept-Ranges: bytes + +<!DOCTYPE html> +<html lang="cn"> +... +``` + +* 签名信息分开分别放到请求头: + +```shell +$ curl http://127.0.0.1:9080/index.html -H 'X-HMAC-SIGNATURE: base64_encode(SIGNATURE)' -H 'X-HMAC-ALGORITHM: ALGORITHM' -H 'X-HMAC-TIMESTAMP: TIMESTAMP' -H 'X-HMAC-ACCESS-KEY: ACCESS_KEY' -i +HTTP/1.1 200 OK +Content-Type: text/html +Content-Length: 13175 +... +Accept-Ranges: bytes + +<!DOCTYPE html> +<html lang="cn"> +``` + + +## 禁用插件 + +当你想去掉 `hmac-auth` 插件的时候,很简单,在插件的配置中把对应的 `json` 配置删除即可,无须重启服务,即刻生效: + +```shell +$ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "uri": "/index.html", + "plugins": {}, + "upstream": { + "type": "roundrobin", + "nodes": { + "39.97.63.215:80": 1 + } + } +}' +``` diff --git a/t/APISIX.pm b/t/APISIX.pm index 7434d43..b7127ed 100644 --- a/t/APISIX.pm +++ b/t/APISIX.pm @@ -96,6 +96,18 @@ etcd: _EOC_ } +my $custom_hmac_auth = $ENV{"CUSTOM_HMAC_AUTH"} || "false"; +if ($custom_hmac_auth eq "true") { + $user_yaml_config .= <<_EOC_; +plugin_attr: + hmac-auth: + signature_key: X-APISIX-HMAC-SIGNATURE + algorithm_key: X-APISIX-HMAC-ALGORITHM + timestamp_key: X-APISIX-HMAC-TIMESTAMP + access_key: X-APISIX-HMAC-ACCESS-KEY +_EOC_ +} + my $profile = $ENV{"APISIX_PROFILE"}; diff --git a/t/admin/plugins.t b/t/admin/plugins.t index d750390..fa69ec6 100644 --- a/t/admin/plugins.t +++ b/t/admin/plugins.t @@ -30,7 +30,7 @@ __DATA__ --- request GET /apisix/admin/plugins/list --- response_body_like eval -qr/\["request-id","fault-injection","serverless-pre-function","batch-requests","cors","ip-restriction","uri-blocker","request-validation","openid-connect","wolf-rbac","basic-auth","jwt-auth","key-auth","consumer-restriction","authz-keycloak","proxy-mirror","proxy-cache","proxy-rewrite","limit-conn","limit-count","limit-req","node-status","redirect","response-rewrite","grpc-transcode","prometheus","echo","http-logger","tcp-logger","kafka-logger","syslog","udp-logger","zipkin","skywalking" [...] +qr/\["request-id","fault-injection","serverless-pre-function","batch-requests","cors","ip-restriction","uri-blocker","request-validation","openid-connect","wolf-rbac","hmac-auth","basic-auth","jwt-auth","key-auth","consumer-restriction","authz-keycloak","proxy-mirror","proxy-cache","proxy-rewrite","limit-conn","limit-count","limit-req","node-status","redirect","response-rewrite","grpc-transcode","prometheus","echo","http-logger","tcp-logger","kafka-logger","syslog","udp-logger","zipkin", [...] --- no_error_log [error] diff --git a/t/debug/debug-mode.t b/t/debug/debug-mode.t index 0b466e6..442cce1 100644 --- a/t/debug/debug-mode.t +++ b/t/debug/debug-mode.t @@ -56,6 +56,7 @@ loaded plugin and sort by priority: 2900 name: uri-blocker loaded plugin and sort by priority: 2800 name: request-validation loaded plugin and sort by priority: 2599 name: openid-connect loaded plugin and sort by priority: 2555 name: wolf-rbac +loaded plugin and sort by priority: 2530 name: hmac-auth loaded plugin and sort by priority: 2520 name: basic-auth loaded plugin and sort by priority: 2510 name: jwt-auth loaded plugin and sort by priority: 2500 name: key-auth diff --git a/t/plugin/custom_hmac_auth.t b/t/plugin/custom_hmac_auth.t new file mode 100644 index 0000000..f52d13b --- /dev/null +++ b/t/plugin/custom_hmac_auth.t @@ -0,0 +1,364 @@ +# +# 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. +# +BEGIN { + $ENV{"CUSTOM_HMAC_AUTH"} = "true" +} + +use t::APISIX 'no_plan'; + +repeat_each(2); +no_long_string(); +no_root_location(); +no_shuffle(); +run_tests; + +__DATA__ + +=== TEST 1: add consumer with username and plugins +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "jack", + "plugins": { + "hmac-auth": { + "access_key": "my-access-key", + "secret_key": "my-secret-key" + } + } + }]], + [[{ + "node": { + "value": { + "username": "jack", + "plugins": { + "hmac-auth": { + "access_key": "my-access-key", + "secret_key": "my-secret-key", + "algorithm": "hmac-sha256", + "clock_skew": 300 + } + } + } + }, + "action": "set" + }]] + ) + + ngx.status = code + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 2: add consumer with plugin hmac-auth - missing secret key +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "jack", + "plugins": { + "hmac-auth": { + "access_key": "user-key" + } + } + }]]) + + ngx.status = code + ngx.say(body) + } + } +--- request +GET /t +--- error_code: 400 +--- no_error_log +[error] + + + +=== TEST 3: add consumer with plugin hmac-auth - missing access key +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "jack", + "plugins": { + "hmac-auth": { + "secret_key": "skey" + } + } + }]]) + + ngx.status = code + ngx.say(body) + } + } +--- request +GET /t +--- error_code: 400 +--- no_error_log +[error] + + + +=== TEST 4: enable hmac auth plugin using admin api +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "hmac-auth": {} + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 5: verify, missing signature +--- request +GET /hello +--- error_code: 401 +--- response_body +{"message":"access key or signature missing"} +--- no_error_log +[error] + + + +=== TEST 6: verify: invalid access key +--- request +GET /hello +--- more_headers +X-APISIX-HMAC-SIGNATURE: asdf +X-APISIX-HMAC-ALGORITHM: hmac-sha256 +X-APISIX-HMAC-TIMESTAMP: 112 +X-APISIX-HMAC-ACCESS-KEY: sdf +--- error_code: 401 +--- response_body +{"message":"Invalid access key"} +--- no_error_log +[error] + + + +=== TEST 7: verify: invalid algorithm +--- request +GET /hello +--- more_headers +X-APISIX-HMAC-SIGNATURE: asdf +X-APISIX-HMAC-ALGORITHM: ljlj +X-APISIX-HMAC-TIMESTAMP: 112 +X-APISIX-HMAC-ACCESS-KEY: sdf +--- error_code: 401 +--- response_body +{"message":"Invalid access key"} +--- no_error_log +[error] + + + +=== TEST 8: verify: invalid timestamp +--- request +GET /hello +--- more_headers +X-APISIX-HMAC-SIGNATURE: asdf +X-APISIX-HMAC-ALGORITHM: hmac-sha256 +X-APISIX-HMAC-TIMESTAMP: 112 +X-APISIX-HMAC-ACCESS-KEY: my-access-key +--- error_code: 401 +--- response_body +{"message":"Invalid timestamp"} +--- no_error_log +[error] + + + +=== TEST 9: verify: ok +--- config +location /t { + content_by_lua_block { + local ngx_time = ngx.time + local core = require("apisix.core") + local t = require("lib.test_admin") + local hmac = require("resty.hmac") + local ngx_encode_base64 = ngx.encode_base64 + + local secret_key = "my-secret-key" + local timestamp = ngx_time() + local access_key = "my-access-key" + local signing_string = "GET" .. "/hello" .. "" .. + "" .. access_key .. timestamp .. secret_key + + local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) + core.log.info("signature:", ngx_encode_base64(signature)) + local headers = {} + headers["X-APISIX-HMAC-SIGNATURE"] = ngx_encode_base64(signature) + headers["X-APISIX-HMAC-ALGORITHM"] = "hmac-sha256" + headers["X-APISIX-HMAC-TIMESTAMP"] = timestamp + headers["X-APISIX-HMAC-ACCESS-KEY"] = access_key + + local code, body = t.test('/hello', + ngx.HTTP_GET, + "", + nil, + headers + ) + + ngx.status = code + ngx.say(body) + } +} +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 10: update consumer with clock skew +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "pony", + "plugins": { + "hmac-auth": { + "access_key": "my-access-key2", + "secret_key": "my-secret-key2", + "clock_skew": 1 + } + } + }]], + [[{ + "node": { + "value": { + "username": "pony", + "plugins": { + "hmac-auth": { + "access_key": "my-access-key2", + "secret_key": "my-secret-key2", + "algorithm": "hmac-sha256", + "clock_skew": 1 + } + } + } + }, + "action": "set" + }]] + ) + + ngx.status = code + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 11: verify: invalid timestamp +--- config +location /t { + content_by_lua_block { + local ngx_time = ngx.time + local core = require("apisix.core") + local t = require("lib.test_admin") + local hmac = require("resty.hmac") + local ngx_encode_base64 = ngx.encode_base64 + + local secret_key = "my-secret-key2" + local timestamp = ngx_time() + local access_key = "my-access-key2" + local signing_string = "GET" .. "/hello" .. "" .. + "" .. access_key .. timestamp .. secret_key + + ngx.sleep(2) + + local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) + core.log.info("signature:", ngx_encode_base64(signature)) + local headers = {} + headers["X-APISIX-HMAC-SIGNATURE"] = ngx_encode_base64(signature) + headers["X-APISIX-HMAC-ALGORITHM"] = "hmac-sha256" + headers["X-APISIX-HMAC-TIMESTAMP"] = timestamp + headers["X-APISIX-HMAC-ACCESS-KEY"] = access_key + + local code, body = t.test('/hello', + ngx.HTTP_GET, + core.json.encode(data), + nil, + headers + ) + + ngx.status = code + ngx.say(body) + } +} +--- request +GET /t +--- error_code: 401 +--- response_body eval +qr/\{"message":"Invalid timestamp"\}/ +--- no_error_log +[error] diff --git a/t/plugin/hmac-auth.t b/t/plugin/hmac-auth.t new file mode 100644 index 0000000..772d22f --- /dev/null +++ b/t/plugin/hmac-auth.t @@ -0,0 +1,596 @@ +# +# 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. +# +use t::APISIX 'no_plan'; + +repeat_each(2); +no_long_string(); +no_root_location(); +no_shuffle(); +run_tests; + +__DATA__ + +=== TEST 1: add consumer with username and plugins +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "jack", + "plugins": { + "hmac-auth": { + "access_key": "my-access-key", + "secret_key": "my-secret-key" + } + } + }]], + [[{ + "node": { + "value": { + "username": "jack", + "plugins": { + "hmac-auth": { + "access_key": "my-access-key", + "secret_key": "my-secret-key", + "algorithm": "hmac-sha256", + "clock_skew": 300 + } + } + } + }, + "action": "set" + }]] + ) + + ngx.status = code + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 2: add consumer with plugin hmac-auth - missing secret key +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "foo", + "plugins": { + "hmac-auth": { + "access_key": "user-key" + } + } + }]]) + + ngx.status = code + ngx.say(body) + } + } +--- request +GET /t +--- error_code: 400 +--- response_body eval +qr/\{"error_msg":"invalid plugins configuration: failed to check the configuration of plugin hmac-auth err: value should match only one schema, but matches none"\}/ +--- no_error_log +[error] + + + +=== TEST 3: add consumer with plugin hmac-auth - missing access key +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "bar", + "plugins": { + "hmac-auth": { + "secret_key": "skey" + } + } + }]]) + + ngx.status = code + ngx.say(body) + } + } +--- request +GET /t +--- error_code: 400 +--- response_body eval +qr/\{"error_msg":"invalid plugins configuration: failed to check the configuration of plugin hmac-auth err: value should match only one schema, but matches none"\}/ +--- no_error_log +[error] + + + +=== TEST 4: add consumer with plugin hmac-auth - access key exceeds the length limit +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "li", + "plugins": { + "hmac-auth": { + "access_key": "akeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakey", + "secret_key": "skey" + } + } + }]]) + + ngx.status = code + ngx.say(body) + } + } +--- request +GET /t +--- error_code: 400 +--- response_body eval +qr/\{"error_msg":"invalid plugins configuration: failed to check the configuration of plugin hmac-auth err: value should match only one schema, but matches none"\}/ +--- no_error_log +[error] + + + +=== TEST 5: add consumer with plugin hmac-auth - access key exceeds the length limit +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "zhang", + "plugins": { + "hmac-auth": { + "access_key": "akey", + "secret_key": "skeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskey" + } + } + }]]) + + ngx.status = code + ngx.say(body) + } + } +--- request +GET /t +--- error_code: 400 +--- response_body eval +qr/\{"error_msg":"invalid plugins configuration: failed to check the configuration of plugin hmac-auth err: value should match only one schema, but matches none"\}/ +--- no_error_log +[error] + + + +=== TEST 6: enable hmac auth plugin using admin api +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "hmac-auth": {} + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 7: verify, missing signature +--- request +GET /hello +--- error_code: 401 +--- response_body +{"message":"access key or signature missing"} +--- no_error_log +[error] + + + +=== TEST 8: verify: invalid access key +--- request +GET /hello +--- more_headers +X-HMAC-SIGNATURE: asdf +X-HMAC-ALGORITHM: hmac-sha256 +X-HMAC-TIMESTAMP: 112 +X-HMAC-ACCESS-KEY: sdf +--- error_code: 401 +--- response_body +{"message":"Invalid access key"} +--- no_error_log +[error] + + + +=== TEST 9: verify: invalid algorithm +--- request +GET /hello +--- more_headers +X-HMAC-SIGNATURE: asdf +X-HMAC-ALGORITHM: ljlj +X-HMAC-TIMESTAMP: 112 +X-HMAC-ACCESS-KEY: my-access-key +--- error_code: 401 +--- response_body +{"message":"algorithm ljlj not supported"} +--- no_error_log +[error] + + + +=== TEST 10: verify: invalid timestamp +--- request +GET /hello +--- more_headers +X-HMAC-SIGNATURE: asdf +X-HMAC-ALGORITHM: hmac-sha256 +X-HMAC-TIMESTAMP: 112 +X-HMAC-ACCESS-KEY: my-access-key +--- error_code: 401 +--- response_body +{"message":"Invalid timestamp"} +--- no_error_log +[error] + + + +=== TEST 11: verify: ok +--- config +location /t { + content_by_lua_block { + local ngx_time = ngx.time + local core = require("apisix.core") + local t = require("lib.test_admin") + local hmac = require("resty.hmac") + local ngx_encode_base64 = ngx.encode_base64 + + local secret_key = "my-secret-key" + local timestamp = ngx_time() + local access_key = "my-access-key" + local signing_string = "GET" .. "/hello" .. "" .. + "" .. access_key .. timestamp .. secret_key + + local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) + core.log.info("signature:", ngx_encode_base64(signature)) + local headers = {} + headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature) + headers["X-HMAC-ALGORITHM"] = "hmac-sha256" + headers["X-HMAC-TIMESTAMP"] = timestamp + headers["X-HMAC-ACCESS-KEY"] = access_key + + local code, body = t.test('/hello', + ngx.HTTP_GET, + "", + nil, + headers + ) + + ngx.status = code + ngx.say(body) + } +} +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 12: add consumer with 0 clock skew +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "robin", + "plugins": { + "hmac-auth": { + "access_key": "my-access-key3", + "secret_key": "my-secret-key3", + "clock_skew": 0 + } + } + }]], + [[{ + "node": { + "value": { + "username": "robin", + "plugins": { + "hmac-auth": { + "access_key": "my-access-key3", + "secret_key": "my-secret-key3", + "algorithm": "hmac-sha256", + "clock_skew": 0 + } + } + } + }, + "action": "set" + }]] + ) + + ngx.status = code + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 13: verify: invalid signature +--- request +GET /hello +--- more_headers +X-HMAC-SIGNATURE: asdf +X-HMAC-ALGORITHM: hmac-sha256 +X-HMAC-TIMESTAMP: 112 +X-HMAC-ACCESS-KEY: my-access-key3 +--- error_code: 401 +--- response_body +{"message":"Invalid signature"} +--- no_error_log +[error] + + + +=== TEST 14: add consumer with 1 clock skew +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "pony", + "plugins": { + "hmac-auth": { + "access_key": "my-access-key2", + "secret_key": "my-secret-key2", + "clock_skew": 1 + } + } + }]], + [[{ + "node": { + "value": { + "username": "pony", + "plugins": { + "hmac-auth": { + "access_key": "my-access-key2", + "secret_key": "my-secret-key2", + "algorithm": "hmac-sha256", + "clock_skew": 1 + } + } + } + }, + "action": "set" + }]] + ) + + ngx.status = code + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 15: verify: invalid timestamp +--- config +location /t { + content_by_lua_block { + local ngx_time = ngx.time + local core = require("apisix.core") + local t = require("lib.test_admin") + local hmac = require("resty.hmac") + local ngx_encode_base64 = ngx.encode_base64 + + local secret_key = "my-secret-key2" + local timestamp = ngx_time() + local access_key = "my-access-key2" + local signing_string = "GET" .. "/hello" .. "" .. + "" .. access_key .. timestamp .. secret_key + + ngx.sleep(2) + + local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) + core.log.info("signature:", ngx_encode_base64(signature)) + local headers = {} + headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature) + headers["X-HMAC-ALGORITHM"] = "hmac-sha256" + headers["X-HMAC-TIMESTAMP"] = timestamp + headers["X-HMAC-ACCESS-KEY"] = access_key + + local code, body = t.test('/hello', + ngx.HTTP_GET, + core.json.encode(data), + nil, + headers + ) + + ngx.status = code + ngx.say(body) + } +} +--- request +GET /t +--- error_code: 401 +--- response_body eval +qr/\{"message":"Invalid timestamp"\}/ +--- no_error_log +[error] + + + +=== TEST 16: verify: put ok +--- config +location /t { + content_by_lua_block { + local ngx_time = ngx.time + local core = require("apisix.core") + local t = require("lib.test_admin") + local hmac = require("resty.hmac") + local ngx_encode_base64 = ngx.encode_base64 + + local data = {cert = "ssl_cert", key = "ssl_key", sni = "test.com"} + local req_body = core.json.encode(data) + req_body = req_body or "" + + local secret_key = "my-secret-key" + local timestamp = ngx_time() + local access_key = "my-access-key" + local signing_string = "PUT" .. "/hello" .. "" .. + req_body .. access_key .. timestamp .. secret_key + + local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) + core.log.info("signature:", ngx_encode_base64(signature)) + local headers = {} + headers["X-HMAC-SIGNATURE"] = ngx_encode_base64(signature) + headers["X-HMAC-ALGORITHM"] = "hmac-sha256" + headers["X-HMAC-TIMESTAMP"] = timestamp + headers["X-HMAC-ACCESS-KEY"] = access_key + + local code, body = t.test('/hello', + ngx.HTTP_PUT, + req_body, + nil, + headers + ) + + ngx.status = code + ngx.say(body) + } +} +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 17: verify: put ok (pass auth data by header `Authorization`) +--- config +location /t { + content_by_lua_block { + local ngx_time = ngx.time + local core = require("apisix.core") + local t = require("lib.test_admin") + local hmac = require("resty.hmac") + local ngx_encode_base64 = ngx.encode_base64 + + local data = {cert = ssl_cert, key = ssl_key, sni = "test.com"} + local req_body = core.json.encode(data) + req_body = req_body or "" + + local secret_key = "my-secret-key" + local timestamp = ngx_time() + local access_key = "my-access-key" + local signing_string = "PUT" .. "/hello" .. "" .. + req_body .. access_key .. timestamp .. secret_key + + local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string) + core.log.info("signature:", ngx_encode_base64(signature)) + local auth_string = "hmac-auth-v1#" .. access_key .. "#" .. ngx_encode_base64(signature) .. "#" .. + "hmac-sha256#" .. timestamp + local headers = {} + headers["Authorization"] = auth_string + + local code, body = t.test('/hello', + ngx.HTTP_PUT, + req_body, + nil, + headers + ) + + ngx.status = code + ngx.say(body) + } +} +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 18: hit route without auth info +--- request +GET /hello +--- error_code: 401 +--- response_body +{"message":"access key or signature missing"} +--- no_error_log +[error]