This is an automated email from the ASF dual-hosted git repository.
shreemaan-abhishek 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 5d5613aea feat(hmac-auth): default signed_headers to ["date"] (#13388)
5d5613aea is described below
commit 5d5613aea2bd6d8a1b28228b3bbe7e5de4af8561
Author: Shreemaan Abhishek <[email protected]>
AuthorDate: Thu May 28 12:51:39 2026 +0800
feat(hmac-auth): default signed_headers to ["date"] (#13388)
---
apisix/plugins/hmac-auth.lua | 14 +-
docs/en/latest/plugins/hmac-auth.md | 4 +-
docs/zh/latest/plugins/hmac-auth.md | 4 +-
t/plugin/hmac-auth.t | 278 +++++++++++++++++++++++++++++++++++-
4 files changed, 286 insertions(+), 14 deletions(-)
diff --git a/apisix/plugins/hmac-auth.lua b/apisix/plugins/hmac-auth.lua
index 6c53a6f40..9edd12807 100644
--- a/apisix/plugins/hmac-auth.lua
+++ b/apisix/plugins/hmac-auth.lua
@@ -51,6 +51,7 @@ local schema = {
},
signed_headers = {
type = "array",
+ default = {"date"},
items = {
type = "string",
minLength = 1,
@@ -232,15 +233,10 @@ local function validate(ctx, conf, params)
-- 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
+ local params_headers_map = params.headers and
array_to_map(params.headers) or {}
+ 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
diff --git a/docs/en/latest/plugins/hmac-auth.md
b/docs/en/latest/plugins/hmac-auth.md
index 6319ce23f..4713b5da6 100644
--- a/docs/en/latest/plugins/hmac-auth.md
+++ b/docs/en/latest/plugins/hmac-auth.md
@@ -63,8 +63,8 @@ The following attributes are available for configurations on
Routes or Services.
| Name | Type | Required | Default
| Valid values
| Description
[...]
|-----------------------|---------------|----------|----------------------------------------------|---------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
[...]
| allowed_algorithms | array[string] | False | `["hmac-sha1",
"hmac-sha256", "hmac-sha512"]` | Combination of `"hmac-sha1"`, `"hmac-sha256"`,
and `"hmac-sha512"` | The list of HMAC algorithms allowed.
[...]
-| clock_skew | integer | False | 300
| >=1
| Maximum allowable time difference in seconds between the
client request's timestamp and APISIX server's current time. This helps account
for discrepancies in time synchronization between the client's and server's
clocks and protect against replay attacks. The timestamp in the `Date` header
(must be in GMT format) w [...]
-| signed_headers | array[string] | False |
|
| The list of HMAC-signed headers that should be included in the
client request's HMAC signature.
[...]
+| clock_skew | integer | False | 300
| >=1
| Maximum allowable difference in seconds between the value of
the request's `Date` header (which must be in GMT format) and APISIX's current
time. This helps reject stale requests when the client and server clocks are
reasonably in sync. For the freshness window to be meaningful, `date` must be
part of `signed_heade [...]
+| signed_headers | array[string] | False | `["date"]`
|
| The list of headers that must be included in the client
request's HMAC signing string. The default value of `["date"]` ensures the
`Date` header is always bound into the signing string, so that the value used
for the `clock_skew` freshness check is itself covered by the HMAC. Set this
explicitly to override the hea [...]
| validate_request_body | boolean | False | false
|
| If true, validate the integrity of the request body to ensure
it has not been tampered with during transmission. Specifically, the Plugin
creates a SHA-256 base64-encoded digest and compares it to the `Digest` header.
If the `Digest` header is missing or if the digests do not match, the
validation fails. [...]
| hide_credentials | boolean | False | false
|
| If true, do not pass the authorization request header to
Upstream services.
[...]
| anonymous_consumer | string | False |
|
| Anonymous Consumer name. If configured, allow anonymous users
to bypass the authentication.
[...]
diff --git a/docs/zh/latest/plugins/hmac-auth.md
b/docs/zh/latest/plugins/hmac-auth.md
index 93d4ee7a7..f10580403 100644
--- a/docs/zh/latest/plugins/hmac-auth.md
+++ b/docs/zh/latest/plugins/hmac-auth.md
@@ -64,8 +64,8 @@ import TabItem from '@theme/TabItem';
| 名称 | 类型 | 必选项 | 默认值
| 有效值 |
描述
[...]
|-----------------------|---------------|--------|------------------------------------------------|---------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
[...]
| allowed_algorithms | array[string] | 否 | `["hmac-sha1",
"hmac-sha256", "hmac-sha512"]` | `"hmac-sha1"`、`"hmac-sha256"` 和
`"hmac-sha512"` 的组合 | 允许的 HMAC 算法列表。
[...]
-| clock_skew | integer | 否 | 300
| >=1
| 客户端请求的时间戳与 APISIX
服务器当前时间之间允许的最大时间差(以秒为单位)。这有助于解决客户端和服务器之间的时间同步差异,并防止重放攻击。时间戳将根据 `Date` 头中的时间(必须为
GMT 格式)进行计算。
|
-| signed_headers | array[string] | 否 |
|
| 客户端请求的 HMAC 签名中应包含的标头列表。
|
+| clock_skew | integer | 否 | 300
| >=1
| 请求的 `Date` 头(必须为 GMT 格式)与 APISIX
当前时间之间允许的最大时间差(秒)。该机制用于在客户端与服务端时钟基本同步的前提下拒绝过期请求。要使该时效窗口具有实际意义,`date` 必须出现在
`signed_headers` 中,从而将 `Date` 的取值绑定进签名字符串。`signed_headers` 默认值为
`["date"]`,开箱即默认完成该绑定。
|
+| signed_headers | array[string] | 否 | `["date"]`
|
| HMAC 签名字符串中必须包含的请求头列表。默认值为 `["date"]`,确保 `Date` 头始终被绑定进签名字符串,使得
`clock_skew` 时效校验所依据的取值同样受 HMAC 覆盖。如需覆盖默认值,请显式设置该字段;设置为空数组 `[]` 则完全取消该要求。
|
| validate_request_body | boolean | 否 | false
|
| 如果为 true,则验证请求正文的完整性,以确保在传输过程中没有被篡改。具体来说,插件会创建一个 SHA-256 的 base64 编码
digest,并将其与 `Digest` 头进行比较。如果 `Digest` 头丢失或 digest 不匹配,验证将失败。
|
| hide_credentials | boolean | 否 | false
|
| 如果为 true,则不会将授权请求头传递给上游服务。
|
| anonymous_consumer | string | 否 |
|
| 匿名 Consumer 名称。如果已配置,则允许匿名用户绕过身份验证。
|
diff --git a/t/plugin/hmac-auth.t b/t/plugin/hmac-auth.t
index a44c1070a..43812c106 100644
--- a/t/plugin/hmac-auth.t
+++ b/t/plugin/hmac-auth.t
@@ -915,6 +915,7 @@ location /t {
local signing_string = {
key_id,
"GET /headers",
+ "date: " .. gmt,
}
signing_string = core.table.concat(signing_string, "\n") .. "\n"
@@ -922,7 +923,7 @@ location /t {
core.log.info("signature:", ngx_encode_base64(signature))
local headers = {}
headers["date"] = gmt
- headers["Authorization"] = "Signature keyId=\"" .. key_id ..
"\",algorithm=\"hmac-sha256\"" .. ",headers=\"@request-target\",signature=\""
.. ngx_encode_base64(signature) .. "\""
+ headers["Authorization"] = "Signature keyId=\"" .. key_id ..
"\",algorithm=\"hmac-sha256\"" .. ",headers=\"@request-target
date\",signature=\"" .. ngx_encode_base64(signature) .. "\""
local code, _, body = t.test('/headers',
ngx.HTTP_GET,
"",
@@ -1190,3 +1191,278 @@ qr/client request can't be validated: [^,]+/
client request can't be validated: Invalid algorithm
--- no_error_log
my-secret-key
+
+
+
+=== TEST 35: update route to default hmac-auth config (no signed_headers
override)
+--- 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
+
+
+
+=== TEST 36: default signed_headers requires date in signing (headers clause
omitted)
+--- config
+location /t {
+ content_by_lua_block {
+ local ngx_time = ngx.time
+ local ngx_http_time = ngx.http_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 gmt = ngx_http_time(timestamp)
+ local key_id = "my-access-key"
+
+ local signing_string = {
+ key_id,
+ }
+ signing_string = core.table.concat(signing_string, "\n") .. "\n"
+
+ local signature = hmac:new(secret_key,
hmac.ALGOS.SHA256):final(signing_string)
+ local headers = {}
+ headers["Date"] = gmt
+ headers["Authorization"] = "Signature keyId=\"" .. key_id ..
"\",algorithm=\"hmac-sha256\",signature=\"" .. ngx_encode_base64(signature) ..
"\""
+
+ local code, body = t.test('/hello',
+ ngx.HTTP_GET,
+ "",
+ nil,
+ headers
+ )
+
+ ngx.status = code
+ ngx.print(body)
+ }
+}
+--- request
+GET /t
+--- error_code: 401
+--- response_body
+{"message":"client request can't be validated"}
+--- grep_error_log eval
+qr/client request can't be validated: [^,]+/
+--- grep_error_log_out
+client request can't be validated: expected header "date" missing in signing
+--- no_error_log
+my-secret-key
+
+
+
+=== TEST 37: default signed_headers accepts a request that binds date into the
signing string
+--- config
+location /t {
+ content_by_lua_block {
+ local ngx_time = ngx.time
+ local ngx_http_time = ngx.http_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 gmt = ngx_http_time(timestamp)
+ local key_id = "my-access-key"
+
+ local signing_string = {
+ key_id,
+ "date: " .. gmt,
+ }
+ signing_string = core.table.concat(signing_string, "\n") .. "\n"
+
+ local signature = hmac:new(secret_key,
hmac.ALGOS.SHA256):final(signing_string)
+ local headers = {}
+ headers["Date"] = gmt
+ headers["Authorization"] = "Signature keyId=\"" .. key_id ..
"\",algorithm=\"hmac-sha256\",headers=\"date\",signature=\"" ..
ngx_encode_base64(signature) .. "\""
+
+ 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
+my-secret-key
+
+
+
+=== TEST 38: changing only the Date on an existing Authorization triggers
replay rejection
+--- config
+location /t {
+ content_by_lua_block {
+ local ngx_time = ngx.time
+ local ngx_http_time = ngx.http_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 t1 = ngx_time()
+ local gmt1 = ngx_http_time(t1)
+ local gmt2 = ngx_http_time(t1 + 60)
+ local key_id = "my-access-key"
+
+ local signing_string = {
+ key_id,
+ "date: " .. gmt1,
+ }
+ signing_string = core.table.concat(signing_string, "\n") .. "\n"
+
+ local signature = hmac:new(secret_key,
hmac.ALGOS.SHA256):final(signing_string)
+ local auth = "Signature keyId=\"" .. key_id ..
"\",algorithm=\"hmac-sha256\",headers=\"date\",signature=\"" ..
ngx_encode_base64(signature) .. "\""
+
+ local headers1 = {}
+ headers1["Date"] = gmt1
+ headers1["Authorization"] = auth
+
+ local code1, _ = t.test('/hello',
+ ngx.HTTP_GET,
+ "",
+ nil,
+ headers1
+ )
+
+ local headers2 = {}
+ headers2["Date"] = gmt2
+ headers2["Authorization"] = auth
+
+ local code2, _ = t.test('/hello',
+ ngx.HTTP_GET,
+ "",
+ nil,
+ headers2
+ )
+
+ ngx.say(code1 .. " " .. code2)
+ }
+}
+--- request
+GET /t
+--- response_body
+200 401
+--- grep_error_log eval
+qr/client request can't be validated: [^,]+/
+--- grep_error_log_out
+client request can't be validated: Invalid signature
+--- no_error_log
+my-secret-key
+
+
+
+=== TEST 39: update route to opt out of default signed_headers
+--- 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": {
+ "signed_headers": []
+ }
+ },
+ "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
+
+
+
+=== TEST 40: with signed_headers: [], a request signing only @request-target
is accepted
+--- config
+location /t {
+ content_by_lua_block {
+ local ngx_time = ngx.time
+ local ngx_http_time = ngx.http_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 key_id = "my-access-key"
+ local secret_key = "my-secret-key"
+
+ local signing_string = {
+ key_id,
+ "GET /hello",
+ }
+ signing_string = core.table.concat(signing_string, "\n") .. "\n"
+
+ local signature = hmac:new(secret_key,
hmac.ALGOS.SHA256):final(signing_string)
+ local headers = {}
+ headers["Date"] = ngx_http_time(ngx_time())
+ headers["Authorization"] = "Signature keyId=\"" .. key_id ..
"\",algorithm=\"hmac-sha256\",headers=\"@request-target\",signature=\"" ..
ngx_encode_base64(signature) .. "\""
+
+ local code, body = t.test('/hello',
+ ngx.HTTP_GET,
+ "",
+ nil,
+ headers
+ )
+
+ ngx.status = code
+ ngx.say(body)
+ }
+}
+--- request
+GET /t
+--- response_body
+passed