This is an automated email from the ASF dual-hosted git repository.

spacewander pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git


The following commit(s) were added to refs/heads/master by this push:
     new 0d4f65a  feat(vault): vault lua module, integration with jwt-auth 
authentication plugin (#5745)
0d4f65a is described below

commit 0d4f65a9ae06430388af609ca0dcad386225d616
Author: Bisakh <bisakhmonda...@gmail.com>
AuthorDate: Wed Dec 15 09:15:59 2021 +0530

    feat(vault): vault lua module, integration with jwt-auth authentication 
plugin (#5745)
---
 apisix/core/vault.lua               | 122 ++++++++++++
 apisix/plugins/jwt-auth.lua         | 166 +++++++++++++----
 ci/centos7-ci.sh                    |   3 +
 ci/common.sh                        |   6 +
 ci/linux-ci-init-service.sh         |   3 +
 ci/linux_openresty_common_runner.sh |   5 +-
 ci/pod/docker-compose.yml           |  18 ++
 conf/config-default.yaml            |  13 ++
 docs/en/latest/plugins/jwt-auth.md  |  89 ++++++++-
 t/certs/private.pem                 |  27 +++
 t/certs/public.pem                  |   9 +
 t/plugin/jwt-auth-vault.t           | 362 ++++++++++++++++++++++++++++++++++++
 12 files changed, 777 insertions(+), 46 deletions(-)

diff --git a/apisix/core/vault.lua b/apisix/core/vault.lua
new file mode 100644
index 0000000..f98926d
--- /dev/null
+++ b/apisix/core/vault.lua
@@ -0,0 +1,122 @@
+--
+-- 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 http = require("resty.http")
+local json = require("cjson")
+
+local fetch_local_conf = require("apisix.core.config_local").local_conf
+local norm_path = require("pl.path").normpath
+
+local _M = {}
+
+local function fetch_vault_conf()
+    local conf, err = fetch_local_conf()
+    if not conf then
+        return nil, "failed to fetch vault configuration from config yaml: " 
.. err
+    end
+
+    if not conf.vault then
+        return nil, "accessing vault data requires configuration information"
+    end
+    return conf.vault
+end
+
+
+local function make_request_to_vault(method, key, skip_prefix, data)
+    local vault, err = fetch_vault_conf()
+    if not vault then
+        return nil, err
+    end
+
+    local httpc = http.new()
+    -- config timeout or default to 5000 ms
+    httpc:set_timeout((vault.timeout or 5)*1000)
+
+    local req_addr = vault.host
+    if not skip_prefix then
+        req_addr = req_addr .. norm_path("/v1/"
+                .. vault.prefix .. "/" .. key)
+    else
+        req_addr = req_addr .. norm_path("/v1/" .. key)
+    end
+
+    local res, err = httpc:request_uri(req_addr, {
+        method = method,
+        headers = {
+            ["X-Vault-Token"] = vault.token
+        },
+        body = core.json.encode(data or  {}, true)
+    })
+    if not res then
+        return nil, err
+    end
+
+    return res.body
+end
+
+-- key is the vault kv engine path, joined with config yaml vault prefix.
+-- It takes an extra optional boolean param skip_prefix. If enabled, it simply 
doesn't use the
+-- prefix defined inside config yaml under vault config for fetching data.
+local function get(key, skip_prefix)
+    core.log.info("fetching data from vault for key: ", key)
+
+    local res, err = make_request_to_vault("GET", key, skip_prefix)
+    if not res or err then
+        return nil, "failed to retrtive data from vault kv engine " .. err
+    end
+
+    return json.decode(res)
+end
+
+_M.get = get
+
+-- key is the vault kv engine path, data is json key vaule pair.
+-- It takes an extra optional boolean param skip_prefix. If enabled, it simply 
doesn't use the
+-- prefix defined inside config yaml under vault config for storing data.
+local function set(key, data, skip_prefix)
+    core.log.info("stroing data into vault for key: ", key,
+                    "and value: ", core.json.delay_encode(data, true))
+
+    local res, err = make_request_to_vault("POST", key, skip_prefix, data)
+    if not res or err then
+        return nil, "failed to store data into vault kv engine " .. err
+    end
+
+    return true
+end
+_M.set = set
+
+
+-- key is the vault kv engine path, joined with config yaml vault prefix.
+-- It takes an extra optional boolean param skip_prefix. If enabled, it simply 
doesn't use the
+-- prefix defined inside config yaml under vault config for deleting data.
+local function delete(key, skip_prefix)
+    core.log.info("deleting data from vault for key: ", key)
+
+    local res, err = make_request_to_vault("DELETE", key, skip_prefix)
+
+    if not res or err then
+        return nil, "failed to delete data into vault kv engine " .. err
+    end
+
+    return true
+end
+
+_M.delete = delete
+
+return _M
diff --git a/apisix/plugins/jwt-auth.lua b/apisix/plugins/jwt-auth.lua
index cf3152a..bf52fa0 100644
--- a/apisix/plugins/jwt-auth.lua
+++ b/apisix/plugins/jwt-auth.lua
@@ -19,6 +19,7 @@ local jwt      = require("resty.jwt")
 local ck       = require("resty.cookie")
 local consumer_mod = require("apisix.consumer")
 local resty_random = require("resty.random")
+local vault        = require("apisix.core.vault")
 
 local ngx_encode_base64 = ngx.encode_base64
 local ngx_decode_base64 = ngx.decode_base64
@@ -54,6 +55,10 @@ local consumer_schema = {
         base64_secret = {
             type = "boolean",
             default = false
+        },
+        vault = {
+            type = "object",
+            properties = {}
         }
     },
     dependencies = {
@@ -76,7 +81,20 @@ local consumer_schema = {
                         },
                     },
                     required = {"public_key", "private_key"},
-                }
+                },
+                {
+                    properties = {
+                        vault = {
+                            type = "object",
+                            properties = {}
+                        },
+                        algorithm = {
+                            enum = {"RS256"},
+                        },
+                    },
+                    required = {"vault"},
+                },
+
             }
         }
     },
@@ -119,29 +137,34 @@ function _M.check_schema(conf, schema_type)
     if schema_type == core.schema.TYPE_CONSUMER then
         ok, err = core.schema.check(consumer_schema, conf)
     else
-        ok, err = core.schema.check(schema, conf)
+        return core.schema.check(schema, conf)
     end
 
     if not ok then
         return false, err
     end
 
-    if schema_type == core.schema.TYPE_CONSUMER then
-        if conf.algorithm ~= "RS256" 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
+    if conf.vault then
+        core.log.info("skipping jwt-auth schema validation with vault")
+        return true
+    end
+
+    if conf.algorithm ~= "RS256" 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 conf.algorithm == "RS256" 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
+    if conf.algorithm == "RS256" then
+        -- Possible options are a) both are in vault, b) both in schema
+        -- c) one in schema, another in vault.
+        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
 
@@ -175,12 +198,62 @@ local function fetch_jwt_token(ctx)
 end
 
 
-local function get_secret(conf)
+local function get_vault_path(username)
+    return "consumer/".. username .. "/jwt-auth"
+end
+
+
+local function get_secret(conf, consumer_name)
+    local secret = conf.secret
+    if conf.vault then
+        local res, err = vault.get(get_vault_path(consumer_name))
+        if not res or err then
+            return nil, err
+        end
+
+        if not res.data or not res.data.secret then
+            return nil, "secret could not found in vault: " .. 
core.json.encode(res)
+        end
+        secret = res.data.secret
+    end
+
     if conf.base64_secret then
-        return ngx_decode_base64(conf.secret)
+        return ngx_decode_base64(secret)
     end
 
-    return conf.secret
+    return secret
+end
+
+
+local function get_rsa_keypair(conf, consumer_name)
+    local public_key = conf.public_key
+    local private_key = conf.private_key
+    -- if keys are present in conf, no need to query vault (fallback)
+    if public_key and private_key then
+        return public_key, private_key
+    end
+
+    local vout = {}
+    if conf.vault then
+        local res, err = vault.get(get_vault_path(consumer_name))
+        if not res or err then
+            return nil, nil, err
+        end
+
+        if not res.data then
+            return nil, nil, "keypairs could not found in vault: " .. 
core.json.encode(res)
+        end
+        vout = res.data
+    end
+
+    if not public_key and not vout.public_key then
+        return nil, nil, "missing public key, not found in config/vault"
+    end
+    if not private_key and not vout.private_key then
+        return nil, nil, "missing private key, not found in config/vault"
+    end
+
+    return public_key or vout.public_key, private_key or vout.private_key
 end
 
 
@@ -197,16 +270,20 @@ local function get_real_payload(key, auth_conf, payload)
 end
 
 
-local function sign_jwt_with_HS(key, auth_conf, payload)
-    local auth_secret = get_secret(auth_conf)
+local function sign_jwt_with_HS(key, consumer, payload)
+    local auth_secret, err = get_secret(consumer.auth_conf, consumer.username)
+    if not auth_secret then
+        core.log.error("failed to sign jwt, err: ", err)
+        core.response.exit(503, "failed to sign jwt")
+    end
     local ok, jwt_token = pcall(jwt.sign, _M,
         auth_secret,
         {
             header = {
                 typ = "JWT",
-                alg = auth_conf.algorithm
+                alg = consumer.auth_conf.algorithm
             },
-            payload = get_real_payload(key, auth_conf, payload)
+            payload = get_real_payload(key, consumer.auth_conf, payload)
         }
     )
     if not ok then
@@ -217,18 +294,24 @@ local function sign_jwt_with_HS(key, auth_conf, payload)
 end
 
 
-local function sign_jwt_with_RS256(key, auth_conf, payload)
+local function sign_jwt_with_RS256(key, consumer, payload)
+    local public_key, private_key, err = get_rsa_keypair(consumer.auth_conf, 
consumer.username)
+    if not public_key then
+        core.log.error("failed to sign jwt, err: ", err)
+        core.response.exit(503, "failed to sign jwt")
+    end
+
     local ok, jwt_token = pcall(jwt.sign, _M,
-        auth_conf.private_key,
+        private_key,
         {
             header = {
                 typ = "JWT",
-                alg = auth_conf.algorithm,
+                alg = consumer.auth_conf.algorithm,
                 x5c = {
-                    auth_conf.public_key,
+                    public_key,
                 }
             },
-            payload = get_real_payload(key, auth_conf, payload)
+            payload = get_real_payload(key, consumer.auth_conf, payload)
         }
     )
     if not ok then
@@ -238,13 +321,22 @@ local function sign_jwt_with_RS256(key, auth_conf, 
payload)
     return jwt_token
 end
 
-
-local function algorithm_handler(consumer)
+-- 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
-        return sign_jwt_with_HS, get_secret(consumer.auth_conf)
+        if method_only then
+            return sign_jwt_with_HS
+        end
+
+        return get_secret(consumer.auth_conf, consumer.username)
     elseif consumer.auth_conf.algorithm == "RS256" then
-        return sign_jwt_with_RS256, consumer.auth_conf.public_key
+        if method_only then
+            return sign_jwt_with_RS256
+        end
+
+        local public_key, _, err = get_rsa_keypair(consumer.auth_conf, 
consumer.username)
+        return public_key, err
     end
 end
 
@@ -284,7 +376,11 @@ function _M.rewrite(conf, ctx)
     end
     core.log.info("consumer: ", core.json.delay_encode(consumer))
 
-    local _, auth_secret = algorithm_handler(consumer)
+    local auth_secret, err = algorithm_handler(consumer)
+    if not auth_secret then
+        core.log.error("failed to retrive secrets, err: ", err)
+        return 503, {message = "failed to verify jwt"}
+    end
     jwt_obj = jwt:verify_jwt_obj(auth_secret, jwt_obj)
     core.log.info("jwt object: ", core.json.delay_encode(jwt_obj))
 
@@ -325,8 +421,8 @@ local function gen_token()
 
     core.log.info("consumer: ", core.json.delay_encode(consumer))
 
-    local sign_handler, _ = algorithm_handler(consumer)
-    local jwt_token = sign_handler(key, consumer.auth_conf, payload)
+    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
diff --git a/ci/centos7-ci.sh b/ci/centos7-ci.sh
index f5a1799..c620417 100755
--- a/ci/centos7-ci.sh
+++ b/ci/centos7-ci.sh
@@ -40,6 +40,9 @@ install_dependencies() {
     cp ./etcd-v3.4.0-linux-amd64/etcdctl /usr/local/bin/
     rm -rf etcd-v3.4.0-linux-amd64
 
+    # install vault cli capabilities
+    install_vault_cli
+
     # install test::nginx
     yum install -y cpanminus perl
     cpanm --notest Test::Nginx IPC::Run > build.log 2>&1 || (cat build.log && 
exit 1)
diff --git a/ci/common.sh b/ci/common.sh
index f27583b..ec8b7e6 100644
--- a/ci/common.sh
+++ b/ci/common.sh
@@ -39,4 +39,10 @@ install_grpcurl () {
     tar -xvf grpcurl_${GRPCURL_VERSION}_linux_x86_64.tar.gz -C /usr/local/bin
 }
 
+install_vault_cli () {
+    VAULT_VERSION="1.9.0"
+    wget 
https://releases.hashicorp.com/vault/${VAULT_VERSION}/vault_${VAULT_VERSION}_linux_amd64.zip
+    unzip vault_${VAULT_VERSION}_linux_amd64.zip && mv ./vault /usr/local/bin
+}
+
 GRPC_SERVER_EXAMPLE_VER=20210819
diff --git a/ci/linux-ci-init-service.sh b/ci/linux-ci-init-service.sh
index 2939e82..765c115 100755
--- a/ci/linux-ci-init-service.sh
+++ b/ci/linux-ci-init-service.sh
@@ -32,6 +32,9 @@ docker exec -i rmqnamesrv 
/home/rocketmq/rocketmq-4.6.0/bin/mqadmin updateTopic
 docker exec -i rmqnamesrv /home/rocketmq/rocketmq-4.6.0/bin/mqadmin 
updateTopic -n rocketmq_namesrv:9876 -t test3 -c DefaultCluster
 docker exec -i rmqnamesrv /home/rocketmq/rocketmq-4.6.0/bin/mqadmin 
updateTopic -n rocketmq_namesrv:9876 -t test4 -c DefaultCluster
 
+# prepare vault kv engine
+docker exec -i vault sh -c "VAULT_TOKEN='root' 
VAULT_ADDR='http://0.0.0.0:8200' vault secrets enable -path=kv -version=1 kv"
+
 # prepare OPA env
 curl -XPUT 'http://localhost:8181/v1/policies/example' \
 --header 'Content-Type: text/plain' \
diff --git a/ci/linux_openresty_common_runner.sh 
b/ci/linux_openresty_common_runner.sh
index 7916d1f..98a9be2 100755
--- a/ci/linux_openresty_common_runner.sh
+++ b/ci/linux_openresty_common_runner.sh
@@ -54,8 +54,11 @@ do_install() {
     CGO_ENABLED=0 go build
     cd ../../
 
-    # installing grpcurl
+    # install grpcurl
     install_grpcurl
+
+    # install vault cli capabilities
+    install_vault_cli
 }
 
 script() {
diff --git a/ci/pod/docker-compose.yml b/ci/pod/docker-compose.yml
index 1055372..b632a59 100644
--- a/ci/pod/docker-compose.yml
+++ b/ci/pod/docker-compose.yml
@@ -136,6 +136,23 @@ services:
       consul_net:
 
 
+  ## HashiCorp Vault
+  vault:
+    image: vault:1.9.0
+    container_name: vault
+    restart: unless-stopped
+    ports:
+      - "8200:8200"
+    cap_add:
+      - IPC_LOCK
+    environment:
+      VAULT_DEV_ROOT_TOKEN_ID: root
+      VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8200
+    command: [ "vault", "server", "-dev" ]
+    networks:
+      vault_net:
+
+
   ## OpenLDAP
   openldap:
     image: bitnami/openldap:2.5.8
@@ -396,4 +413,5 @@ networks:
   nacos_net:
   skywalk_net:
   rocketmq_net:
+  vault_net:
   opa_net:
diff --git a/conf/config-default.yaml b/conf/config-default.yaml
index 0a9dec0..b53b32b 100644
--- a/conf/config-default.yaml
+++ b/conf/config-default.yaml
@@ -286,6 +286,19 @@ etcd:
                                   # the default value is true, e.g. the 
certificate will be verified strictly.
     #sni:                         # the SNI for etcd TLS requests. If missed, 
the host part of the URL will be used.
 
+# HashiCorp Vault storage backend for sensitive data retrieval. The config 
shows an example of what APISIX expects if you
+# wish to integrate Vault for secret (sensetive string, public private keys 
etc.) retrieval. APISIX communicates with Vault
+# server HTTP APIs. By default, APISIX doesn't need this configuration.
+# vault:
+#   host: "http://0.0.0.0:8200";   # The host address where the vault server is 
running.
+#   timeout: 10                   # request timeout 30 seconds
+#   token: root                   # Authentication token to access Vault HTTP 
APIs
+#   prefix: kv/apisix             # APISIX supports vault kv engine v1, where 
sensitive data are being stored
+                                  # and retrieved through vault HTTP APIs. 
enabling a prefix allows you to better enforcement of
+                                  # policies, generate limited scoped tokens 
and tightly control the data that can be accessed
+                                  # from APISIX.
+
+
 #discovery:                       # service discovery center
 #  dns:
 #    servers:
diff --git a/docs/en/latest/plugins/jwt-auth.md 
b/docs/en/latest/plugins/jwt-auth.md
index 1ed30ce..a5bad8b 100644
--- a/docs/en/latest/plugins/jwt-auth.md
+++ b/docs/en/latest/plugins/jwt-auth.md
@@ -23,14 +23,16 @@ title: jwt-auth
 
 ## Summary
 
-- [**Name**](#name)
-- [**Attributes**](#attributes)
-- [**API**](#api)
-- [**How To Enable**](#how-to-enable)
-- [**Test Plugin**](#test-plugin)
-    - [get the token in `jwt-auth` plugin:](#get-the-token-in-jwt-auth-plugin)
-    - [try request with token](#try-request-with-token)
-- [**Disable Plugin**](#disable-plugin)
+- [Summary](#summary)
+- [Name](#name)
+- [Attributes](#attributes)
+- [API](#api)
+- [How To Enable](#how-to-enable)
+  - [Enable jwt-auth with Vault 
Compatibility](#enable-jwt-auth-with-vault-compatibility)
+- [Test Plugin](#test-plugin)
+    - [Get the Token in `jwt-auth` Plugin:](#get-the-token-in-jwt-auth-plugin)
+    - [Try Request with Token](#try-request-with-token)
+- [Disable Plugin](#disable-plugin)
 
 ## Name
 
@@ -40,6 +42,8 @@ The `consumer` then adds its key to the query string 
parameter, request header,
 
 For more information on JWT, refer to [JWT](https://jwt.io/) for more 
information.
 
+`jwt-auth` plugin can be integrated with HashiCorp Vault for storing and 
fetching secrets, RSA key pairs from its encrypted kv engine. See the 
[examples](#enable-jwt-auth-with-vault-compatibility) below to have an overview 
of how things work.
+
 ## Attributes
 
 | Name          | Type    | Requirement | Default | Valid                      
 | Description                                                                  
                                                                    |
@@ -51,6 +55,9 @@ For more information on JWT, refer to [JWT](https://jwt.io/) 
for more informatio
 | algorithm     | string  | optional    | "HS256" | ["HS256", "HS512", 
"RS256"] | encryption algorithm.                                                
                                                                            |
 | exp           | integer | optional    | 86400   | [1,...]                    
 | token's expire time, in seconds                                              
                                                                    |
 | base64_secret | boolean | optional    | false   |                            
 | whether secret is base64 encoded                                             
                                                                    |
+| vault | object | optional    |    |                             | whether 
vault to be used for secret (secret for HS256/HS512  or public_key and 
private_key for RS256) storage and retrieval. The plugin by default uses the 
vault path as `kv/apisix/consumer/<consumer name>/jwt-auth` for secret 
retrieval. |
+
+**Note**: To enable vault integration, first visit the 
[config.yaml](https://github.com/apache/apisix/blob/master/conf/config.yaml) 
update it with your vault server configuration, host address and access token. 
You can take a look of what APISIX expects from the config.yaml at 
[config-default.yaml](https://github.com/apache/apisix/blob/master/conf/config-default.yaml)
 under the vault attributes.
 
 ## API
 
@@ -110,6 +117,68 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 
'X-API-KEY: edd1c9f034335f13
 }'
 ```
 
+### Enable jwt-auth with Vault Compatibility
+
+Sometimes, it's quite natural in production to have a centralized key 
management solution like vault where you don't have to update the APISIX 
consumer each time some part of your organization changes the signing secret 
key (secret for HS256/HS512 or public_key and private_key for RS256) and/or for 
privacy concerns you don't want to use the key through APISIX admin APIs. 
APISIX got you covered here. The `jwt-auth` is capable of referencing keys from 
vault.
+
+**Note**: For early version of this integration support, the plugin expects 
the key name of secrets stored into the vault path is among [ `secret`, 
`public_key`, `private_key` ] to successfully use the key. In future releases, 
we are going to add the support of referencing custom named keys.
+
+To enable vault compatibility, just add the empty vault object inside the 
jwt-auth plugin.
+
+1. You have stored HS256 signing secret inside vault and you want to use it 
for jwt signing and verification.
+
+```shell
+curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: 
edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
+{
+    "username": "jack",
+    "plugins": {
+        "jwt-auth": {
+            "key": "key-1",
+            "vault": {}
+        }
+    }
+}'
+```
+
+Here the plugin looks up for key `secret` inside vault path (`<vault.prefix 
from conf.yaml>/consumer/jack/jwt-auth`) for consumer username `jack` mentioned 
in the consumer config and uses it for subsequent signing and jwt verification. 
If the key is not found in the same path, the plugin logs error and fails to 
perform jwt authentication.
+
+2. RS256 rsa keypairs, both public and private keys are stored into vault.
+
+```shell
+curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: 
edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
+{
+    "username": "kowalski",
+    "plugins": {
+        "jwt-auth": {
+            "key": "rsa-keypair",
+            "algorithm": "RS256",
+            "vault": {}
+        }
+    }
+}'
+```
+
+The plugin looks up for `public_key` and `private_key` keys inside vault kv 
path (`<vault.prefix from conf.yaml>/consumer/kowalski/jwt-auth`) for username 
`kowalski` mentioned inside plugin vault configuration. If not found, 
authentication fails.
+
+3. public key in consumer configuration, while the private key is in vault.
+
+```shell
+curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: 
edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
+{
+    "username": "rico",
+    "plugins": {
+        "jwt-auth": {
+            "key": "user-key",
+            "algorithm": "RS256",
+            "public_key": "-----BEGIN PUBLIC KEY-----\n……\n-----END PUBLIC 
KEY-----"
+            "vault": {}
+        }
+    }
+}'
+```
+
+This plugin uses rsa public key from consumer configuration and uses the 
private key directly fetched from vault.
+
 You can use [APISIX Dashboard](https://github.com/apache/apisix-dashboard) to 
complete the above operations through the web console.
 
 1. Add a Consumer through the web console:
@@ -125,7 +194,7 @@ then add jwt-auth plugin in the Consumer page:
 
 ## Test Plugin
 
-#### get the token in `jwt-auth` plugin:
+#### Get the Token in `jwt-auth` Plugin:
 
 * without extension payload:
 
@@ -155,7 +224,7 @@ Server: APISIX/2.4
 
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1bmFtZSI6InRlc3QiLCJ1aWQiOjEwMDAwLCJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTYxOTA3MzgzOX0.jI9-Rpz1gc3u8Y6lZy8I43RXyCu0nSHANCvfn0YZUCY
 ```
 
-#### try request with token
+#### Try Request with Token
 
 * without token:
 
diff --git a/t/certs/private.pem b/t/certs/private.pem
new file mode 100644
index 0000000..76f0875
--- /dev/null
+++ b/t/certs/private.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEA79XYBopfnVMKxI533oU2VFQbEdSPtWRD+xSl73lHLVboGP1l
+SIZtnEj5AcTN2uDW6AYPiWL2iA3lEEsDTs7JBUXyl6pysBPfrqC8n/MOXKaD4e8U
+5GAHFiwHWg2WzHlfFSlFkLjzp0vPkDK+fQ4Clrd7shAyitB7use6DHcVCKuI4bFO
+oFbdI5sBGeyoD833g+ql9bRkH/vf8O+rPwHAM+47r1iv3lY3ex0P45PRd7U7rq8P
+8UIw6qOI1tiYuKlFJmjFdcwtYG0dctxWwgL1+7njrVQoWvuOTSsc9TDMhZkmmSsU
+3wXjaPxJpydck1C/w9ZLqsctKK5swYWhIcbcBQIDAQABAoIBADHXy1FwqHZVr8Mx
+qI/CN4xG/mkyN7uG3unrXKDsH3K4wPuQjeAIr/bu43EOqYl3eLI3sDrpKjsUSCqe
+rE1QhE5oPwZuEe+t8aqlFQ5YwP9YS8hEm57qpg5hkBWTBWfxQWVwclilV13JT5W0
+NgpfQwJ3l2lmHFrlARHMOEom5WQrewKvLh2YXeJBFQc0shHcjC2Pt7cjR9oAUVi6
+M5h6I+eB5xd9jj2a2fXaFL1SKZXEBVT6agSQqdB0tSuVTUsTBzNnuTL5ngS1wdLa
+lEdrw8klOYWrUihKJgYH7rnQrVEVNxGyO6fVs1S9CxMwu/nW2MPcbRBY0WKYCcAO
+QFJ4j4ECgYEA+yaEEPp/SH1E+DJi3U35pGdlHqg8yP0R7sik2cvvPUk4VbPrYVDD
+NQ8gt2H+06keycfRqJTPptS79db9LpKjG59yYP3aWj2YbGsH1H3XxA3sZiWHkNl0
+7i0ZE0GSCmEMbPe3C0Z3726tD9ZyVdaE5RdvRWdz1IloA+rYr3ypnH0CgYEA9Hdl
+KY8qSthtgWsTuthpExcvfppS3Dijgd23+oZJY2JLKf8/yctuBv6rBgqDCwpnUmGR
+tnkxPD/igaBnFtaMjDKNMwWwGHyarWkI7Zc+6HUdNcA/BkI3MCxwYQg2fr7HXY0h
+FalewOHeJz2Tldaue9DrVIO49jfLtBh2DYZFvCkCgYBV7OmGPY3KqUEtgV+dw43D
+l7Ra9shFI4A9J9xuv30MhL6HY9UGKHGA97oDw71BgT0NYBX1DWS1+VaNV46rnnO7
+gaPKV0+bTDOX9E5rftqRMwpMME7fWebNjhRkKCzk7CsqJN41N1jVTBJdtsrLX2d8
+UbY6EpjogFJb9L9J2ubUqQKBgQCk6oKJJbZfJV/CJaz6qBFCOqrkmlD5lQ/ghOUf
+EUYi0GVqYHH0vNJtz5EqEx9R7GPFNGLrGRi4z1QLJF1HD9dioJuWZujjq/NgtnG6
+bgSXJqJc52Lc4wB99AyfuL2ihSrTFmjSRx7Puc9241hTha7Rgh+vNOkq2HsH9FR3
+TTRv+QKBgG5ph+SFenSE7MgYXm2NRfG1k8bp86hrt9C8vHJ7DSO2Rr833RtqEiDJ
+nD4FbR0IObaBpS2VJdOn/jBYXCG0hFuj+Shxiyg/mZN0fwPVaRWDls7jzqqPsA+b
+x3XKRAn57LY8UbsNpOIqZ8kjVLPZhgfYwfOI3yAeSMv4ZnRY/MWe
+-----END RSA PRIVATE KEY-----
diff --git a/t/certs/public.pem b/t/certs/public.pem
new file mode 100644
index 0000000..f122f85
--- /dev/null
+++ b/t/certs/public.pem
@@ -0,0 +1,9 @@
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA79XYBopfnVMKxI533oU2
+VFQbEdSPtWRD+xSl73lHLVboGP1lSIZtnEj5AcTN2uDW6AYPiWL2iA3lEEsDTs7J
+BUXyl6pysBPfrqC8n/MOXKaD4e8U5GAHFiwHWg2WzHlfFSlFkLjzp0vPkDK+fQ4C
+lrd7shAyitB7use6DHcVCKuI4bFOoFbdI5sBGeyoD833g+ql9bRkH/vf8O+rPwHA
+M+47r1iv3lY3ex0P45PRd7U7rq8P8UIw6qOI1tiYuKlFJmjFdcwtYG0dctxWwgL1
++7njrVQoWvuOTSsc9TDMhZkmmSsU3wXjaPxJpydck1C/w9ZLqsctKK5swYWhIcbc
+BQIDAQAB
+-----END PUBLIC KEY-----
diff --git a/t/plugin/jwt-auth-vault.t b/t/plugin/jwt-auth-vault.t
new file mode 100644
index 0000000..c7d9e42
--- /dev/null
+++ b/t/plugin/jwt-auth-vault.t
@@ -0,0 +1,362 @@
+#
+# 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) = @_;
+
+    my $http_config = $block->http_config // <<_EOC_;
+
+    server {
+        listen 8777;
+
+        location /secure-endpoint {
+            content_by_lua_block {
+                ngx.say("successfully invoked secure endpoint")
+            }
+        }
+    }
+_EOC_
+
+    $block->set_value("http_config", $http_config);
+
+    my $vault_config = $block->extra_yaml_config // <<_EOC_;
+vault:
+  host: "http://0.0.0.0:8200";
+  timeout: 10
+  prefix: kv/apisix
+  token: root
+_EOC_
+
+    $block->set_value("extra_yaml_config", $vault_config);
+
+    if (!$block->request) {
+        $block->set_value("request", "GET /t");
+    }
+    if (!$block->no_error_log && !$block->error_log) {
+        $block->set_value("no_error_log", "[error]\n[alert]");
+    }
+});
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: schema check
+--- config
+    location /t {
+        content_by_lua_block {
+            local plugin = require("apisix.plugins.jwt-auth")
+            local core = require("apisix.core")
+            for _, conf in ipairs({
+                {
+                    -- public and private key are not provided for RS256, 
returns error
+                    key = "key-1",
+                    algorithm = "RS256"
+                },
+                {
+                    -- public and private key are not provided but vault 
config is enabled.
+                    key = "key-1",
+                    algorithm = "RS256",
+                    vault = {}
+                }
+            }) do
+                local ok, err = plugin.check_schema(conf, 
core.schema.TYPE_CONSUMER)
+                if not ok then
+                    ngx.say(err)
+                else
+                    ngx.say("ok")
+                end
+            end
+        }
+    }
+--- response_body
+failed to validate dependent schema for "algorithm": value should match only 
one schema, but matches none
+ok
+
+
+
+=== TEST 2: create a consumer with plugin and username
+--- 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": "key-hs256",
+                            "algorithm": "HS256",
+                            "vault":{}
+                        }
+                    }
+                }]]
+            )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 3: 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:8777": 1
+                        },
+                        "type": "roundrobin"
+                    },
+                    "uri": "/secure-endpoint"
+                }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 4: sign a jwt and access/verify /secure-endpoint, fails as no secret 
entry into vault
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, err, sign = t('/apisix/plugin/jwt/sign?key=key-hs256',
+                ngx.HTTP_GET
+            )
+
+            if code > 200 then
+                ngx.status = code
+                ngx.say(err)
+                return
+            end
+
+            local code, _, res = t('/secure-endpoint?jwt=' .. sign,
+                ngx.HTTP_GET
+            )
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.print(res)
+        }
+    }
+--- response_body
+failed to sign jwt
+--- error_code: 503
+--- error_log eval
+qr/failed to sign jwt, err: secret could not found in vault/
+--- grep_error_log_out
+failed to sign jwt, err: secret could not found in vault
+
+
+
+=== TEST 5: store HS256 secret into vault
+--- exec
+VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put 
kv/apisix/consumer/jack/jwt-auth secret=$3nsitiv3-c8d3
+--- response_body
+Success! Data written to: kv/apisix/consumer/jack/jwt-auth
+
+
+
+=== TEST 6: sign a HS256 jwt and access/verify /secure-endpoint
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, err, sign = t('/apisix/plugin/jwt/sign?key=key-hs256',
+                ngx.HTTP_GET
+            )
+
+            if code > 200 then
+                ngx.status = code
+                ngx.say(err)
+                return
+            end
+
+            local code, _, res = t('/secure-endpoint?jwt=' .. sign,
+                ngx.HTTP_GET
+            )
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.print(res)
+        }
+    }
+--- response_body
+successfully invoked secure endpoint
+
+
+
+=== TEST 7: store rsa key pairs into vault from local filesystem
+--- exec
+VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put 
kv/apisix/consumer/jim/jwt-auth public_key=@t/certs/public.pem 
private_key=@t/certs/private.pem
+--- response_body
+Success! Data written to: kv/apisix/consumer/jim/jwt-auth
+
+
+
+=== TEST 8: create consumer for RS256 algorithm with keypair fetched from vault
+--- 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": "jim",
+                    "plugins": {
+                        "jwt-auth": {
+                            "key": "rsa",
+                            "algorithm": "RS256",
+                            "vault":{}
+                        }
+                    }
+                }]]
+            )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 9: sign a jwt with with rsa keypair and access /secure-endpoint
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, err, sign = t('/apisix/plugin/jwt/sign?key=rsa',
+                ngx.HTTP_GET
+            )
+
+            if code > 200 then
+                ngx.status = code
+                ngx.say(err)
+                return
+            end
+
+            local code, _, res = t('/secure-endpoint?jwt=' .. sign,
+                ngx.HTTP_GET
+            )
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.print(res)
+        }
+    }
+--- response_body
+successfully invoked secure endpoint
+
+
+
+=== TEST 10: store rsa private key into vault from local filesystem
+--- exec
+VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put 
kv/apisix/consumer/john/jwt-auth private_key=@t/certs/private.pem
+--- response_body
+Success! Data written to: kv/apisix/consumer/john/jwt-auth
+
+
+
+=== TEST 11: create consumer for RS256 algorithm with private key fetched from 
vault and public key in consumer schema
+--- 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": "john",
+                    "plugins": {
+                        "jwt-auth": {
+                            "key": "rsa1",
+                            "algorithm": "RS256",
+                            "public_key": "-----BEGIN PUBLIC 
KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA79XYBopfnVMKxI533oU2\nVFQbEdSPtWRD+xSl73lHLVboGP1lSIZtnEj5AcTN2uDW6AYPiWL2iA3lEEsDTs7J\nBUXyl6pysBPfrqC8n/MOXKaD4e8U5GAHFiwHWg2WzHlfFSlFkLjzp0vPkDK+fQ4C\nlrd7shAyitB7use6DHcVCKuI4bFOoFbdI5sBGeyoD833g+ql9bRkH/vf8O+rPwHA\nM+47r1iv3lY3ex0P45PRd7U7rq8P8UIw6qOI1tiYuKlFJmjFdcwtYG0dctxWwgL1\n+7njrVQoWvuOTSsc9TDMhZkmmSsU3wXjaPxJpydck1C/w9ZLqsctKK5swYWhIcbc\nBQIDAQAB\n-----END
 PUBLIC [...]
+                            "vault":{}
+                        }
+                    }
+                }]]
+            )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 12: sign a jwt with with rsa keypair and access /secure-endpoint
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, err, sign = t('/apisix/plugin/jwt/sign?key=rsa1',
+                ngx.HTTP_GET
+            )
+
+            if code > 200 then
+                ngx.status = code
+                ngx.say(err)
+                return
+            end
+
+            local code, _, res = t('/secure-endpoint?jwt=' .. sign,
+                ngx.HTTP_GET
+            )
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.print(res)
+        }
+    }
+--- response_body
+successfully invoked secure endpoint

Reply via email to