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