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/

Reply via email to