From 5214b9c77f05adbc04d1e29de4f0d3fdb7f8aefe Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Tue, 27 Jan 2026 14:24:16 +0545 Subject: [PATCH 01/11] feat(jwt): support more algorithm Signed-off-by: Abhishek Choudhary --- apisix/plugins/jwt-auth.lua | 135 ++++++++-- apisix/plugins/jwt-auth/parser.lua | 266 ++++++++++++++++++ t/plugin/jwt-auth-more-algo.t | 419 +++++++++++++++++++++++++++++ t/plugin/jwt-auth.t | 41 +-- 4 files changed, 802 insertions(+), 59 deletions(-) create mode 100644 apisix/plugins/jwt-auth/parser.lua create mode 100644 t/plugin/jwt-auth-more-algo.t diff --git a/apisix/plugins/jwt-auth.lua b/apisix/plugins/jwt-auth.lua index 4c32609bff35..21f8433bfd1a 100644 --- a/apisix/plugins/jwt-auth.lua +++ b/apisix/plugins/jwt-auth.lua @@ -15,20 +15,22 @@ -- limitations under the License. -- local core = require("apisix.core") -local jwt = require("resty.jwt") local consumer_mod = require("apisix.consumer") +local resty_random = require("resty.random") local new_tab = require ("table.new") local auth_utils = require("apisix.utils.auth") local ngx_decode_base64 = ngx.decode_base64 +local ngx_encode_base64 = ngx.encode_base64 local ngx = ngx +local ngx_time = ngx.time local sub_str = string.sub 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 jwt_parser = require("apisix.plugins.jwt-auth.parser") local schema = { type = "object", @@ -60,6 +62,15 @@ 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, + default = {"exp", "nbf"}, + }, }, } @@ -77,7 +88,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,7 +122,7 @@ local consumer_schema = { { properties = { algorithm = { - enum = {"HS256", "HS512"}, + enum = {"HS256", "HS384", "HS512"}, default = "HS256" }, }, @@ -106,7 +131,18 @@ local consumer_schema = { properties = { public_key = {type = "string"}, algorithm = { - enum = {"RS256", "ES256"}, + enum = { + "RS256", + "RS384", + "RS512", + "ES256", + "ES384", + "ES512", + "PS256", + "PS384", + "PS512", + "EdDSA", + }, }, }, required = {"public_key"}, @@ -141,15 +177,24 @@ function _M.check_schema(conf, schema_type) return false, err end + local is_hs_alg = conf.algorithm:sub(1, 2) == "HS" 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\"" + end + + if is_hs_alg and not conf.secret then + conf.secret = ngx_encode_base64(resty_random.bytes(32, true)) elseif 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 +277,48 @@ 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_real_payload(key, auth_conf, payload) + local real_payload = { + key = key, + exp = ngx_time() + auth_conf.exp + } + if payload then + local extra_payload = core.json.decode(payload) + core.table.merge(real_payload, extra_payload) + end + return real_payload +end + + +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 gen_jwt_header(consumer) + local x5c + if consumer.auth_conf.algorithm and consumer.auth_conf.algorithm:sub(1, 2) ~= "HS" then + local public_key = consumer.auth_conf.public_key + if not public_key then + core.log.error("failed to sign jwt, err: missing public key") + core.response.exit(503, "failed to sign jwt") + end + x5c = {public_key} end + + return { + typ = "JWT", + alg = consumer.auth_conf.algorithm, + x5c = x5c + } 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 +327,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 +350,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,23 +359,24 @@ 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)) + -- Now verify the JWT signature + if not jwt:verify_signature(auth_secret) then + core.log.warn("failed to verify jwt: signature mismatch: ", jwt.signature) + return nil, nil, "failed to verify jwt" + end - if not jwt_obj.verified then - err = "failed to verify jwt: " .. jwt_obj.reason - if auth_utils.is_running_under_multi_auth(ctx) then - return nil, nil, err - end - core.log.warn(err) + -- 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 + core.log.error("failed to verify jwt: ", 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 000000000000..f1d8917f76f6 --- /dev/null +++ b/apisix/plugins/jwt-auth/parser.lua @@ -0,0 +1,266 @@ +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 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.format("'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.format("'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 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.get_default_claims(self) + return { + "nbf", + "exp" + } +end + + +function _M.verify_claims(self, claims, conf) + if not claims then + claims = self:get_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/t/plugin/jwt-auth-more-algo.t b/t/plugin/jwt-auth-more-algo.t new file mode 100644 index 000000000000..2fd7cced6a57 --- /dev/null +++ b/t/plugin/jwt-auth-more-algo.t @@ -0,0 +1,419 @@ +# +# 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, err, sign = t('/apisix/plugin/jwt/sign?key=user-key', + ngx.HTTP_GET + ) + + if code > 200 then + ngx.status = code + ngx.say(err) + return + end + + local code, _, res = t('/hello?jwt=' .. sign, + 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=' .. "eyJ0eXAiOiJKV1QiLCJ4NWMiOlsiLS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS1cbk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBaVNwb0NndTNHemVFeHJvaTJZUStcbnhjUWxYcUVPOEQ1LzVEZ3JsR3NFYjNZOWtFWCtsajNheVcvRzkzbkFvYjF4cnRwanpCTGY0Y2hEaXZjbU1qMXFcbk93Z2dvQU9PbUM5RC9FWXpETktBb3MvZ05jZ3N4cmExWDd4ZE1qZStqVVlSOG5RR0xlbWtpZEQ3MVhiT3JyY3lcbkxURTg4NnQvbGNyYXVDM2R4Tmw1NURrWmMyMllaV1Nhbm1pekdmZWRNSUVWdFpiMDh1WGJUaSs4S3lQM2QrUUxcbktZUTJlU2E4QVFyZWRyS21NMGVSRVFIcjZSK3p6NnhxZ3ljSi9QeHArQzBVWUZiVitMVm5Ib201dTZjazJTTkdcbnVHSTFzQlEzVjc2M0JBcmJHcFdscGNldFFUNUpCOFFEaHl3ZjFpaE5kYUpnV2hzd1FKVlNNcEo4Wm1BOFIxQXZcbkRRSURBUUFCXG4tLS0tLUVORCBQVUJMSUMgS0VZLS0tLS0iXSwiYWxnIjoiUFMyNTYifQ.eyJrZXkiOiJ1c2VyLWtleTIiLCJleHAiOjIwODUwNjkxMzl9.FmtBZ-LBqyIDQV3lTiN0XaWOrl19D3s6oF3VmbZZ1xoW7gdHVtkMdOs4FrwflxUiZOyAGq7FDBVaHgbzil0LkyXwFqY8EABARUu4S9S3H0xpM6oFXvXsqoA9ygyq5Nty0L8KBI4LMm-rIL0g34pecjZG5cJEbjFhuN4bHM1ZUvJZf-VX6JMmwdueknTY0rIOM7CzComazue3u9JXrDxF1j1xkPInraUmtkUhNM90JuidAgMFVHQb8XN6U-E2Xbn6cD_kXc93Ul4WJK8H2KQNk_gwLmUXBs45wVMzZuEtJ1_nlsPHJztupSE2tSJvwX_YF1EL-v2_OYhgLBSgFsncpw", + 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 91f883febecc..26277bfb58a5 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 { From bd7bd4631bc8f5a3d118e6dfea4ff8bcffabe2ac Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Tue, 27 Jan 2026 16:13:26 +0545 Subject: [PATCH 02/11] kilin Signed-off-by: Abhishek Choudhary --- Makefile | 3 +++ apisix/plugins/jwt-auth.lua | 33 ------------------------------ apisix/plugins/jwt-auth/parser.lua | 17 +++++++++++++++ docs/en/latest/plugins/jwt-auth.md | 3 ++- docs/zh/latest/plugins/jwt-auth.md | 3 ++- 5 files changed, 24 insertions(+), 35 deletions(-) diff --git a/Makefile b/Makefile index 423b2405673b..647a0fa27e9f 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 21f8433bfd1a..c76bec91a6d7 100644 --- a/apisix/plugins/jwt-auth.lua +++ b/apisix/plugins/jwt-auth.lua @@ -23,7 +23,6 @@ local auth_utils = require("apisix.utils.auth") local ngx_decode_base64 = ngx.decode_base64 local ngx_encode_base64 = ngx.encode_base64 local ngx = ngx -local ngx_time = ngx.time local sub_str = string.sub local table_insert = table.insert local table_concat = table.concat @@ -278,19 +277,6 @@ local function get_secret(conf) end -local function get_real_payload(key, auth_conf, payload) - local real_payload = { - key = key, - exp = ngx_time() + auth_conf.exp - } - if payload then - local extra_payload = core.json.decode(payload) - core.table.merge(real_payload, extra_payload) - end - return real_payload -end - - 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) @@ -300,25 +286,6 @@ local function get_auth_secret(consumer) end -local function gen_jwt_header(consumer) - local x5c - if consumer.auth_conf.algorithm and consumer.auth_conf.algorithm:sub(1, 2) ~= "HS" then - local public_key = consumer.auth_conf.public_key - if not public_key then - core.log.error("failed to sign jwt, err: missing public key") - core.response.exit(503, "failed to sign jwt") - end - x5c = {public_key} - end - - return { - typ = "JWT", - alg = consumer.auth_conf.algorithm, - x5c = x5c - } -end - - local function find_consumer(conf, ctx) -- fetch token and hide credentials if necessary local jwt_token, err = fetch_jwt_token(conf, ctx) diff --git a/apisix/plugins/jwt-auth/parser.lua b/apisix/plugins/jwt-auth/parser.lua index f1d8917f76f6..c35567a3da30 100644 --- a/apisix/plugins/jwt-auth/parser.lua +++ b/apisix/plugins/jwt-auth/parser.lua @@ -1,3 +1,20 @@ +-- +-- 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" diff --git a/docs/en/latest/plugins/jwt-auth.md b/docs/en/latest/plugins/jwt-auth.md index a85d84429365..ffe5ccaec2fa 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 878cf68fc642..e761732e75d9 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) 资源。 From d853b4869b6bebf200d10b1cfe819d7761f159c4 Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Wed, 28 Jan 2026 19:14:24 +0545 Subject: [PATCH 03/11] lint Signed-off-by: Abhishek Choudhary --- apisix/plugins/jwt-auth/parser.lua | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apisix/plugins/jwt-auth/parser.lua b/apisix/plugins/jwt-auth/parser.lua index c35567a3da30..5e206187adce 100644 --- a/apisix/plugins/jwt-auth/parser.lua +++ b/apisix/plugins/jwt-auth/parser.lua @@ -211,7 +211,8 @@ end function _M.verify_signature(self, key) - return alg_verify[self.header.alg](self.raw_header .. "." .. self.raw_payload, base64_decode(self.signature), key) + return alg_verify[self.header.alg](self.raw_header .. "." .. + self.raw_payload, base64_decode(self.signature), key) end @@ -268,7 +269,9 @@ function _M.encode(alg, key, header, data) 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))) + 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 From c0214d64dfca7715a84f61cbf92cb7e7b4c5c6f5 Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Wed, 28 Jan 2026 19:17:55 +0545 Subject: [PATCH 04/11] fix Signed-off-by: Abhishek Choudhary --- t/plugin/jwt-auth-more-algo.t | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/t/plugin/jwt-auth-more-algo.t b/t/plugin/jwt-auth-more-algo.t index 2fd7cced6a57..9465d52680d8 100644 --- a/t/plugin/jwt-auth-more-algo.t +++ b/t/plugin/jwt-auth-more-algo.t @@ -127,17 +127,8 @@ passed location /t { content_by_lua_block { local t = require("lib.test_admin").test - local code, err, sign = t('/apisix/plugin/jwt/sign?key=user-key', - ngx.HTTP_GET - ) - - if code > 200 then - ngx.status = code - ngx.say(err) - return - end - local code, _, res = t('/hello?jwt=' .. sign, + local code, _, res = t('/hello?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTc2OTY5MzQ5OH0.1S9WJkzvmmKNRT2WnPHovSSoK6IZQhFJxtxksdfEWuDLimjdGkcYI-DIFtH65hOF', ngx.HTTP_GET ) From 0bfa51864cf40d4032938fb5250c7163fdd9ff35 Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Wed, 28 Jan 2026 19:36:20 +0545 Subject: [PATCH 05/11] multi auth Signed-off-by: Abhishek Choudhary --- apisix/plugins/jwt-auth.lua | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/apisix/plugins/jwt-auth.lua b/apisix/plugins/jwt-auth.lua index c76bec91a6d7..f7c7f6356eab 100644 --- a/apisix/plugins/jwt-auth.lua +++ b/apisix/plugins/jwt-auth.lua @@ -312,6 +312,7 @@ local function find_consumer(conf, ctx) end local consumer, consumer_conf, err = consumer_mod.find_consumer(plugin_name, "key", user_key) + core.log.warn("dibag cons: ", core.json.delay_encode(consumer)) if not consumer then core.log.warn("failed to find consumer: ", err or "invalid user key") return nil, nil, "Invalid user key in JWT token" @@ -329,7 +330,11 @@ local function find_consumer(conf, ctx) -- Now verify the JWT signature if not jwt:verify_signature(auth_secret) then - core.log.warn("failed to verify jwt: signature mismatch: ", jwt.signature) + 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 + core.log.warn(err) return nil, nil, "failed to verify jwt" end @@ -338,7 +343,11 @@ local function find_consumer(conf, ctx) lifetime_grace_period = consumer.auth_conf.lifetime_grace_period }) if not ok then - core.log.error("failed to verify jwt: ", err) + 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 From ae04a8a95ab2fd686ac3fc8aa3a4054c20eb8ef1 Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Wed, 28 Jan 2026 20:21:07 +0545 Subject: [PATCH 06/11] lint Signed-off-by: Abhishek Choudhary --- apisix/plugins/jwt-auth/parser.lua | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apisix/plugins/jwt-auth/parser.lua b/apisix/plugins/jwt-auth/parser.lua index 5e206187adce..8ac82f3db1c5 100644 --- a/apisix/plugins/jwt-auth/parser.lua +++ b/apisix/plugins/jwt-auth/parser.lua @@ -25,6 +25,13 @@ 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 alg_sign = { HS256 = function(data, key) @@ -181,7 +188,7 @@ local claims_checker = { if nbf < ngx_time() + clock_leeway then return true end - return false, string.format("'nbf' claim not valid until %s", http_time(nbf)) + return false, string_fmt("'nbf' claim not valid until %s", http_time(nbf)) end }, exp = { @@ -191,7 +198,7 @@ local claims_checker = { if exp > ngx_time() - clock_leeway then return true end - return false, string.format("'exp' claim expired at %s", http_time(exp)) + return false, string_fmt("'exp' claim expired at %s", http_time(exp)) end } } From 04f6bec9d123eba7c38183788549f625b7bea7e3 Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Mon, 2 Feb 2026 14:39:40 +0545 Subject: [PATCH 07/11] clr Signed-off-by: Abhishek Choudhary --- apisix/plugins/jwt-auth.lua | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apisix/plugins/jwt-auth.lua b/apisix/plugins/jwt-auth.lua index f7c7f6356eab..19e6cc7d6ae2 100644 --- a/apisix/plugins/jwt-auth.lua +++ b/apisix/plugins/jwt-auth.lua @@ -28,6 +28,7 @@ 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 jwt_parser = require("apisix.plugins.jwt-auth.parser") @@ -68,7 +69,6 @@ local schema = { enum = {"exp","nbf"}, }, uniqueItems = true, - default = {"exp", "nbf"}, }, }, } @@ -128,7 +128,10 @@ local consumer_schema = { }, { properties = { - public_key = {type = "string"}, + public_key = { + type = "string", + minLength = 1, + }, algorithm = { enum = { "RS256", @@ -177,14 +180,12 @@ function _M.check_schema(conf, schema_type) end local is_hs_alg = conf.algorithm:sub(1, 2) == "HS" - if (conf.algorithm == "HS256" or conf.algorithm == "HS512") and not conf.secret then + if is_hs_alg and not conf.secret then return false, "property \"secret\" is required ".. "when \"algorithm\" is \"HS256\" or \"HS512\"" end - if is_hs_alg and not conf.secret then - conf.secret = ngx_encode_base64(resty_random.bytes(32, true)) - elseif conf.base64_secret then + 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 @@ -312,7 +313,6 @@ local function find_consumer(conf, ctx) end local consumer, consumer_conf, err = consumer_mod.find_consumer(plugin_name, "key", user_key) - core.log.warn("dibag cons: ", core.json.delay_encode(consumer)) if not consumer then core.log.warn("failed to find consumer: ", err or "invalid user key") return nil, nil, "Invalid user key in JWT token" From 9b6e70e6529911847006848cef2c818eb9512a6f Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Tue, 3 Feb 2026 14:25:38 +0545 Subject: [PATCH 08/11] f Signed-off-by: Abhishek Choudhary --- apisix/plugins/jwt-auth.lua | 5 +---- t/plugin/jwt-auth4.t | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/apisix/plugins/jwt-auth.lua b/apisix/plugins/jwt-auth.lua index 19e6cc7d6ae2..55866b1d802d 100644 --- a/apisix/plugins/jwt-auth.lua +++ b/apisix/plugins/jwt-auth.lua @@ -16,12 +16,10 @@ -- local core = require("apisix.core") local consumer_mod = require("apisix.consumer") -local resty_random = require("resty.random") local new_tab = require ("table.new") local auth_utils = require("apisix.utils.auth") local ngx_decode_base64 = ngx.decode_base64 -local ngx_encode_base64 = ngx.encode_base64 local ngx = ngx local sub_str = string.sub local table_insert = table.insert @@ -181,8 +179,7 @@ function _M.check_schema(conf, schema_type) 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 \"algorithm\" is \"HS256\" or \"HS512\"" + return false, "property \"secret\" is required when using HS based algorithms" end if conf.base64_secret then diff --git a/t/plugin/jwt-auth4.t b/t/plugin/jwt-auth4.t index b1e873f7d70d..d0d0d4149f8e 100644 --- a/t/plugin/jwt-auth4.t +++ b/t/plugin/jwt-auth4.t @@ -424,3 +424,38 @@ 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 From 2d37146ec53a5266ae1649317bb4688762707cbe Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Wed, 4 Feb 2026 10:14:51 +0545 Subject: [PATCH 09/11] rev comm Signed-off-by: Abhishek Choudhary --- apisix/plugins/jwt-auth/parser.lua | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/apisix/plugins/jwt-auth/parser.lua b/apisix/plugins/jwt-auth/parser.lua index 8ac82f3db1c5..303340f5605d 100644 --- a/apisix/plugins/jwt-auth/parser.lua +++ b/apisix/plugins/jwt-auth/parser.lua @@ -33,6 +33,11 @@ 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) @@ -210,7 +215,7 @@ local _M = {} function _M.new(token) local jwt_obj = jwt:load_jwt(token) - if not jwt_obj.valid then + if type(jwt_obj) == "table" and not jwt_obj.valid then return nil, jwt_obj.reason end return setmetatable(jwt_obj, {__index = _M}) @@ -219,21 +224,13 @@ 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.get_default_claims(self) - return { - "nbf", - "exp" - } + self.raw_payload, base64_decode(self.signature), key) end function _M.verify_claims(self, claims, conf) if not claims then - claims = self:get_default_claims() + claims = default_claims end for _, claim_name in ipairs(claims) do From 6480fc10b44415f2ed4cd3326bfe1e86b56ae395 Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Wed, 4 Feb 2026 10:15:23 +0545 Subject: [PATCH 10/11] f Signed-off-by: Abhishek Choudhary --- t/plugin/jwt-auth4.t | 1 - 1 file changed, 1 deletion(-) diff --git a/t/plugin/jwt-auth4.t b/t/plugin/jwt-auth4.t index d0d0d4149f8e..c2a4822dbe5a 100644 --- a/t/plugin/jwt-auth4.t +++ b/t/plugin/jwt-auth4.t @@ -427,7 +427,6 @@ GET /t - === TEST 11: secret is required for HS algorithms --- config location /t { From 7c429388fdaa8322f7d8b98530a3e699ad7176c0 Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Thu, 5 Feb 2026 09:32:52 +0545 Subject: [PATCH 11/11] fix Signed-off-by: Abhishek Choudhary --- t/plugin/jwt-auth-more-algo.t | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/t/plugin/jwt-auth-more-algo.t b/t/plugin/jwt-auth-more-algo.t index 9465d52680d8..3fb92d02c9de 100644 --- a/t/plugin/jwt-auth-more-algo.t +++ b/t/plugin/jwt-auth-more-algo.t @@ -128,7 +128,7 @@ passed content_by_lua_block { local t = require("lib.test_admin").test - local code, _, res = t('/hello?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTc2OTY5MzQ5OH0.1S9WJkzvmmKNRT2WnPHovSSoK6IZQhFJxtxksdfEWuDLimjdGkcYI-DIFtH65hOF', + local code, _, res = t('/hello?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MjA4NTA4Nzc5Mn0.6BNfYOnGvB27uY5LIwZFgIV_g42wiqLSlITtgAXinuZA9DNcquCTiudmbaXCHj20', ngx.HTTP_GET )