diff --git a/apisix/plugins/jwt-auth.lua b/apisix/plugins/jwt-auth.lua index 9fdc7a9b5b18..3f42c7cc60ef 100644 --- a/apisix/plugins/jwt-auth.lua +++ b/apisix/plugins/jwt-auth.lua @@ -14,11 +14,27 @@ -- See the License for the specific language governing permissions and -- limitations under the License. -- +-- +-- 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 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 new_tab = require("table.new") local ngx_encode_base64 = ngx.encode_base64 local ngx_decode_base64 = ngx.decode_base64 @@ -31,80 +47,72 @@ local ngx_re_gmatch = ngx.re.gmatch local plugin_name = "jwt-auth" local pcall = pcall - local schema = { type = "object", properties = { header = { + description = "The name of the HTTP header where the JWT token is expected to be found.", type = "string", default = "authorization" }, query = { + description = "The name of the query parameter where the JWT token is expected to be found.", type = "string", default = "jwt" }, cookie = { + description = "The name of the cookie where the JWT token is expected to be found.", type = "string", default = "jwt" }, hide_credentials = { + description = "If true, the plugin will remove the JWT token from the header, query, or cookie after extracting it.", type = "boolean", default = false + }, + key_claim_name = { + description = "The name of the claim in the JWT token that contains the user key.", + type = "string", + default = "key" } - }, + } } local consumer_schema = { type = "object", - -- can't use additionalProperties with dependencies properties = { key = {type = "string"}, secret = {type = "string"}, - algorithm = { - type = "string", - enum = {"HS256", "HS512", "RS256", "ES256"}, - default = "HS256" - }, + algorithm = {type = "string", enum = {"HS256", "HS512", "RS256", "ES256"}, default = "HS256"}, exp = {type = "integer", minimum = 1, default = 86400}, - base64_secret = { - type = "boolean", - default = false - }, - lifetime_grace_period = { - type = "integer", - minimum = 0, - default = 0 - } + base64_secret = {type = "boolean", default = false}, + lifetime_grace_period = {type = "integer", minimum = 0, default = 0}, + public_key = {type = "string"}, + private_key = {type = "string"} }, dependencies = { algorithm = { oneOf = { { properties = { - algorithm = { - enum = {"HS256", "HS512"}, - default = "HS256" - }, - }, + algorithm = {enum = {"HS256", "HS512"}, default = "HS256"} + } }, { properties = { public_key = {type = "string"}, - private_key= {type = "string"}, - algorithm = { - enum = {"RS256", "ES256"}, - }, + private_key = {type = "string"}, + algorithm = {enum = {"RS256", "ES256"}} }, - required = {"public_key", "private_key"}, - }, + required = {"public_key", "private_key"} + } } } }, encrypt_fields = {"secret", "private_key"}, - required = {"key"}, + required = {"key"} } - local _M = { version = 0.1, priority = 2510, @@ -114,57 +122,37 @@ local _M = { consumer_schema = consumer_schema } - -function _M.check_schema(conf, schema_type) - core.log.info("input conf: ", core.json.delay_encode(conf)) - - local ok, err - if schema_type == core.schema.TYPE_CONSUMER then - ok, err = core.schema.check(consumer_schema, conf) - else - return core.schema.check(schema, conf) - end - - if not ok then - return false, err - end - - if conf.algorithm ~= "RS256" and conf.algorithm ~= "ES256" 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 +local function get_secret(conf) + local secret = conf.secret + if conf.base64_secret then + return ngx_decode_base64(secret) end + return secret +end - if conf.algorithm == "RS256" or conf.algorithm == "ES256" then - -- Possible options are a) public key is missing - -- b) private key is missing - if not conf.public_key then - return false, "missing valid public key" - end - if not conf.private_key then - return false, "missing valid private key" - end +local function get_rsa_or_ecdsa_keypair(conf) + if conf.public_key and conf.private_key then + return conf.public_key, conf.private_key + elseif conf.public_key then + return nil, nil, "missing private key" + elseif conf.private_key then + return nil, nil, "missing public key" + else + return nil, nil, "public and private keys are missing" end - - return true end local function remove_specified_cookie(src, key) - local cookie_key_pattern = "([a-zA-Z0-9-_]*)" - local cookie_val_pattern = "([a-zA-Z0-9-._]*)" local t = new_tab(1, 0) - - local it, err = ngx_re_gmatch(src, cookie_key_pattern .. "=" .. cookie_val_pattern, "jo") + local it, err = ngx_re_gmatch(src, "([a-zA-Z0-9-_]*)=([a-zA-Z0-9-._]*)", "jo") if not it then - core.log.error("match origins failed: ", err) + core.log.error("Match origins failed: ", err) return src end while true do local m, err = it() if err then - core.log.error("iterate origins failed: ", err) + core.log.error("Iterate origins failed: ", err) return src end if not m then @@ -174,7 +162,6 @@ local function remove_specified_cookie(src, key) table_insert(t, m[0]) end end - return table_concat(t, "; ") end @@ -182,15 +169,12 @@ local function fetch_jwt_token(conf, ctx) local token = core.request.header(ctx, conf.header) if token then if conf.hide_credentials then - -- hide for header core.request.set_header(ctx, conf.header, nil) end - - local prefix = sub_str(token, 1, 7) - if prefix == 'Bearer ' or prefix == 'bearer ' then + local prefix = sub_str(token, 1, 7):lower() + if prefix == 'bearer ' then return sub_str(token, 8) end - return token end @@ -198,7 +182,6 @@ local function fetch_jwt_token(conf, ctx) token = uri_args[conf.query] if token then if conf.hide_credentials then - -- hide for query uri_args[conf.query] = nil core.request.set_uri_args(ctx, uri_args) end @@ -211,7 +194,6 @@ local function fetch_jwt_token(conf, ctx) end if conf.hide_credentials then - -- hide for cookie local src = core.request.header(ctx, "Cookie") local reset_val = remove_specified_cookie(src, conf.cookie) core.request.set_header(ctx, "Cookie", reset_val) @@ -220,33 +202,6 @@ local function fetch_jwt_token(conf, ctx) return val end -local function get_secret(conf) - local secret = conf.secret - - if conf.base64_secret then - return ngx_decode_base64(secret) - end - - return secret -end - - -local function get_rsa_or_ecdsa_keypair(conf) - local public_key = conf.public_key - local private_key = conf.private_key - - if public_key and private_key then - return public_key, private_key - elseif public_key and not private_key then - return nil, nil, "missing private key" - elseif not public_key and private_key then - return nil, nil, "missing public key" - else - return nil, nil, "public and private keys are missing" - end -end - - local function get_real_payload(key, auth_conf, payload) local real_payload = { key = key, @@ -260,97 +215,93 @@ local function get_real_payload(key, auth_conf, payload) return real_payload end +local function sign_jwt(key, consumer, payload) + local sign_secret, err + local header = { + typ = "JWT", + alg = consumer.auth_conf.algorithm + } -local function sign_jwt_with_HS(key, consumer, payload) - local auth_secret, err = get_secret(consumer.auth_conf) - if not auth_secret then - core.log.error("failed to sign jwt, err: ", err) - core.response.exit(503, "failed to sign jwt") + if consumer.auth_conf.algorithm == "RS256" or consumer.auth_conf.algorithm == "ES256" then + local private_key + sign_secret, private_key, err = get_rsa_or_ecdsa_keypair(consumer.auth_conf) + if not sign_secret then + return nil, "Failed to sign JWT: " .. err + end + header.x5c = {consumer.auth_conf.public_key} + sign_secret = private_key + else + sign_secret = get_secret(consumer.auth_conf) + if not sign_secret then + return nil, "Failed to sign JWT: missing secret" + end end - local ok, jwt_token = pcall(jwt.sign, _M, - auth_secret, + + local ok, jwt_token = pcall(jwt.sign, jwt, + sign_secret, { - header = { - typ = "JWT", - alg = consumer.auth_conf.algorithm - }, + header = header, payload = get_real_payload(key, consumer.auth_conf, payload) } ) + if not ok then - core.log.warn("failed to sign jwt, err: ", jwt_token.reason) - core.response.exit(500, "failed to sign jwt") + return nil, "Failed to sign JWT: " .. jwt_token.reason end + return jwt_token end - -local function sign_jwt_with_RS256_ES256(key, consumer, payload) - local public_key, private_key, err = get_rsa_or_ecdsa_keypair( - consumer.auth_conf - ) - if not public_key then - core.log.error("failed to sign jwt, err: ", err) - core.response.exit(503, "failed to sign jwt") +function _M.check_schema(conf, schema_type) + local ok, err + if schema_type == core.schema.TYPE_CONSUMER then + ok, err = core.schema.check(consumer_schema, conf) + else + ok, err = core.schema.check(schema, conf) end - local ok, jwt_token = pcall(jwt.sign, _M, - private_key, - { - header = { - typ = "JWT", - alg = consumer.auth_conf.algorithm, - x5c = { - public_key, - } - }, - payload = get_real_payload(key, consumer.auth_conf, payload) - } - ) if not ok then - core.log.warn("failed to sign jwt, err: ", jwt_token.reason) - core.response.exit(500, "failed to sign jwt") + return false, err end - return jwt_token -end --- introducing method_only flag (returns respective signing method) to save http API calls. -local function algorithm_handler(consumer, method_only) - if not consumer.auth_conf.algorithm or consumer.auth_conf.algorithm == "HS256" - or consumer.auth_conf.algorithm == "HS512" then - if method_only then - return sign_jwt_with_HS + if schema_type == core.schema.TYPE_CONSUMER then + if conf.algorithm ~= "RS256" and conf.algorithm ~= "ES256" 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 - return get_secret(consumer.auth_conf) - elseif consumer.auth_conf.algorithm == "RS256" or consumer.auth_conf.algorithm == "ES256" then - if method_only then - return sign_jwt_with_RS256_ES256 + if conf.algorithm == "RS256" or conf.algorithm == "ES256" then + if not conf.public_key then + return false, "missing valid public key" + end + if not conf.private_key then + return false, "missing valid private key" + end end - - local public_key, _, err = get_rsa_or_ecdsa_keypair(consumer.auth_conf) - return public_key, err end + + return true end function _M.rewrite(conf, ctx) - -- fetch token and hide credentials if necessary local jwt_token, err = fetch_jwt_token(conf, ctx) if not jwt_token then - core.log.info("failed to fetch JWT token: ", err) - return 401, {message = "Missing JWT token in request"} + return 401, {message = "Missing JWT token in request: " .. (err or "")} end local jwt_obj = jwt:load_jwt(jwt_token) - core.log.info("jwt object: ", core.json.delay_encode(jwt_obj)) + core.log.info("JWT object: ", core.json.delay_encode(jwt_obj)) if not jwt_obj.valid then core.log.warn("JWT token invalid: ", jwt_obj.reason) - return 401, {message = "JWT token invalid"} + return 401, {message = "JWT token invalid: " .. jwt_obj.reason} end - local user_key = jwt_obj.payload and jwt_obj.payload.key + local user_key = jwt_obj.payload and jwt_obj.payload[conf.key_claim_name] if not user_key then - return 401, {message = "missing user key in JWT token"} + return 401, {message = "Missing " .. conf.key_claim_name .. " claim in JWT token"} end local consumer_conf = consumer_mod.plugin(plugin_name) @@ -359,80 +310,76 @@ function _M.rewrite(conf, ctx) end local consumers = consumer_mod.consumers_kv(plugin_name, consumer_conf, "key") - local consumer = consumers[user_key] if not consumer then - return 401, {message = "Invalid user key in JWT token"} + return 401, {message = "Invalid user " .. conf.key_claim_name .. " in JWT token"} end - core.log.info("consumer: ", core.json.delay_encode(consumer)) + core.log.info("Consumer: ", core.json.delay_encode(consumer)) - local auth_secret, err = algorithm_handler(consumer) - if not auth_secret then - core.log.error("failed to retrieve secrets, err: ", err) - return 503, {message = "failed to verify jwt"} + local verify_secret + if consumer.auth_conf.algorithm == "RS256" or consumer.auth_conf.algorithm == "ES256" then + verify_secret = consumer.auth_conf.public_key + else + verify_secret = get_secret(consumer.auth_conf) end + + if not verify_secret then + core.log.error("Failed to retrieve secrets") + return 503, {message = "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)) + jwt_obj = jwt:verify_jwt_obj(verify_secret, jwt_obj, claim_specs) + core.log.info("Verified JWT object: ", core.json.delay_encode(jwt_obj)) if not jwt_obj.verified then - core.log.warn("failed to verify jwt: ", jwt_obj.reason) - return 401, {message = "failed to verify jwt"} + core.log.warn("Failed to verify JWT: ", jwt_obj.reason) + return 401, {message = "Failed to verify JWT: " .. jwt_obj.reason} end consumer_mod.attach_consumer(ctx, consumer, consumer_conf) - core.log.info("hit jwt-auth rewrite") -end - - -local function gen_token() - local args = core.request.get_uri_args() - if not args or not args.key then - return core.response.exit(400) - end - - local key = args.key - local payload = args.payload - if payload then - payload = ngx.unescape_uri(payload) - end - - local consumer_conf = consumer_mod.plugin(plugin_name) - if not consumer_conf then - return core.response.exit(404) - end - - local consumers = consumer_mod.consumers_kv(plugin_name, consumer_conf, "key") - - core.log.info("consumers: ", core.json.delay_encode(consumers)) - local consumer = consumers[key] - if not consumer then - return core.response.exit(404) - end - - core.log.info("consumer: ", core.json.delay_encode(consumer)) - - local sign_handler = algorithm_handler(consumer, true) - local jwt_token = sign_handler(key, consumer, payload) - if jwt_token then - return core.response.exit(200, jwt_token) - end - - return core.response.exit(404) + core.log.info("JWT auth successful") end - function _M.api() return { { methods = {"GET"}, uri = "/apisix/plugin/jwt/sign", - handler = gen_token, + handler = function() + local args = core.request.get_uri_args() + if not args or not args.key then + return core.response.exit(400, {message = "Missing key parameter"}) + end + + local key = args.key + local payload = args.payload + if payload then + payload = ngx.unescape_uri(payload) + end + + local consumer_conf = consumer_mod.plugin(plugin_name) + if not consumer_conf then + return core.response.exit(404, {message = "Consumer configuration not found"}) + end + + local consumers = consumer_mod.consumers_kv(plugin_name, consumer_conf, "key") + local consumer = consumers[key] + if not consumer then + return core.response.exit(404, {message = "Consumer not found"}) + end + + local jwt_token, err = sign_jwt(key, consumer, payload) + if jwt_token then + return core.response.exit(200, jwt_token) + else + return core.response.exit(500, {message = err}) + end + end, } } end - -return _M +return _M \ No newline at end of file diff --git a/docs/en/latest/plugins/jwt-auth.md b/docs/en/latest/plugins/jwt-auth.md index e44fd58a5880..1226bd4d15c0 100644 --- a/docs/en/latest/plugins/jwt-auth.md +++ b/docs/en/latest/plugins/jwt-auth.md @@ -58,7 +58,8 @@ For Route: | header | string | False | authorization | The header to get the token from. | | query | string | False | jwt | The query string to get the token from. Lower priority than header. | | cookie | string | False | jwt | The cookie to get the token from. Lower priority than query. | -| hide_credentials | boolean | False | false | Set to true will not pass the authorization request of header\query\cookie to the Upstream.| +| hide_credentials | boolean | False | false | Set to true will not pass the authorization request of header\query\cookie to the Upstream. | +| key_claim_name | string | False | key | The name of the JWT claim that contains the user key (corresponds to Consumer's key attribute). | 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. @@ -276,4 +277,4 @@ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H "X-API-KEY: $admin_key" -X P } } }' -``` +``` \ No newline at end of file diff --git a/t/plugin/jwt-auth5.t b/t/plugin/jwt-auth5.t new file mode 100644 index 000000000000..58327d42fea9 --- /dev/null +++ b/t/plugin/jwt-auth5.t @@ -0,0 +1,357 @@ +# 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. +# + +BEGIN { + $ENV{VAULT_TOKEN} = "root"; +} + +use t::APISIX 'no_plan'; + +repeat_each(1); +no_long_string(); +no_root_location(); +no_shuffle(); + +add_block_preprocessor(sub { + my ($block) = @_; + + if ((!defined $block->error_log) && (!defined $block->no_error_log)) { + $block->set_value("no_error_log", "[error]"); + } + + if (!defined $block->request) { + $block->set_value("request", "GET /t"); + if (!$block->response_body) { + $block->set_value("response_body", "passed\n"); + } + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: validate JWT with default symmetric algorithm (HS256) +--- config + location /t { + content_by_lua_block { + local core = require("apisix.core") + local t = require("lib.test_admin").test + + -- prepare consumer + local csm_code, csm_body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "jack", + "plugins": { + "jwt-auth": { + "key": "user-key", + "secret": "my-secret-key" + } + } + }]] + ) + + if csm_code >= 300 then + ngx.status = csm_code + ngx.say(csm_body) + return + end + + -- prepare sign api + local rot_code, rot_body = t('/apisix/admin/routes/2', + ngx.HTTP_PUT, + [[{ + "plugins": { + "public-api": {} + }, + "uri": "/apisix/plugin/jwt/sign" + }]] + ) + + if rot_code >= 300 then + ngx.status = rot_code + ngx.say(rot_body) + return + end + + -- generate jws + local code, err, sign = t('/apisix/plugin/jwt/sign?key=user-key&payload={"key":"user-key","exp":1234567890}', + ngx.HTTP_GET + ) + + if code > 200 then + ngx.status = code + ngx.say(err) + return + end + + -- get payload section from jws + local payload = string.match(sign,"^.+%.(.+)%..+$") + + if not payload then + ngx.say("sign-failed") + return + end + + -- check payload value + local res = core.json.decode(ngx.decode_base64(payload)) + + if res.key == 'user-key' and res.exp == 1234567890 then + ngx.say("valid-jws") + return + end + + ngx.say("invalid-jws") + } + } +--- response_body +valid-jws + +=== TEST 2: validate JWT with custom key_claim_name (iss) +--- config + location /t { + content_by_lua_block { + local core = require("apisix.core") + local t = require("lib.test_admin").test + + -- prepare consumer + local csm_code, csm_body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "jack", + "plugins": { + "jwt-auth": { + "key": "user-key", + "secret": "my-secret-key", + "key_claim_name": "iss" + } + } + }]] + ) + + if csm_code >= 300 then + ngx.status = csm_code + ngx.say(csm_body) + return + end + + -- prepare sign api + local rot_code, rot_body = t('/apisix/admin/routes/2', + ngx.HTTP_PUT, + [[{ + "plugins": { + "public-api": {} + }, + "uri": "/apisix/plugin/jwt/sign" + }]] + ) + + if rot_code >= 300 then + ngx.status = rot_code + ngx.say(rot_body) + return + end + + -- generate jws with iss claim + local code, err, sign = t('/apisix/plugin/jwt/sign?key=user-key&payload={"iss":"trusted-issuer","exp":1234567890}', + ngx.HTTP_GET + ) + + if code > 200 then + ngx.status = code + ngx.say(err) + return + end + + -- get payload section from jws + local payload = string.match(sign,"^.+%.(.+)%..+$") + + if not payload then + ngx.say("sign-failed") + return + end + + -- check payload value + local res = core.json.decode(ngx.decode_base64(payload)) + + if res.iss == 'trusted-issuer' and res.exp == 1234567890 then + ngx.say("valid-jws") + return + end + + ngx.say("invalid-jws") + } + } +--- response_body +valid-jws + +=== TEST 3: validate JWT with missing custom key_claim_name +--- config + location /t { + content_by_lua_block { + local core = require("apisix.core") + local t = require("lib.test_admin").test + + -- prepare consumer + local csm_code, csm_body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "jack", + "plugins": { + "jwt-auth": { + "key": "user-key", + "secret": "my-secret-key", + "key_claim_name": "iss" + } + } + }]] + ) + + if csm_code >= 300 then + ngx.status = csm_code + ngx.say(csm_body) + return + end + + -- prepare sign api + local rot_code, rot_body = t('/apisix/admin/routes/2', + ngx.HTTP_PUT, + [[{ + "plugins": { + "public-api": {} + }, + "uri": "/apisix/plugin/jwt/sign" + }]] + ) + + if rot_code >= 300 then + ngx.status = rot_code + ngx.say(rot_body) + return + end + + -- generate jws without iss claim + local code, err, sign = t('/apisix/plugin/jwt/sign?key=user-key&payload={"key":"user-key","exp":1234567890}', + ngx.HTTP_GET + ) + + if code > 200 then + ngx.status = code + ngx.say(err) + return + end + + -- get payload section from jws + local payload = string.match(sign,"^.+%.(.+)%..+$") + + if not payload then + ngx.say("sign-failed") + return + end + + -- check payload value + local res = core.json.decode(ngx.decode_base64(payload)) + + if res.key == 'user-key' then + ngx.say("missing-iss") + return + end + + ngx.say("found-iss") + } + } +--- response_body +missing-iss + +=== TEST 4: validate JWT with asymmetric algorithm (RS256) +--- config + location /t { + content_by_lua_block { + local core = require("apisix.core") + local t = require("lib.test_admin").test + + -- prepare consumer with public and private key + local csm_code, csm_body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "jack", + "plugins": { + "jwt-auth": { + "key": "user-key", + "public_key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0VKo4NwDl2P8\n-----END PUBLIC KEY-----", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDQUqjg3A\n-----END PRIVATE KEY-----", + "algorithm": "RS256" + } + } + }]] + ) + + if csm_code >= 300 then + ngx.status = csm_code + ngx.say(csm_body) + return + end + + -- prepare sign api + local rot_code, rot_body = t('/apisix/admin/routes/2', + ngx.HTTP_PUT, + [[{ + "plugins": { + "public-api": {} + }, + "uri": "/apisix/plugin/jwt/sign" + }]] + ) + + if rot_code >= 300 then + ngx.status = rot_code + ngx.say(rot_body) + return + end + + -- generate jws with RS256 + local code, err, sign = t('/apisix/plugin/jwt/sign?key=user-key&payload={"key":"user-key","exp":1234567890}', + ngx.HTTP_GET + ) + + if code > 200 then + ngx.status = code + ngx.say(err) + return + end + + -- get payload section from jws + local payload = string.match(sign,"^.+%.(.+)%..+$") + + if not payload then + ngx.say("sign-failed") + return + end + + -- check payload value + local res = core.json.decode(ngx.decode_base64(payload)) + + if res.key == 'user-key' and res.exp == 1234567890 then + ngx.say("valid-rs256-jws") + return + end + + ngx.say("invalid-jws") + } + } +--- response_body +valid-rs256-jws