From 528ec8971d05bee0449e58198ac3a454ff9b2028 Mon Sep 17 00:00:00 2001 From: Shreemaan Abhishek Date: Wed, 27 Nov 2024 13:09:53 +0545 Subject: [PATCH] feat(jwt-auth): support configuring `key_claim_name` (#11772) --- apisix/plugins/jwt-auth.lua | 10 +- ci/common.sh | 4 + docs/en/latest/plugins/jwt-auth.md | 1 + t/plugin/jwt-auth4.t | 162 +++++++++++++++++++++++++++++ 4 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 t/plugin/jwt-auth4.t diff --git a/apisix/plugins/jwt-auth.lua b/apisix/plugins/jwt-auth.lua index 740efcdc6b4e..87f4214990d6 100644 --- a/apisix/plugins/jwt-auth.lua +++ b/apisix/plugins/jwt-auth.lua @@ -48,7 +48,12 @@ local schema = { hide_credentials = { type = "boolean", default = false - } + }, + key_claim_name = { + type = "string", + default = "key", + minLength = 1, + }, }, } @@ -240,7 +245,8 @@ function _M.rewrite(conf, ctx) return 401, {message = "JWT token invalid"} end - local user_key = jwt_obj.payload and jwt_obj.payload.key + local key_claim_name = conf.key_claim_name + local user_key = jwt_obj.payload and jwt_obj.payload[key_claim_name] if not user_key then return 401, {message = "missing user key in JWT token"} end diff --git a/ci/common.sh b/ci/common.sh index ae5d12b2b7c6..8c8a40435e86 100644 --- a/ci/common.sh +++ b/ci/common.sh @@ -33,6 +33,10 @@ create_lua_deps() { echo "Create lua deps" make deps + + # just for jwt-auth test + luarocks install lua-resty-openssl --tree deps + # maybe reopen this feature later # luarocks install luacov-coveralls --tree=deps --local > build.log 2>&1 || (cat build.log && exit 1) # for github action cache diff --git a/docs/en/latest/plugins/jwt-auth.md b/docs/en/latest/plugins/jwt-auth.md index a3522efe730a..1f8f470927ed 100644 --- a/docs/en/latest/plugins/jwt-auth.md +++ b/docs/en/latest/plugins/jwt-auth.md @@ -47,6 +47,7 @@ For Consumer: | 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,...] | Define the leeway in seconds to account for clock skew between the server that generated the jwt and the server validating it. Value should be zero (0) or a positive integer. | +| key_claim_name | string | False | key | | The name of the JWT claim that contains the user key (corresponds to Consumer's key attribute). | NOTE: `encrypt_fields = {"secret"}` is also defined in the schema, which means that the field will be stored encrypted in etcd. See [encrypted storage fields](../plugin-develop.md#encrypted-storage-fields). diff --git a/t/plugin/jwt-auth4.t b/t/plugin/jwt-auth4.t new file mode 100644 index 000000000000..075fbb85f01f --- /dev/null +++ b/t/plugin/jwt-auth4.t @@ -0,0 +1,162 @@ +# +# 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(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: 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" + } + } + }]] + ) + + 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": { + "key": "user-key", + "secret": "my-secret-key", + "key_claim_name": "iss" + } + }, + "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: verify (in header) +--- config + location /t { + content_by_lua_block { + local function gen_token(payload) + local buffer = require "string.buffer" + local openssl_mac = require "resty.openssl.mac" + + local base64 = require "ngx.base64" + local base64_encode = base64.encode_base64url + + local json = require("cjson") + + local function sign(data, key) + return openssl_mac.new(key, "HMAC", nil, "sha256"):final(data) + end + local header = { typ = "JWT", alg = "HS256" } + local buf = buffer.new() + + buf:put(base64_encode(json.encode(header))):put("."):put(base64_encode(json.encode(payload))) + + local ok, signature = pcall(sign, buf:tostring(), "my-secret-key") + if not ok then + return nil, signature + end + + buf:put("."):put(base64_encode(signature)) + + return buf:get() + end + + local payload = { + sub = "1234567890", + iss = "user-key", + exp = 9916239022 + } + + local token = gen_token(payload) + + local http = require("resty.http") + local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello" + local opt = {method = "POST", headers = {["Authorization"] = "Bearer " .. token}} + local httpc = http.new() + local res = httpc:request_uri(uri, opt) + assert(res.status == 200) + + ngx.print(res.body) + } + } +--- request +GET /t +--- more_headers +--- response_body +hello world