This is an automated email from the ASF dual-hosted git repository.

nic443 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 282995f26 feat(jwt): support more algorithms (#12944)
282995f26 is described below

commit 282995f26636090df5e441ddf89db93bd2ffd1cd
Author: Shreemaan Abhishek <[email protected]>
AuthorDate: Fri Feb 6 08:13:48 2026 +0545

    feat(jwt): support more algorithms (#12944)
    
    Signed-off-by: Abhishek Choudhary <[email protected]>
---
 Makefile                           |   3 +
 apisix/plugins/jwt-auth.lua        | 112 +++++++---
 apisix/plugins/jwt-auth/parser.lua | 290 ++++++++++++++++++++++++++
 docs/en/latest/plugins/jwt-auth.md |   3 +-
 docs/zh/latest/plugins/jwt-auth.md |   3 +-
 t/plugin/jwt-auth-more-algo.t      | 410 +++++++++++++++++++++++++++++++++++++
 t/plugin/jwt-auth.t                |  41 +---
 t/plugin/jwt-auth4.t               |  34 +++
 8 files changed, 833 insertions(+), 63 deletions(-)

diff --git a/Makefile b/Makefile
index b54d7eefc..6617dea22 100644
--- a/Makefile
+++ b/Makefile
@@ -391,6 +391,9 @@ install: runtime
        $(ENV_INSTALL) apisix/plugins/mcp/broker/*.lua 
$(ENV_INST_LUADIR)/apisix/plugins/mcp/broker
        $(ENV_INSTALL) apisix/plugins/mcp/transport/*.lua 
$(ENV_INST_LUADIR)/apisix/plugins/mcp/transport
 
+       $(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/jwt-auth
+       $(ENV_INSTALL) apisix/plugins/jwt-auth/*.lua 
$(ENV_INST_LUADIR)/apisix/plugins/jwt-auth
+
        $(ENV_INSTALL) bin/apisix $(ENV_INST_BINDIR)/apisix
 
 
diff --git a/apisix/plugins/jwt-auth.lua b/apisix/plugins/jwt-auth.lua
index 4c32609bf..55866b1d8 100644
--- a/apisix/plugins/jwt-auth.lua
+++ b/apisix/plugins/jwt-auth.lua
@@ -15,7 +15,6 @@
 -- limitations under the License.
 --
 local core     = require("apisix.core")
-local jwt      = require("resty.jwt")
 local consumer_mod = require("apisix.consumer")
 local new_tab = require ("table.new")
 local auth_utils = require("apisix.utils.auth")
@@ -27,8 +26,9 @@ local table_insert = table.insert
 local table_concat = table.concat
 local ngx_re_gmatch = ngx.re.gmatch
 local plugin_name = "jwt-auth"
-local schema_def = require("apisix.schema_def")
 
+local schema_def = require("apisix.schema_def")
+local jwt_parser = require("apisix.plugins.jwt-auth.parser")
 
 local schema = {
     type = "object",
@@ -60,6 +60,14 @@ local schema = {
         },
         realm = schema_def.get_realm_schema("jwt"),
         anonymous_consumer = schema_def.anonymous_consumer_schema,
+        claims_to_verify = {
+            type = "array",
+            items = {
+                type = "string",
+                enum = {"exp","nbf"},
+            },
+            uniqueItems = true,
+        },
     },
 }
 
@@ -77,7 +85,21 @@ local consumer_schema = {
         },
         algorithm = {
             type = "string",
-            enum = {"HS256", "HS512", "RS256", "ES256"},
+            enum = {
+                "HS256",
+                "HS384",
+                "HS512",
+                "RS256",
+                "RS384",
+                "RS512",
+                "ES256",
+                "ES384",
+                "ES512",
+                "PS256",
+                "PS384",
+                "PS512",
+                "EdDSA",
+            },
             default = "HS256"
         },
         exp = {type = "integer", minimum = 1, default = 86400},
@@ -97,16 +119,30 @@ local consumer_schema = {
                 {
                     properties = {
                         algorithm = {
-                            enum = {"HS256", "HS512"},
+                            enum = {"HS256", "HS384", "HS512"},
                             default = "HS256"
                         },
                     },
                 },
                 {
                     properties = {
-                        public_key = {type = "string"},
+                        public_key = {
+                            type = "string",
+                            minLength = 1,
+                        },
                         algorithm = {
-                            enum = {"RS256", "ES256"},
+                            enum = {
+                                "RS256",
+                                "RS384",
+                                "RS512",
+                                "ES256",
+                                "ES384",
+                                "ES512",
+                                "PS256",
+                                "PS384",
+                                "PS512",
+                                "EdDSA",
+                            },
                         },
                     },
                     required = {"public_key"},
@@ -141,15 +177,21 @@ function _M.check_schema(conf, schema_type)
         return false, err
     end
 
-    if (conf.algorithm == "HS256" or conf.algorithm == "HS512") and not 
conf.secret then
-        return false, "property \"secret\" is required "..
-                      "when \"algorithm\" is \"HS256\" or \"HS512\""
-    elseif conf.base64_secret then
+    local is_hs_alg = conf.algorithm:sub(1, 2) == "HS"
+    if is_hs_alg and not conf.secret then
+        return false, "property \"secret\" is required when using HS based 
algorithms"
+    end
+
+    if conf.base64_secret then
         if ngx_decode_base64(conf.secret) == nil then
             return false, "base64_secret required but the secret is not in 
base64 format"
         end
     end
 
+    if not is_hs_alg and not conf.public_key then
+        return false, "missing valid public key"
+    end
+
     return true
 end
 
@@ -232,15 +274,16 @@ local function get_secret(conf)
     return secret
 end
 
-local function get_auth_secret(auth_conf)
-    if not auth_conf.algorithm or auth_conf.algorithm == "HS256"
-            or auth_conf.algorithm == "HS512" then
-        return get_secret(auth_conf)
-    elseif auth_conf.algorithm == "RS256" or auth_conf.algorithm == "ES256"  
then
-        return auth_conf.public_key
+
+local function get_auth_secret(consumer)
+    if not consumer.auth_conf.algorithm or consumer.auth_conf.algorithm:sub(1, 
2) == "HS" then
+        return get_secret(consumer.auth_conf)
+    else
+        return consumer.auth_conf.public_key
     end
 end
 
+
 local function find_consumer(conf, ctx)
     -- fetch token and hide credentials if necessary
     local jwt_token, err = fetch_jwt_token(conf, ctx)
@@ -249,19 +292,19 @@ local function find_consumer(conf, ctx)
         return nil, nil, "Missing JWT token in request"
     end
 
-    local jwt_obj = jwt:load_jwt(jwt_token)
-    core.log.info("jwt object: ", core.json.delay_encode(jwt_obj))
-    if not jwt_obj.valid then
-        err = "JWT token invalid: " .. jwt_obj.reason
+    local jwt, err = jwt_parser.new(jwt_token)
+    if not jwt then
+        err = "JWT token invalid: " .. err
         if auth_utils.is_running_under_multi_auth(ctx) then
             return nil, nil, err
         end
         core.log.warn(err)
         return nil, nil, "JWT token invalid"
     end
+    core.log.debug("parsed jwt object: ", core.json.delay_encode(jwt, true))
 
     local key_claim_name = conf.key_claim_name
-    local user_key = jwt_obj.payload and jwt_obj.payload[key_claim_name]
+    local user_key = jwt.payload and jwt.payload[key_claim_name]
     if not user_key then
         return nil, nil, "missing user key in JWT token"
     end
@@ -272,7 +315,7 @@ local function find_consumer(conf, ctx)
         return nil, nil, "Invalid user key in JWT token"
     end
 
-    local auth_secret, err = get_auth_secret(consumer.auth_conf)
+    local auth_secret, err = get_auth_secret(consumer)
     if not auth_secret then
         err = "failed to retrieve secrets, err: " .. err
         if auth_utils.is_running_under_multi_auth(ctx) then
@@ -281,14 +324,10 @@ local function find_consumer(conf, ctx)
         core.log.error(err)
         return nil, nil, "failed to verify jwt"
     end
-    local claim_specs = jwt:get_default_validation_options(jwt_obj)
-    claim_specs.lifetime_grace_period = 
consumer.auth_conf.lifetime_grace_period
-
-    jwt_obj = jwt:verify_jwt_obj(auth_secret, jwt_obj, claim_specs)
-    core.log.info("jwt object: ", core.json.delay_encode(jwt_obj))
 
-    if not jwt_obj.verified then
-        err = "failed to verify jwt: " .. jwt_obj.reason
+    -- Now verify the JWT signature
+    if not jwt:verify_signature(auth_secret) then
+        local err = "failed to verify jwt: signature mismatch: " .. 
jwt.signature
         if auth_utils.is_running_under_multi_auth(ctx) then
             return nil, nil, err
         end
@@ -296,8 +335,21 @@ local function find_consumer(conf, ctx)
         return nil, nil, "failed to verify jwt"
     end
 
+    -- Verify the JWT registered claims
+    local ok, err = jwt:verify_claims(conf.claims_to_verify, {
+        lifetime_grace_period = consumer.auth_conf.lifetime_grace_period
+    })
+    if not ok then
+        err = "failed to verify jwt: " .. err
+        if auth_utils.is_running_under_multi_auth(ctx) then
+            return nil, nil, err
+        end
+        core.log.error(err)
+        return nil, nil, "failed to verify jwt"
+    end
+
     if conf.store_in_ctx then
-        ctx.jwt_auth_payload = jwt_obj.payload
+        ctx.jwt_auth_payload = jwt.payload
     end
 
     return consumer, consumer_conf
diff --git a/apisix/plugins/jwt-auth/parser.lua 
b/apisix/plugins/jwt-auth/parser.lua
new file mode 100644
index 000000000..303340f56
--- /dev/null
+++ b/apisix/plugins/jwt-auth/parser.lua
@@ -0,0 +1,290 @@
+--
+-- 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 buffer = require "string.buffer"
+local openssl_digest = require "resty.openssl.digest"
+local openssl_mac = require "resty.openssl.mac"
+local openssl_pkey = require "resty.openssl.pkey"
+local base64 = require "ngx.base64"
+local core = require "apisix.core"
+local jwt = require("resty.jwt")
+
+local ngx_time = ngx.time
+local http_time = ngx.http_time
+local string_fmt = string.format
+local assert = assert
+local setmetatable = setmetatable
+local ipairs = ipairs
+local type = type
+local error = error
+local pcall = pcall
+
+local default_claims = {
+    "nbf",
+    "exp"
+}
+
+local alg_sign = {
+    HS256 = function(data, key)
+        return openssl_mac.new(key, "HMAC", nil, "sha256"):final(data)
+    end,
+    HS384 = function(data, key)
+        return openssl_mac.new(key, "HMAC", nil, "sha384"):final(data)
+    end,
+    HS512 = function(data, key)
+        return openssl_mac.new(key, "HMAC", nil, "sha512"):final(data)
+    end,
+    RS256 = function(data, key)
+        local digest = openssl_digest.new("sha256")
+        assert(digest:update(data))
+        return assert(openssl_pkey.new(key):sign(digest))
+    end,
+    RS384 = function(data, key)
+        local digest = openssl_digest.new("sha384")
+        assert(digest:update(data))
+        return assert(openssl_pkey.new(key):sign(digest))
+    end,
+    RS512 = function(data, key)
+        local digest = openssl_digest.new("sha512")
+        assert(digest:update(data))
+        return assert(openssl_pkey.new(key):sign(digest))
+    end,
+    ES256 = function(data, key)
+        local pkey = openssl_pkey.new(key)
+        local sig = assert(pkey:sign(data, "sha256", nil, {ecdsa_use_raw = 
true}))
+        if not sig then
+            return nil
+        end
+        return sig
+    end,
+    ES384 = function(data, key)
+        local pkey = openssl_pkey.new(key)
+        local sig = assert(pkey:sign(data, "sha384", nil, {ecdsa_use_raw = 
true}))
+        if not sig then
+            return nil
+        end
+        return sig
+    end,
+    ES512 = function(data, key)
+        local pkey = openssl_pkey.new(key)
+        local sig = assert(pkey:sign(data, "sha512", nil, {ecdsa_use_raw = 
true}))
+        if not sig then
+            return nil
+        end
+        return sig
+    end,
+    PS256 = function(data, key)
+        local pkey = openssl_pkey.new(key)
+        local sig = assert(pkey:sign(data, "sha256", 
openssl_pkey.PADDINGS.RSA_PKCS1_PSS_PADDING))
+        if not sig then
+            return nil
+        end
+        return sig
+    end,
+    PS384 = function(data, key)
+        local pkey = openssl_pkey.new(key)
+        local sig = assert(pkey:sign(data, "sha384", 
openssl_pkey.PADDINGS.RSA_PKCS1_PSS_PADDING))
+        if not sig then
+            return nil
+        end
+        return sig
+    end,
+    PS512 = function(data, key)
+        local pkey = openssl_pkey.new(key)
+        local sig = assert(pkey:sign(data, "sha512", 
openssl_pkey.PADDINGS.RSA_PKCS1_PSS_PADDING))
+        if not sig then
+            return nil
+        end
+        return sig
+    end,
+    EdDSA = function(data, key)
+        local pkey = assert(openssl_pkey.new(key))
+        return assert(pkey:sign(data))
+    end
+}
+
+local alg_verify = {
+    HS256 = function(data, signature, key)
+        return signature == alg_sign.HS256(data, key)
+    end,
+    HS384 = function(data, signature, key)
+        return signature == alg_sign.HS384(data, key)
+    end,
+    HS512 = function(data, signature, key)
+        return signature == alg_sign.HS512(data, key)
+    end,
+    RS256 = function(data, signature, key)
+        local pkey, _ = openssl_pkey.new(key)
+        assert(pkey, "Consumer Public Key is Invalid")
+        return pkey:verify(signature, data, "sha256")
+    end,
+    RS384 = function(data, signature, key)
+        local pkey, _ = openssl_pkey.new(key)
+        assert(pkey, "Consumer Public Key is Invalid")
+        return pkey:verify(signature, data, "sha384")
+    end,
+    RS512 = function(data, signature, key)
+        local pkey, _ = openssl_pkey.new(key)
+        assert(pkey, "Consumer Public Key is Invalid")
+        return pkey:verify(signature, data, "sha512")
+    end,
+    ES256 = function(data, signature, key)
+        local pkey, _ = openssl_pkey.new(key)
+        assert(pkey, "Consumer Public Key is Invalid")
+        assert(#signature == 64, "Signature must be 64 bytes.")
+        return pkey:verify(signature, data, "sha256", nil, {ecdsa_use_raw = 
true})
+    end,
+    ES384 = function(data, signature, key)
+        local pkey, _ = openssl_pkey.new(key)
+        assert(pkey, "Consumer Public Key is Invalid")
+        assert(#signature == 96, "Signature must be 96 bytes.")
+        return pkey:verify(signature, data, "sha384", nil, {ecdsa_use_raw = 
true})
+    end,
+    ES512 = function(data, signature, key)
+        local pkey, _ = openssl_pkey.new(key)
+        assert(pkey, "Consumer Public Key is Invalid")
+        assert(#signature == 132, "Signature must be 132 bytes.")
+        return pkey:verify(signature, data, "sha512", nil, {ecdsa_use_raw = 
true})
+    end,
+    PS256 = function(data, signature, key)
+        local pkey, _ = openssl_pkey.new(key)
+        assert(pkey, "Consumer Public Key is Invalid")
+        assert(#signature == 256, "Signature must be 256 bytes")
+        return pkey:verify(signature, data, "sha256", 
openssl_pkey.PADDINGS.RSA_PKCS1_PSS_PADDING)
+    end,
+    PS384 = function(data, signature, key)
+        local pkey, _ = openssl_pkey.new(key)
+        assert(pkey, "Consumer Public Key is Invalid")
+        assert(#signature == 256, "Signature must be 256 bytes")
+        return pkey:verify(signature, data, "sha384", 
openssl_pkey.PADDINGS.RSA_PKCS1_PSS_PADDING)
+    end,
+    PS512 = function(data, signature, key)
+        local pkey, _ = openssl_pkey.new(key)
+        assert(pkey, "Consumer Public Key is Invalid")
+        assert(#signature == 256, "Signature must be 256 bytes")
+        return pkey:verify(signature, data, "sha512", 
openssl_pkey.PADDINGS.RSA_PKCS1_PSS_PADDING)
+    end,
+    EdDSA = function(data, signature, key)
+        local pkey, _ = openssl_pkey.new(key)
+        assert(pkey, "Consumer Public Key is Invalid")
+        return pkey:verify(signature, data)
+    end
+}
+
+local claims_checker = {
+    nbf = {
+        type = "number",
+        check = function(nbf, conf)
+            local clock_leeway = conf and conf.lifetime_grace_period or 0
+            if nbf < ngx_time() + clock_leeway then
+                return true
+            end
+            return false, string_fmt("'nbf' claim not valid until %s", 
http_time(nbf))
+        end
+    },
+    exp = {
+        type = "number",
+        check = function(exp, conf)
+            local clock_leeway = conf and conf.lifetime_grace_period or 0
+            if exp > ngx_time() - clock_leeway then
+                return true
+            end
+            return false, string_fmt("'exp' claim expired at %s", 
http_time(exp))
+        end
+    }
+}
+
+local base64_encode = base64.encode_base64url
+local base64_decode = base64.decode_base64url
+
+local _M = {}
+
+function _M.new(token)
+    local jwt_obj = jwt:load_jwt(token)
+    if type(jwt_obj) == "table" and not jwt_obj.valid then
+        return nil, jwt_obj.reason
+    end
+    return setmetatable(jwt_obj, {__index = _M})
+end
+
+
+function _M.verify_signature(self, key)
+    return alg_verify[self.header.alg](self.raw_header .. "." ..
+               self.raw_payload, base64_decode(self.signature), key)
+end
+
+
+function _M.verify_claims(self, claims, conf)
+    if not claims then
+        claims = default_claims
+    end
+
+    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
+        end
+    end
+
+    return true
+end
+
+
+function _M.encode(alg, key, header, data)
+    alg = alg or "HS256"
+    if not alg_sign[alg] then
+        return nil, "algorithm not supported"
+    end
+
+    if type(key) ~= "string" then
+        error("Argument #2 must be string", 2)
+        return nil, "key must be a string"
+    end
+
+    if header and type(header) ~= "table" then
+        return nil, "header must be a table"
+    end
+
+    if type(data) ~= "table" then
+        return nil, "data must be a table"
+    end
+
+    local header = header or {typ = "JWT", alg = alg}
+    local buf = buffer.new()
+
+    buf:put(base64_encode(core.json.encode(header)))
+        :put(".")
+        :put(base64_encode(core.json.encode(data)))
+
+    local ok, signature = pcall(alg_sign[alg], buf:tostring(), key)
+    if not ok then
+        return nil, signature
+    end
+
+    buf:put("."):put(base64_encode(signature))
+
+    return buf:get()
+end
+
+return _M
diff --git a/docs/en/latest/plugins/jwt-auth.md 
b/docs/en/latest/plugins/jwt-auth.md
index a85d84429..ffe5ccaec 100644
--- a/docs/en/latest/plugins/jwt-auth.md
+++ b/docs/en/latest/plugins/jwt-auth.md
@@ -49,7 +49,7 @@ For Consumer/Credential:
 | key           | string  | True                                               
   |         |     non-empty       | Unique key for a Consumer.                 
                                                                                
                                                                 |
 | secret        | string  | False                                              
   |         |        non-empty        | Shared key used to sign and verify the 
JWT when the algorithm is symmetric. Required when using `HS256` or `HS512` as 
the algorithm. This field supports saving the value in Secret Manager using the 
[APISIX Secret](../terminology/secret.md) resource.       |
 | public_key    | string  | True if `RS256` or `ES256` is set for the 
`algorithm` attribute. |         |                             | RSA or ECDSA 
public key. This field supports saving the value in Secret Manager using the 
[APISIX Secret](../terminology/secret.md) resource.                      |
-| algorithm     | string  | False                                              
   | HS256 | ["HS256","HS512","RS256","ES256"] | Encryption algorithm.          
                                                                                
                                                                             |
+| algorithm     | string  | False                                              
   | HS256 | ["HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", 
"ES384", "ES512", "PS256", "PS384", "PS512", "EdDSA"] | Encryption algorithm.   
                                                                                
                                                                                
    |
 | exp           | integer | False                                              
   | 86400   | [1,...]                     | Expiry time of the token in 
seconds.                                                                        
                                                                                
|
 | base64_secret | boolean | False                                              
   | false   |                             | Set to true if the secret is 
base64 encoded.                                                                 
                                                                               |
 | lifetime_grace_period | integer | False                                      
   | 0       | [0,...]                     | Grace period in seconds. Used to 
account for clock skew between the server generating the JWT and the server 
validating the JWT.  |
@@ -69,6 +69,7 @@ For Routes or Services:
 | anonymous_consumer | string | False  | false  | Anonymous Consumer name. If 
configured, allow anonymous users to bypass the authentication.   |
 | store_in_ctx     | boolean | False    | false   | Set to true will store the 
JWT payload in the request context (`ctx.jwt_auth_payload`). This allows 
lower-priority plugins that run afterwards on the same request to retrieve and 
use the JWT token. |
 | realm            | string  | False    | jwt     | The realm to include in 
the `WWW-Authenticate` header when authentication fails. |
+| claims_to_verify | array[string] | False | ["exp", "nbf"] | ["exp", "nbf"] | 
The claims that need to be verified in the JWT payload. |
 
 You can implement `jwt-auth` with [HashiCorp 
Vault](https://www.vaultproject.io/) to store and fetch secrets and RSA keys 
pairs from its [encrypted KV 
engine](https://developer.hashicorp.com/vault/docs/secrets/kv) using the 
[APISIX Secret](../terminology/secret.md) resource.
 
diff --git a/docs/zh/latest/plugins/jwt-auth.md 
b/docs/zh/latest/plugins/jwt-auth.md
index 878cf68fc..e761732e7 100644
--- a/docs/zh/latest/plugins/jwt-auth.md
+++ b/docs/zh/latest/plugins/jwt-auth.md
@@ -45,7 +45,7 @@ Consumer/Credential 端:
 | key           | string  | 是    |         |                             | 
消费者的唯一密钥。  |
 | secret        | string  | 否    |         |                             | 
当使用对称算法时,用于对 JWT 进行签名和验证的共享密钥。使用 `HS256` 或 `HS512` 作为算法时必填。该字段支持使用 [APISIX 
Secret](../terminology/secret.md) 资源,将值保存在 Secret Manager 中。   |
 | public_key    | string  | 否    |         |                             | RSA 
或 ECDSA 公钥, `algorithm` 属性选择 `RS256` 或 `ES256` 算法时必选。该字段支持使用 [APISIX 
Secret](../terminology/secret.md) 资源,将值保存在 Secret Manager 中。       |
-| algorithm     | string  | 否    | "HS256" | ["HS256","HS512","RS256","ES256"] 
| 加密算法。                                                                         
                             |
+| algorithm     | string  | 否    | "HS256" | ["HS256", "HS384", "HS512", 
"RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", 
"PS512", "EdDSA"] | 加密算法。                                                       
                                               |
 | exp           | integer | 否    | 86400   | [1,...]                     | 
token 的超时时间。                                                                    
                          |
 | base64_secret | boolean | 否    | false   |                             | 
当设置为 `true` 时,密钥为 base64 编码。                                                    
                                     |
 | lifetime_grace_period | integer | 否    | 0  | [0,...]                  | 
宽限期(以秒为单位)。用于解决生成 JWT 的服务器与验证 JWT 的服务器之间的时钟偏差。 |
@@ -64,6 +64,7 @@ Route 端:
 | key_claim_name | string  | 否     | key           | 包含用户密钥(对应消费者的密钥属性)的 JWT 
声明的名称。|
 | anonymous_consumer | string | 否     | false  | 匿名消费者名称。如果已配置,则允许匿名用户绕过身份验证。  
|
 | store_in_ctx | boolean | 否     | false  | 设置为 `true` 将会将 JWT 负载存储在请求上下文 
(`ctx.jwt_auth_payload`) 中。这允许在同一请求上随后运行的低优先级插件检索和使用 JWT 令牌。 |
+| claims_to_verify | array[string] | 否 | ["exp", "nbf"] | ["exp", "nbf"] | 需要在 
JWT 负载中验证的声明。 |
 
 您可以使用 [HashiCorp Vault](https://www.vaultproject.io/) 实施 `jwt-auth`,以从其[加密的 KV 
引擎](https://developer.hashicorp.com/vault/docs/secrets/kv) 使用 [APISIX 
Secret](../terminology/secret.md) 资源。
 
diff --git a/t/plugin/jwt-auth-more-algo.t b/t/plugin/jwt-auth-more-algo.t
new file mode 100644
index 000000000..3fb92d02c
--- /dev/null
+++ b/t/plugin/jwt-auth-more-algo.t
@@ -0,0 +1,410 @@
+#
+# 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();
+log_level("debug");
+
+add_block_preprocessor(sub {
+    my ($block) = @_;
+
+    if (!defined $block->request) {
+        $block->set_value("request", "GET /t");
+    }
+});
+
+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": {
+                        "jwt-auth": {
+                            "key": "user-key",
+                            "secret": "my-secret-key",
+                            "algorithm": "HS384"
+                        }
+                    }
+                }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 2: enable jwt 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": {
+                        "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 3: create public API route (jwt-auth sign)
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/2',
+                 ngx.HTTP_PUT,
+                 [[{
+                        "plugins": {
+                            "public-api": {}
+                        },
+                        "uri": "/apisix/plugin/jwt/sign"
+                 }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 4: sign / verify in argument
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+
+            local code, _, res = 
t('/hello?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MjA4NTA4Nzc5Mn0.6BNfYOnGvB27uY5LIwZFgIV_g42wiqLSlITtgAXinuZA9DNcquCTiudmbaXCHj20',
+                ngx.HTTP_GET
+            )
+
+            ngx.status = code
+            ngx.print(res)
+        }
+    }
+--- response_body
+hello world
+--- error_log
+"alg":"HS384"
+
+
+
+=== TEST 5: verify: invalid JWT token
+--- request
+GET 
/hello?jwt=invalid-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTU2Mzg3MDUwMX0.pPNVvh-TQsdDzorRwa-uuiLYiEBODscp9wv0cwD6c68
+--- error_code: 401
+--- response_body
+{"message":"JWT token invalid"}
+--- error_log
+JWT token invalid: invalid header: invalid-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
+
+
+
+=== TEST 6: verify token with algorithm HS256
+--- request
+GET /hello
+--- more_headers
+Authorization: 
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTg3OTMxODU0MX0.fNtFJnNmJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs
+--- response_body
+hello world
+--- error_log
+"alg":"HS256"
+
+
+
+=== TEST 7: missing public key and private 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": {
+                        "jwt-auth": {
+                            "key": "user-key",
+                            "secret": "my-secret-key",
+                            "algorithm": "PS256"
+                        }
+                    }
+                }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.print(body)
+        }
+    }
+--- error_code: 400
+--- response_body
+{"error_msg":"invalid plugins configuration: failed to check the configuration 
of plugin jwt-auth err: failed to validate dependent schema for \"algorithm\": 
value should match only one schema, but matches none"}
+
+
+
+=== TEST 8: missing public key and private key
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local json = require("cjson").encode
+            local cons_tab = {
+                username = "jack",
+                plugins = {
+                    ["jwt-auth"] = {
+                        key = "user-key2",
+                        secret = "my-secret-key",
+                        algorithm = "PS256",
+                        public_key = "-----BEGIN PUBLIC 
KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAiSpoCgu3GzeExroi2YQ+\nxcQlXqEO8D5/5DgrlGsEb3Y9kEX+lj3ayW/G93nAob1xrtpjzBLf4chDivcmMj1q\nOwggoAOOmC9D/EYzDNKAos/gNcgsxra1X7xdMje+jUYR8nQGLemkidD71XbOrrcy\nLTE886t/lcrauC3dxNl55DkZc22YZWSanmizGfedMIEVtZb08uXbTi+8KyP3d+QL\nKYQ2eSa8AQredrKmM0eREQHr6R+zz6xqgycJ/Pxp+C0UYFbV+LVnHom5u6ck2SNG\nuGI1sBQ3V763BArbGpWlpcetQT5JB8QDhywf1ihNdaJgWhswQJVSMpJ8ZmA8R1Av\nDQIDAQAB\n-----END
 PUBLIC KEY-----"
+                    }
+                }
+            }
+            local code, body = t('/apisix/admin/consumers',
+                ngx.HTTP_PUT,
+                json(cons_tab)
+                )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 9: sign / verify in argument
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+
+            local code, _, res = t('/hello?jwt=' .. 
"eyJ0eXAiOiJKV1QiLCJ4NWMiOlsiLS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS1cbk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBaVNwb0NndTNHemVFeHJvaTJZUStcbnhjUWxYcUVPOEQ1LzVEZ3JsR3NFYjNZOWtFWCtsajNheVcvRzkzbkFvYjF4cnRwanpCTGY0Y2hEaXZjbU1qMXFcbk93Z2dvQU9PbUM5RC9FWXpETktBb3MvZ05jZ3N4cmExWDd4ZE1qZStqVVlSOG5RR0xlbWtpZEQ3MVhiT3JyY3lcbkxURTg4NnQvbGNyYXVDM2R4Tmw1NURrWmMyMllaV1Nhbm1pekdmZWRNSUVWdFpiMDh1WGJUaSs4S3lQM2QrUUxcbktZUTJlU2E4QVFyZWRyS21
 [...]
+                ngx.HTTP_GET
+            )
+
+            ngx.status = code
+            ngx.print(res)
+        }
+    }
+--- response_body
+hello world
+--- error_log
+"alg":"PS256"
+
+
+
+=== TEST 10: verify token with algorithm PS356
+--- request
+GET /hello
+--- more_headers
+Authorization: 
eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ind3dy5iZWpzb24uY29tIiwic3ViIjoiZGVtbyIsImtleSI6InVzZXIta2V5MiIsImV4cCI6MjA4OTcyNDA1N30.cKaijeZ4ydKVKCC37UZObPFj_kVsdiScEuGwK_G9JBjg0dcRnL8Xvr6Ofp8kDJz16FO2vy8FHgA_9HVjVpzehNe-AbtYJ88Qopy2pAQHsottGuQe3jgAt-yBI5chf26GzpqTtyymteg-lt-cW6EoP4gVHfXEbzQaOZt0wmdNBX17jISKW70okdxrp7cJKbv4hXQXjhYwKY8h0jYnGb-RhuHXRwWFhp6TZVV57Lfpi1yUDm6GqXM42W7owOOwjUqS8-7KYv1iugQzTo7qcVjPic7X5Wug7N-4t8BRM9jZkUiNrAY2BoxxBMUUru4fd201KY23p4bZDwQFpg6MVck7XA
+--- response_body
+hello world
+
+
+
+=== TEST 11: 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": {
+                        "jwt-auth": {
+                            "key": "user-key",
+                            "secret": "my-secret-key",
+                            "algorithm": "HS384"
+                        }
+                    }
+                }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 12: only verify nbf claim
+--- 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": ["nbf"]
+                        }
+                    },
+                    "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 13: verify success with expired token
+--- request
+GET /hello
+--- more_headers
+Authorization: 
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTU2Mzg3MDUwMX0.pPNVvh-TQsdDzorRwa-uuiLYiEBODscp9wv0cwD6c68
+--- response_body
+hello world
+
+
+
+=== TEST 14: verify failed before nbf claim
+--- request
+GET /hello
+--- more_headers
+Authorization: 
eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTU2Mzg3MDUwMSwibmJmIjoyMjI5NjcxODc0fQ.RJynr34TyCesYHwvDwOwETi1vOfZXKqc_wvQJ3pijBfrx1x5IF3O1CCUCvd5lMYf
+--- error_code: 401
+--- response_body eval
+qr/failed to verify jwt/
+--- error_log
+'nbf' claim not valid until Mon, 27 Aug 2040 09:17:54 GMT
+
+
+
+=== TEST 15: verify success after nbf claim
+--- request
+GET /hello
+--- more_headers
+Authorization: 
eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTU2Mzg3MDUwMSwibmJmIjoxNzI5Njc1MDQyfQ.IycpH4Lc48BHSxUBXBNDXGawvNgi_6a-qsa-xnhYFLooeWc8DyX8zLadvyEFpMPq
+--- response_body
+hello world
+
+
+
+=== TEST 16: EdDSA algorithm
+--- 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",
+                            "algorithm": "EdDSA",
+                            "public_key": "-----BEGIN PUBLIC 
KEY-----\nMCowBQYDK2VwAyEA9PdGVALrrBX4oX5t9DKb5JHYx7XRb0RXU42r0FVO2sA=\n-----END
 PUBLIC KEY-----",
+                            "private_key": "-----BEGIN PRIVATE 
KEY-----\nMC4CAQAwBQYDK2VwBCIEIKmBJXpq9Fp0K97TpJ2X9V6jszx23j7NtKKa6gZRaAjI\n-----END
 PRIVATE KEY-----"
+                        }
+                    }
+                }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 17: sign / verify in argument
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+
+            local code, _, res = t('/hello?jwt=' .. 
"eyJ4NWMiOlsiLS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS1cbk1Db3dCUVlESzJWd0F5RUE5UGRHVkFMcnJCWDRvWDV0OURLYjVKSFl4N1hSYjBSWFU0MnIwRlZPMnNBPVxuLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tIl0sInR5cCI6IkpXVCIsImFsZyI6IkVkRFNBIn0.eyJleHAiOjIwODUwNzA2MDQsImtleSI6InVzZXIta2V5In0.FmPpxVDubPukcnW58DICZOYMqvkikn4TuUzIQX68-s9KOBUhOgH1_TZM3gUk5Wv0L86c4joVzU7hqstsJSs0Cw",
+                ngx.HTTP_GET
+            )
+
+            ngx.status = code
+            ngx.print(res)
+        }
+    }
+--- response_body
+hello world
+--- error_log
+"alg":"EdDSA"
diff --git a/t/plugin/jwt-auth.t b/t/plugin/jwt-auth.t
index 91f883feb..26277bfb5 100644
--- a/t/plugin/jwt-auth.t
+++ b/t/plugin/jwt-auth.t
@@ -953,28 +953,7 @@ hello world
 
 
 
-=== TEST 42: test for unsupported algorithm
---- config
-    location /t {
-        content_by_lua_block {
-            local plugin = require("apisix.plugins.jwt-auth")
-            local core = require("apisix.core")
-            local conf = {key = "123", algorithm = "ES512"}
-
-            local ok, err = plugin.check_schema(conf, 
core.schema.TYPE_CONSUMER)
-            if not ok then
-                ngx.say(err)
-            end
-
-            ngx.say(require("toolkit.json").encode(conf))
-        }
-    }
---- response_body_like eval
-qr/property "algorithm" validation failed/
-
-
-
-=== TEST 43: wrong format of secret
+=== TEST 42: wrong format of secret
 --- config
     location /t {
         content_by_lua_block {
@@ -997,7 +976,7 @@ base64_secret required but the secret is not in base64 
format
 
 
 
-=== TEST 44: when the exp value is not set, make sure the default value(86400) 
works
+=== TEST 43: when the exp value is not set, make sure the default value(86400) 
works
 --- config
     location /t {
         content_by_lua_block {
@@ -1022,7 +1001,7 @@ passed
 
 
 
-=== TEST 45: RS256 without public key
+=== TEST 44: RS256 without public key
 --- config
     location /t {
         content_by_lua_block {
@@ -1049,7 +1028,7 @@ qr/failed to validate dependent schema for 
\\"algorithm\\"/
 
 
 
-=== TEST 46: RS256 without private key
+=== TEST 45: RS256 without private key
 --- config
     location /t {
         content_by_lua_block {
@@ -1075,7 +1054,7 @@ qr/failed to validate dependent schema for 
\\"algorithm\\"/
 
 
 
-=== TEST 47: add consumer with username and plugins with public_key
+=== TEST 46: add consumer with username and plugins with public_key, 
private_key(ES256)
 --- config
     location /t {
         content_by_lua_block {
@@ -1105,7 +1084,7 @@ passed
 
 
 
-=== TEST 48: JWT sign and verify use ES256 algorithm(private_key numbits = 512)
+=== TEST 47: JWT sign and verify use ES256 algorithm(private_key numbits = 512)
 --- config
     location /t {
         content_by_lua_block {
@@ -1139,7 +1118,7 @@ passed
 
 
 
-=== TEST 49: sign/verify use ES256 algorithm(private_key numbits = 512)
+=== TEST 48: sign/verify use ES256 algorithm(private_key numbits = 512)
 --- config
     location /t {
         content_by_lua_block {
@@ -1164,7 +1143,7 @@ hello world
 
 
 
-=== TEST 50: add consumer missing public_key (algorithm=RS256)
+=== TEST 49: add consumer missing public_key (algorithm=RS256)
 --- config
     location /t {
         content_by_lua_block {
@@ -1192,7 +1171,7 @@ hello world
 
 
 
-=== TEST 51: add consumer missing public_key (algorithm=ES256)
+=== TEST 50: add consumer missing public_key (algorithm=ES256)
 --- config
     location /t {
         content_by_lua_block {
@@ -1220,7 +1199,7 @@ hello world
 
 
 
-=== TEST 52: secret is required when algorithm is not RS256 or ES256
+=== TEST 51: secret is required when algorithm is not RS256 or ES256
 --- config
     location /t {
         content_by_lua_block {
diff --git a/t/plugin/jwt-auth4.t b/t/plugin/jwt-auth4.t
index b1e873f7d..c2a4822db 100644
--- a/t/plugin/jwt-auth4.t
+++ b/t/plugin/jwt-auth4.t
@@ -424,3 +424,37 @@ GET /t
 (PASS: Ed448 signature verification successful|FAIL: Ed448 signature 
verification failed)
 --- no_error_log
 [error]
+
+
+
+=== TEST 11: secret is required for HS algorithms
+--- 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",
+                            "algorithm": "HS384"
+                        }
+                    }
+                }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.log(ngx.ERR, body)
+            ngx.say("failed")
+
+        }
+    }
+--- error_code: 400
+--- response_body
+failed
+--- error_log
+property \"secret\" is required when using HS based algorithms


Reply via email to