Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(vault): vault lua module, integration with jwt-auth authentication plugin #5745

Merged
merged 28 commits into from
Dec 15, 2021
Merged
Changes from 23 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b2788ff
vault-auth init
bisakhmondal Dec 3, 2021
a88c615
vault storage kv engine integration
bisakhmondal Dec 6, 2021
17b3c23
not required file
bisakhmondal Dec 6, 2021
7605c38
integrating vault storage backend with jwt-auth authentication plugin
bisakhmondal Dec 8, 2021
ae240ed
Merge branch 'master' into vault-jwt
bisakhmondal Dec 9, 2021
876cce3
Merge branch 'master' into vault-jwt
bisakhmondal Dec 9, 2021
c3a7d4a
openssl rsa-2048 pem public private keypairs
bisakhmondal Dec 9, 2021
ed628b2
vault integration tests with corner cases
bisakhmondal Dec 9, 2021
9ec682a
minor updates
bisakhmondal Dec 9, 2021
36f0141
adding real vault server into CIs
bisakhmondal Dec 9, 2021
c3aaf8f
lint fix
bisakhmondal Dec 9, 2021
80358b9
suggestions
bisakhmondal Dec 9, 2021
e4d10da
now get doesnot returns vault data
bisakhmondal Dec 9, 2021
f927fb9
update exposed port address
bisakhmondal Dec 9, 2021
ee251aa
documentation
bisakhmondal Dec 9, 2021
6158837
blank commit
bisakhmondal Dec 9, 2021
f9cdc4e
remove custom path support from mvp
bisakhmondal Dec 10, 2021
6729106
trimming down validation and key generation if vault config is enabled
bisakhmondal Dec 10, 2021
83b3fe0
remove redundant codes
bisakhmondal Dec 10, 2021
58292d2
Ci fix
bisakhmondal Dec 10, 2021
55c105d
changing vault kv suffix to /consumer/<username>/jwt-auth
bisakhmondal Dec 10, 2021
1f2ff22
update tests and modify the way http status code were sent
bisakhmondal Dec 10, 2021
cac28d1
fix doc broken link
bisakhmondal Dec 10, 2021
f78cf89
comment out vault config in yaml and update tests accordingly
bisakhmondal Dec 12, 2021
6a28225
Merge branch 'master' into vault-jwt
bisakhmondal Dec 13, 2021
2d44654
change yaml_config to extra_yaml_config
bisakhmondal Dec 13, 2021
66ee305
single extra yaml config
bisakhmondal Dec 13, 2021
a56ed8e
suggestion
bisakhmondal Dec 14, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions apisix/core/vault.lua
Original file line number Diff line number Diff line change
@@ -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
166 changes: 131 additions & 35 deletions apisix/plugins/jwt-auth.lua
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions ci/centos7-ci.sh
Original file line number Diff line number Diff line change
@@ -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)
6 changes: 6 additions & 0 deletions ci/common.sh
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions ci/linux-ci-init-service.sh
Original file line number Diff line number Diff line change
@@ -31,3 +31,5 @@ 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 test2 -c DefaultCluster
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

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"
5 changes: 4 additions & 1 deletion ci/linux_openresty_common_runner.sh
Original file line number Diff line number Diff line change
@@ -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() {
18 changes: 18 additions & 0 deletions ci/pod/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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
@@ -386,3 +403,4 @@ networks:
nacos_net:
skywalk_net:
rocketmq_net:
vault_net:
13 changes: 13 additions & 0 deletions conf/config-default.yaml
Original file line number Diff line number Diff line change
@@ -281,6 +281,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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's comment out this section, we do not need to require vault by default.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Err. Actually, I mean like this :

#vault:
  # ...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, I get it now 😄

Copy link
Member Author

@bisakhmondal bisakhmondal Dec 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated. Thanks for the review

host: "http://0.0.0.0:8200" # The host address where the vault server is running.
timeout: 10 # request timeout 30 seconds
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.

token: root # Authentication token to access Vault HTTP APIs

#discovery: # service discovery center
# dns:
# servers:
89 changes: 79 additions & 10 deletions docs/en/latest/plugins/jwt-auth.md
Original file line number Diff line number Diff line change
@@ -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.

1. 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:

27 changes: 27 additions & 0 deletions t/certs/private.pem
Original file line number Diff line number Diff line change
@@ -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-----
9 changes: 9 additions & 0 deletions t/certs/public.pem
Original file line number Diff line number Diff line change
@@ -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-----
368 changes: 368 additions & 0 deletions t/plugin/jwt-auth-vault.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,368 @@
#
# 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);

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":{}
}
}
}]],
[[{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we remove the expected response? We don't need to check this output anymore. More other tests already do it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. Done

"node": {
"value": {
"username": "jack",
"plugins": {
"jwt-auth": {
"key": "key-hs256",
"algorithm": "HS256",
"vault":{}
}
}
}
},
"action": "set"
}]]
)
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: true
--- grep_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 KEY-----\n",
"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