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 b6f80f5d4 change(auth): require configured jwt claims, harden empty
claims_to_verify and key-auth anonymous fallback (#13468)
b6f80f5d4 is described below
commit b6f80f5d4cfdf028173d604f3302206c60dd92a9
Author: Shreemaan Abhishek <[email protected]>
AuthorDate: Tue Jun 9 16:18:51 2026 +0800
change(auth): require configured jwt claims, harden empty claims_to_verify
and key-auth anonymous fallback (#13468)
---
apisix/plugins/jwt-auth/parser.lua | 44 +++++++++---
apisix/plugins/key-auth.lua | 15 ++++
t/lib/server.lua | 16 +++++
t/plugin/jwt-auth-more-algo.t | 38 +++++++++-
t/plugin/jwt-auth.t | 127 +++++++++++++++++++++++++++++++++
t/plugin/key-auth-anonymous-consumer.t | 74 +++++++++++++++++++
6 files changed, 300 insertions(+), 14 deletions(-)
diff --git a/apisix/plugins/jwt-auth/parser.lua
b/apisix/plugins/jwt-auth/parser.lua
index 303340f56..098a26825 100644
--- a/apisix/plugins/jwt-auth/parser.lua
+++ b/apisix/plugins/jwt-auth/parser.lua
@@ -229,21 +229,43 @@ end
function _M.verify_claims(self, claims, conf)
- if not claims then
- claims = default_claims
+ -- When `claims_to_verify` is not configured (nil or an explicitly empty
+ -- array), fall back to the default claims (exp/nbf) and validate them only
+ -- if they are present in the payload. This closes the expired-token hole
+ -- while staying lenient for tokens that legitimately omit these claims.
+ -- An empty array must NOT skip validation, otherwise it reopens the
bypass.
+ if not claims or #claims == 0 then
+ for _, claim_name in ipairs(default_claims) do
+ local claim = self.payload[claim_name]
+ if claim ~= nil then
+ local checker = claims_checker[claim_name]
+ if type(claim) ~= checker.type then
+ return false, "claim " .. claim_name .. " is not a " ..
checker.type
+ end
+ local ok, err = checker.check(claim, conf)
+ if not ok then
+ return false, err
+ end
+ end
+ end
+
+ return true
end
+ -- When `claims_to_verify` is explicitly configured, the listed claims are
+ -- required: they must exist in the payload and be valid.
for _, claim_name in ipairs(claims) do
local claim = self.payload[claim_name]
- if claim then
- local checker = claims_checker[claim_name]
- if type(claim) ~= checker.type then
- return false, "claim " .. claim_name .. " is not a " ..
checker.type
- end
- local ok, err = checker.check(claim, conf)
- if not ok then
- return false, err
- end
+ if claim == nil then
+ return false, "claim " .. claim_name .. " is missing"
+ end
+ local checker = claims_checker[claim_name]
+ if type(claim) ~= checker.type then
+ return false, "claim " .. claim_name .. " is not a " ..
checker.type
+ end
+ local ok, err = checker.check(claim, conf)
+ if not ok then
+ return false, err
end
end
diff --git a/apisix/plugins/key-auth.lua b/apisix/plugins/key-auth.lua
index 020326dce..14944e98b 100644
--- a/apisix/plugins/key-auth.lua
+++ b/apisix/plugins/key-auth.lua
@@ -108,6 +108,21 @@ function _M.rewrite(conf, ctx)
core.response.set_header("WWW-Authenticate", "apikey realm=\"" ..
conf.realm .. "\"")
return 401, { message = err}
end
+ -- Strip credentials before falling back to the anonymous consumer when
+ -- hide_credentials is enabled. find_consumer() only strips on the
+ -- successful-auth path, so without this an invalid credential would be
+ -- forwarded upstream during anonymous fallback. A request may carry
the
+ -- credential in both the header and the query string, so clean up
both.
+ if conf.hide_credentials then
+ if core.request.header(ctx, conf.header) then
+ core.request.set_header(ctx, conf.header, nil)
+ end
+ local args = core.request.get_uri_args(ctx) or {}
+ if args[conf.query] then
+ args[conf.query] = nil
+ core.request.set_uri_args(ctx, args)
+ end
+ end
consumer, consumer_conf, err =
consumer_mod.get_anonymous_consumer(conf.anonymous_consumer)
if not consumer then
err = "key-auth failed to authenticate the request, code: 401.
error: " .. err
diff --git a/t/lib/server.lua b/t/lib/server.lua
index 0dd07fe90..88b8e603e 100644
--- a/t/lib/server.lua
+++ b/t/lib/server.lua
@@ -413,6 +413,22 @@ function _M.print_uri_detailed()
ngx.say("ngx.var.request_uri: ", ngx.var.request_uri)
end
+-- echo back exactly what the upstream received: the full request URI (with
+-- query string) and every request header. Lets tests assert on what was
+-- actually proxied upstream instead of scanning the error log.
+function _M.print_request_received()
+ ngx.say("request_uri: ", ngx.var.request_uri)
+ local headers = ngx.req.get_headers()
+ local keys = {}
+ for k in pairs(headers) do
+ keys[#keys + 1] = k
+ end
+ table.sort(keys)
+ for _, k in ipairs(keys) do
+ ngx.say(k, ": ", headers[k])
+ end
+end
+
function _M.headers()
local args = ngx.req.get_uri_args()
for name, val in pairs(args) do
diff --git a/t/plugin/jwt-auth-more-algo.t b/t/plugin/jwt-auth-more-algo.t
index b7e5f5311..1916e4579 100644
--- a/t/plugin/jwt-auth-more-algo.t
+++ b/t/plugin/jwt-auth-more-algo.t
@@ -325,13 +325,18 @@ passed
-=== TEST 13: verify success with expired token
+=== TEST 13: configured claim (nbf) missing from token -> rejected
+# claims_to_verify lists nbf, but this token only carries exp. A claim that is
+# explicitly configured is required, so the missing nbf is rejected.
--- request
GET /hello
--- more_headers
Authorization:
eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTU2Mzg3MDUwMX0.SJNbFYR1qlIOD7D8aItkB9hYxdhc0d_JGaLgVjOCDOAHd8CSJuHp_R6YQniRDq8S
---- response_body
-hello world
+--- error_code: 401
+--- response_body eval
+qr/failed to verify jwt/
+--- error_log
+claim nbf is missing
@@ -363,6 +368,33 @@ hello world
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
+
+ -- TEST 12 left claims_to_verify=["nbf"] on this route. Now that
+ -- configured claims are required, that stale requirement would
reject
+ -- the EdDSA token in TEST 17 (which carries exp but no nbf). This
test
+ -- group targets signature verification, so restore a clean
jwt-auth
+ -- config on the route first.
+ local code = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ [[{
+ "plugins": {
+ "jwt-auth": {}
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello"
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ ngx.say("failed to reset route")
+ return
+ end
+
local code, body = t('/apisix/admin/consumers',
ngx.HTTP_PUT,
[[{
diff --git a/t/plugin/jwt-auth.t b/t/plugin/jwt-auth.t
index c6efd95d5..f9cb66ec4 100644
--- a/t/plugin/jwt-auth.t
+++ b/t/plugin/jwt-auth.t
@@ -1301,3 +1301,130 @@ passed
{"message":"failed to verify jwt"}
--- error_log
failed to verify jwt: algorithm mismatch, expected RS256
+
+
+
+=== TEST 53: add consumer for default-claims verification
+--- 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": {
+ "jwt-auth": {
+ "key": "user-key",
+ "secret": "my-secret-key"
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 54: enable jwt-auth WITHOUT claims_to_verify (default exp/nbf path)
+--- 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": {
+ "jwt-auth": {}
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 55: expired token with no claims_to_verify configured -> rejected
+--- request
+GET
/hello?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTU2Mzg3MDUwMX0.pPNVvh-TQsdDzorRwa-uuiLYiEBODscp9wv0cwD6c68
+--- error_code: 401
+--- response_body
+{"message":"failed to verify jwt"}
+--- error_log
+failed to verify jwt: 'exp' claim expired at Tue, 23 Jul 2019 08:28:21 GMT
+
+
+
+=== TEST 56: token without exp claim and no claims_to_verify configured ->
accepted
+--- request
+GET
/hello?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSJ9._7aoTZdzQDT0r9swHTcHb3nsujexcGjSTU-LRzTRVyY
+--- response_body
+hello world
+--- no_error_log
+[error]
+
+
+
+=== TEST 57: enable jwt-auth with an explicit empty claims_to_verify array
+--- 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": {
+ "jwt-auth": {
+ "claims_to_verify": []
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 58: expired token with an explicit empty claims_to_verify -> still
rejected
+--- request
+GET
/hello?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTU2Mzg3MDUwMX0.pPNVvh-TQsdDzorRwa-uuiLYiEBODscp9wv0cwD6c68
+--- error_code: 401
+--- response_body
+{"message":"failed to verify jwt"}
+--- error_log
+failed to verify jwt: 'exp' claim expired at Tue, 23 Jul 2019 08:28:21 GMT
diff --git a/t/plugin/key-auth-anonymous-consumer.t
b/t/plugin/key-auth-anonymous-consumer.t
index 20448a0a8..1d521483a 100644
--- a/t/plugin/key-auth-anonymous-consumer.t
+++ b/t/plugin/key-auth-anonymous-consumer.t
@@ -221,3 +221,77 @@ GET /hello
failed to get anonymous consumer not-found-anonymous
--- response_body
{"message":"Invalid user authorization"}
+
+
+
+=== TEST 8: enable key-auth with anonymous consumer and hide_credentials
+# Route to an upstream that echoes back the request URI (with query string) and
+# every request header it received, so the tests can assert on what is actually
+# proxied upstream. Scanning the error log is unreliable here: nginx always
logs
+# the original client request line, which still contains the credential even
+# after it is stripped from the upstream request.
+--- 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": {
+ "key-auth": {
+ "query": "auth",
+ "anonymous_consumer": "anonymous",
+ "hide_credentials": true
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/print_request_received"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 9: invalid key in header falls back to anonymous, credential not
forwarded upstream
+# The upstream body must contain the anonymous marker (X-Consumer-Username:
+# anonymous, injected by apisix) AND must not contain the credential anywhere,
+# proving the invalid key was stripped before the request was proxied upstream.
+--- request
+GET /print_request_received
+--- more_headers
+apikey: invalid-key
+--- response_body_like eval
+qr/(?s)^(?!.*invalid-key).*x-consumer-username: anonymous/
+
+
+
+=== TEST 10: invalid key in query falls back to anonymous, credential not
forwarded upstream
+--- request
+GET /print_request_received?auth=invalid-key
+--- response_body_like eval
+qr/(?s)^(?!.*invalid-key).*x-consumer-username: anonymous/
+
+
+
+=== TEST 11: invalid key in BOTH header and query -> neither forwarded upstream
+--- request
+GET /print_request_received?auth=invalid-key
+--- more_headers
+apikey: invalid-key
+--- response_body_like eval
+qr/(?s)^(?!.*invalid-key).*x-consumer-username: anonymous/