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 4c32609bff35..55866b1d802d 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 000000000000..303340f5605d --- /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 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) 资源。 diff --git a/t/plugin/jwt-auth-more-algo.t b/t/plugin/jwt-auth-more-algo.t new file mode 100644 index 000000000000..3fb92d02c9de --- /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=' .. "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 { diff --git a/t/plugin/jwt-auth4.t b/t/plugin/jwt-auth4.t index b1e873f7d70d..c2a4822dbe5a 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