-
Notifications
You must be signed in to change notification settings - Fork 2.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(hmac-auth): Add validate request body for hmac auth plugin #5038
Changes from 6 commits
2e461f2
1bc110c
8271bbd
70b4813
eddf479
304f3d5
d14aafa
6cbef7e
4623d2f
a4edde5
6863828
9891f46
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -29,13 +29,16 @@ local hmac = require("resty.hmac") | |||||
local consumer = require("apisix.consumer") | ||||||
local plugin = require("apisix.plugin") | ||||||
local ngx_decode_base64 = ngx.decode_base64 | ||||||
local ngx_encode_base64 = ngx.encode_base64 | ||||||
|
||||||
local DIGEST = "Digest" | ||||||
local SIGNATURE_KEY = "X-HMAC-SIGNATURE" | ||||||
local ALGORITHM_KEY = "X-HMAC-ALGORITHM" | ||||||
local DATE_KEY = "Date" | ||||||
local ACCESS_KEY = "X-HMAC-ACCESS-KEY" | ||||||
local SIGNED_HEADERS_KEY = "X-HMAC-SIGNED-HEADERS" | ||||||
local plugin_name = "hmac-auth" | ||||||
local MAX_REQ_BODY = 1024 * 512 | ||||||
|
||||||
local lrucache = core.lrucache.new({ | ||||||
type = "plugin", | ||||||
|
@@ -79,7 +82,17 @@ local consumer_schema = { | |||||
type = "boolean", | ||||||
title = "Whether to escape the uri parameter", | ||||||
default = true, | ||||||
} | ||||||
}, | ||||||
validate_request_body = { | ||||||
type = "boolean", | ||||||
title = "A boolean value telling the plugin to enable body validation.", | ||||||
default = false, | ||||||
}, | ||||||
max_req_body = { | ||||||
type = "number", | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
title = "Max request body allowed", | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "Max request body size" would be better. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK |
||||||
default = MAX_REQ_BODY, | ||||||
}, | ||||||
}, | ||||||
required = {"access_key", "secret_key"}, | ||||||
} | ||||||
|
@@ -193,6 +206,17 @@ local function do_nothing(v) | |||||
return v | ||||||
end | ||||||
|
||||||
local function validate_body(ctx, secret_key, params, req_body) | ||||||
req_body = req_body or "" | ||||||
local digest_header = core.request.header(ctx, DIGEST) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The reason I use this header is that: |
||||||
if not digest_header then | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we could check this before reading the body There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe not, because we can omit digest header when body is empty. |
||||||
-- it's ok if there is no digest header and no body | ||||||
return req_body == "" | ||||||
end | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we judge this first? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe not, because we can omit digest header when body is empty. |
||||||
|
||||||
local request_body_hash = ngx_encode_base64(hmac_funcs[params.algorithm](secret_key, req_body)) | ||||||
return request_body_hash == digest_header | ||||||
end | ||||||
|
||||||
local function generate_signature(ctx, secret_key, params) | ||||||
local canonical_uri = ctx.var.uri | ||||||
|
@@ -327,6 +351,18 @@ local function validate(ctx, params) | |||||
return nil, {message = "Invalid signature"} | ||||||
end | ||||||
|
||||||
local validate_request_body = get_conf_field(params.access_key, "validate_request_body") | ||||||
if validate_request_body then | ||||||
local max_req_body = get_conf_field(params.access_key, "max_req_body") | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we could return here if no params.body_digest. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should check whether the params.body_digest exists or not. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. when validate_request_body == true and no params.body_digest, why not return false? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When there is no request body, the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK. Thanks for your explanation. How about when There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix it |
||||||
local req_body, err = core.request.get_body(max_req_body, ctx) | ||||||
if err then | ||||||
return nil, {message = "Exceed body limit size"} | ||||||
end | ||||||
if not validate_body(ctx, secret_key, params, req_body) then | ||||||
return nil, {message = "Invalid digest"} | ||||||
end | ||||||
end | ||||||
|
||||||
return consumer | ||||||
end | ||||||
|
||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -51,6 +51,8 @@ The `consumer` then adds its key to request header to verify its request. | |
| signed_headers | array[string] | optional | | | Restrict the headers that are added to the encrypted calculation. After the specified, the client request can only specify the headers within this range. When this item is empty, all the headers specified by the client request will be added to the encrypted calculation | | ||
| keep_headers | boolean | optional | false | [ true, false ] | Whether it is necessary to keep the request headers of `X-HMAC-SIGNATURE`, `X-HMAC-ALGORITHM` and `X-HMAC-SIGNED-HEADERS` in the http request after successful authentication. true: means to keep the http request header, false: means to remove the http request header. | | ||
| encode_uri_params | boolean | optional | true | [ true, false ] | Whether to encode the uri parameter in the signature, for example: `params1=hello%2Cworld` is encoded, `params2=hello,world` is not encoded. true: means to encode the uri parameter in the signature, false: not to encode the uri parameter in the signature. | | ||
| validate_request_body | boolean | optional | false | [ true, false ] | Whether to check request body. | | ||
| max_req_body | number | optional | 512KB | | Max allowed body size. | | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Better to use a number instead of string in the doc's default value cell |
||
|
||
## How To Enable | ||
|
||
|
@@ -192,6 +194,16 @@ print(base64.b64encode(hash.digest())) | |
| --------- | -------------------------------------------- | | ||
| SIGNATURE | 8XV1GB7Tq23OJcoz6wjqTs4ZLxr9DiLoY4PxzScWGYg= | | ||
|
||
### Request body checking | ||
|
||
When `validate_request_body` is assigned to `true`, the plugin will check the request body. The plugin will calculate the hmac-sha value of the request body,and check against the Digest header. | ||
|
||
``` | ||
Digest: base64(hmac-sha(<body>)) | ||
``` | ||
|
||
when there is no request body, the Digest header can be omitted. You can also set Digest to the hmac-sha of empty string. | ||
|
||
### Use the generated signature to try the request | ||
|
||
```shell | ||
|
@@ -278,6 +290,29 @@ Accept-Ranges: bytes | |
<html lang="cn"> | ||
``` | ||
|
||
### Enable request body checking | ||
|
||
```shell | ||
$ curl -X "POST" "http://localhost:9080/index.html?age=36&name=james" \ | ||
-H 'X-HMAC-ACCESS-KEY: zyedu-hmac-01' \ | ||
-H 'X-HMAC-SIGNATURE: ivlwjZPoVdSVvdSSM4drEFk9q9HS2jeJ5cAN9JffmdA=' \ | ||
-H 'X-HMAC-ALGORITHM: hmac-sha256' \ | ||
-H 'Date: Tue, 24 Aug 2021 03:19:21 GMT' \ | ||
-H 'X-HMAC-SIGNED-HEADERS: User-Agent;Digest' \ | ||
-H 'User-Agent: curl/7.29.0' \ | ||
-H 'Digest: L9b/+QMvhvnoUlSw5vq+kHPqnZiHGl61T8oavMVTaC4=' \ | ||
-H 'Content-Type: text/plain; charset=utf-8' \ | ||
-d "{\"hello\":\"world\"}" | ||
|
||
HTTP/1.1 200 OK | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Better to only show the first line? APISIX 2.2 doesn't support this feature. We can avoid confusing users by only show the first line. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Got it |
||
Content-Type: text/html; charset=utf-8 | ||
Transfer-Encoding: chunked | ||
Connection: keep-alive | ||
Date: Tue, 19 Jan 2021 11:33:20 GMT | ||
Server: APISIX/2.2 | ||
...... | ||
``` | ||
|
||
## Disable Plugin | ||
|
||
When you want to disable the `hmac-auth` plugin, it is very simple, | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -51,6 +51,8 @@ title: hmac-auth | |||||
| signed_headers | array[string] | 可选 | | | 限制加入加密计算的 headers ,指定后客户端请求只能在此范围内指定 headers ,此项为空时将把所有客户端请求指定的 headers 加入加密计算。如: ["User-Agent", "Accept-Language", "x-custom-a"] | | ||||||
| keep_headers | boolean | 可选 | false | [ true, false ] | 认证成功后的 http 请求中是否需要保留 `X-HMAC-SIGNATURE`、`X-HMAC-ALGORITHM` 和 `X-HMAC-SIGNED-HEADERS` 的请求头。true: 表示保留 http 请求头,false: 表示移除 http 请求头。 | | ||||||
| encode_uri_param | boolean | 可选 | true | [ true, false ] | 是否对签名中的 uri 参数进行编码,例如: `params1=hello%2Cworld` 进行了编码,`params2=hello,world` 没有进行编码。true: 表示对签名中的 uri 参数进行编码,false: 不对签名中的 uri 参数编码。 | | ||||||
| validate_request_body | boolean | 可选 | false | [ true, false ] | 是否对请求 body 做签名校验。| | ||||||
| max_req_body | number | 可选 | 512KB | | 最大允许的 body 大小。| | ||||||
|
||||||
## 如何启用 | ||||||
|
||||||
|
@@ -186,6 +188,16 @@ print(base64.b64encode(hash.digest())) | |||||
| --------- | -------------------------------------------- | | ||||||
| SIGNATURE | 8XV1GB7Tq23OJcoz6wjqTs4ZLxr9DiLoY4PxzScWGYg= | | ||||||
|
||||||
### Body 校验 | ||||||
|
||||||
把 `validate_request_body` 设置为 true 来进行请求 body 的校验。插件将计算 hmac-sha 值,对比头部中的 Digest 头部值。 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
``` | ||||||
Digest: base64(hmac-sha(<body>)) | ||||||
``` | ||||||
|
||||||
当无请求 body 时,可不传 Digest 头部,网关会校验是否确实无请求 body。如果要传 Digest 头部,可计算长度为 0 的空字符串的 hmac-sha 值。 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
### 使用生成好的签名进行请求尝试 | ||||||
|
||||||
```shell | ||||||
|
@@ -272,6 +284,29 @@ Accept-Ranges: bytes | |||||
<html lang="cn"> | ||||||
``` | ||||||
|
||||||
### 开启 body 校验 | ||||||
|
||||||
```shell | ||||||
$ curl -X "POST" "http://localhost:9080/index.html?age=36&name=james" \ | ||||||
-H 'X-HMAC-ACCESS-KEY: zyedu-hmac-01' \ | ||||||
-H 'X-HMAC-SIGNATURE: ivlwjZPoVdSVvdSSM4drEFk9q9HS2jeJ5cAN9JffmdA=' \ | ||||||
-H 'X-HMAC-ALGORITHM: hmac-sha256' \ | ||||||
-H 'Date: Tue, 24 Aug 2021 03:19:21 GMT' \ | ||||||
-H 'X-HMAC-SIGNED-HEADERS: User-Agent;Digest' \ | ||||||
-H 'User-Agent: curl/7.29.0' \ | ||||||
-H 'Digest: L9b/+QMvhvnoUlSw5vq+kHPqnZiHGl61T8oavMVTaC4=' \ | ||||||
-H 'Content-Type: text/plain; charset=utf-8' \ | ||||||
-d "{\"hello\":\"world\"}" | ||||||
|
||||||
HTTP/1.1 200 OK | ||||||
Content-Type: text/html; charset=utf-8 | ||||||
Transfer-Encoding: chunked | ||||||
Connection: keep-alive | ||||||
Date: Tue, 19 Jan 2021 11:33:20 GMT | ||||||
Server: APISIX/2.2 | ||||||
...... | ||||||
``` | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you help to update the English doc? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will |
||||||
|
||||||
## 禁用插件 | ||||||
|
||||||
当你想去掉 `hmac-auth` 插件的时候,很简单,在插件的配置中把对应的 `json` 配置删除即可,无须重启服务,即刻生效: | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we make this header configurable and default to "X-HMAC-DIGEST"?
There is already a Digest header in the HTTP standard and it is not relative to the hmac.
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Digest
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK