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: Jwt-auth plugin no longer requires a private_key to be uploaded. #11597

Merged
merged 31 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
ef2067a
feat: remove /jwt/sign
dspo Sep 23, 2024
36ca4d0
Merge remote-tracking branch 'origin/master' into dspo/remove-jwt-sign
dspo Sep 23, 2024
cffae79
fix tests
dspo Sep 23, 2024
ed0f740
fix tests
dspo Sep 23, 2024
fdf7b09
typo
dspo Sep 23, 2024
b6492b8
fix tests index
dspo Sep 24, 2024
1fbeaef
reindex
dspo Sep 24, 2024
7d116ee
refactor document
dspo Sep 24, 2024
d4cefb1
jwt-auth plugin no longer need private_key
dspo Sep 24, 2024
7de6736
reindex
dspo Sep 24, 2024
36b5a36
fix unused variable
dspo Sep 24, 2024
c08f917
fix lint
dspo Sep 24, 2024
4398e9e
set default fields values in gen_token
dspo Sep 24, 2024
b399a55
fix tests
dspo Sep 24, 2024
f11dec5
fix test_http.py
dspo Sep 24, 2024
45002a3
remove 1 line comment
dspo Sep 25, 2024
544174b
refactor gen_token
dspo Sep 25, 2024
e1e1c3f
remove useless comment
dspo Sep 25, 2024
21bb26c
typo
dspo Sep 25, 2024
bccbeda
Update apisix/plugins/jwt-auth.lua
dspo Sep 25, 2024
5971c56
batch-requests plugin API as the public-api example
dspo Sep 25, 2024
9f0c3f3
remove redundant schema validation
dspo Sep 25, 2024
80a7c7f
remove code for test from jwt-auth.lua
dspo Sep 25, 2024
46cb2d3
gen_jwt_token locally in test_http.py
dspo Sep 25, 2024
fcc286b
update document: remove descriptions about private_key
dspo Sep 25, 2024
92cc273
lint doc
dspo Sep 25, 2024
9c9b0cf
Instead of using a local jwt library to generate jwt tokens, use a th…
dspo Sep 25, 2024
c558a03
merge origin/master
dspo Sep 27, 2024
fa8bf89
add test cases to test jwt-auth schema
dspo Sep 27, 2024
3ad1c3a
comment that why needs private_key in test cases
dspo Sep 27, 2024
ee706a1
comment that why needs payload in test cases
dspo Sep 27, 2024
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
174 changes: 9 additions & 165 deletions apisix/plugins/jwt-auth.lua
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,11 @@ local new_tab = require ("table.new")
local ngx_encode_base64 = ngx.encode_base64
local ngx_decode_base64 = ngx.decode_base64
local ngx = ngx
local ngx_time = ngx.time
local sub_str = string.sub
local table_insert = table.insert
local table_concat = table.concat
local ngx_re_gmatch = ngx.re.gmatch
local plugin_name = "jwt-auth"
local pcall = pcall


local schema = {
Expand Down Expand Up @@ -90,17 +88,16 @@ local consumer_schema = {
{
properties = {
public_key = {type = "string"},
private_key= {type = "string"},
algorithm = {
enum = {"RS256", "ES256"},
},
},
required = {"public_key", "private_key"},
required = {"public_key"},
nic-6443 marked this conversation as resolved.
Show resolved Hide resolved
},
}
}
},
encrypt_fields = {"secret", "private_key"},
encrypt_fields = {"secret"},
required = {"key"},
}

Expand Down Expand Up @@ -137,17 +134,6 @@ function _M.check_schema(conf, schema_type)
end
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
end

return true
end

Expand Down Expand Up @@ -230,106 +216,12 @@ local function get_secret(conf)
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,
exp = ngx_time() + auth_conf.exp
}
if payload then
local extra_payload = core.json.decode(payload)
core.table.merge(extra_payload, real_payload)
return extra_payload
end
return real_payload
end


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")
end
local ok, jwt_token = pcall(jwt.sign, _M,
auth_secret,
{
header = {
typ = "JWT",
alg = consumer.auth_conf.algorithm
},
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")
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")
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")
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
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
end

local public_key, _, err = get_rsa_or_ecdsa_keypair(consumer.auth_conf)
return public_key, err
local function get_auth_secret(auth_conf)
if not auth_conf.algorithm or auth_conf.algorithm == "HS256"
or auth_conf.algorithm == "HS512" then
return get_secret(auth_conf)
elseif auth_conf.algorithm == "RS256" or auth_conf.algorithm == "ES256" then
return auth_conf.public_key
end
end

Expand Down Expand Up @@ -366,7 +258,7 @@ function _M.rewrite(conf, ctx)
end
core.log.info("consumer: ", core.json.delay_encode(consumer))

local auth_secret, err = algorithm_handler(consumer)
local auth_secret, err = get_auth_secret(consumer.auth_conf)
if not auth_secret then
core.log.error("failed to retrieve secrets, err: ", err)
return 503, {message = "failed to verify jwt"}
Expand All @@ -387,52 +279,4 @@ function _M.rewrite(conf, ctx)
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)
end


function _M.api()
return {
{
methods = {"GET"},
uri = "/apisix/plugin/jwt/sign",
handler = gen_token,
}
}
end


return _M
13 changes: 7 additions & 6 deletions docs/en/latest/plugin-develop.md
Original file line number Diff line number Diff line change
Expand Up @@ -439,19 +439,20 @@ end

## register public API

A plugin can register API which exposes to the public. Take jwt-auth plugin as an example, this plugin registers `GET /apisix/plugin/jwt/sign` to allow client to sign its key:
A plugin can register API which exposes to the public. Take batch-requests plugin as an example, this plugin registers `POST /apisix/batch-requests` to allow developers to group multiple API requests into a single HTTP request/response cycle:

```lua
local function gen_token()
--...
function batch_requests()
-- ...
end

function _M.api()
-- ...
return {
{
methods = {"GET"},
uri = "/apisix/plugin/jwt/sign",
handler = gen_token,
methods = {"POST"},
uri = "/apisix/batch-requests",
handler = batch_requests,
}
}
end
Expand Down
64 changes: 7 additions & 57 deletions docs/en/latest/plugins/jwt-auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,12 @@ For Consumer:
| key | string | True | | | Unique key for a Consumer. |
| secret | string | False | | | The encryption key. If unspecified, auto generated in the background. This field supports saving the value in Secret Manager using the [APISIX Secret](../terminology/secret.md) resource. |
| public_key | string | True if `RS256` or `ES256` is set for the `algorithm` attribute. | | | RSA or ECDSA public key. This field supports saving the value in Secret Manager using the [APISIX Secret](../terminology/secret.md) resource. |
| private_key | string | True if `RS256` or `ES256` is set for the `algorithm` attribute. | | | RSA or ECDSA private key. This field supports saving the value in Secret Manager using the [APISIX Secret](../terminology/secret.md) resource. |
| algorithm | string | False | "HS256" | ["HS256", "HS512", "RS256", "ES256"] | Encryption algorithm. |
| 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. |

NOTE: `encrypt_fields = {"secret", "private_key"}` 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).
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).

For Route:

Expand All @@ -62,16 +61,6 @@ For Route:

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.

## API

This Plugin adds `/apisix/plugin/jwt/sign` as an endpoint.

:::note

You may need to use the [public-api](public-api.md) plugin to expose this endpoint.

:::

## Enable Plugin

To enable the Plugin, you have to create a Consumer object with the JWT token and configure your Route to use JWT authentication.
Expand Down Expand Up @@ -102,7 +91,7 @@ curl http://127.0.0.1:9180/apisix/admin/consumers -H "X-API-KEY: $admin_key" -X

:::note

The `jwt-auth` Plugin uses the HS256 algorithm by default. To use the RS256 algorithm, you can configure the public key and private key and specify the algorithm:
The `jwt-auth` Plugin uses the HS256 algorithm by default. To use the RS256 algorithm, you can configure the public key and specify the algorithm:

```shell
curl http://127.0.0.1:9180/apisix/admin/consumers -H "X-API-KEY: $admin_key" -X PUT -d '
Expand All @@ -112,7 +101,6 @@ curl http://127.0.0.1:9180/apisix/admin/consumers -H "X-API-KEY: $admin_key" -X
"jwt-auth": {
"key": "user-key",
"public_key": "-----BEGIN PUBLIC KEY-----\n……\n-----END PUBLIC KEY-----",
"private_key": "-----BEGIN RSA PRIVATE KEY-----\n……\n-----END RSA PRIVATE KEY-----",
"algorithm": "RS256"
}
}
Expand Down Expand Up @@ -148,53 +136,15 @@ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H "X-API-KEY: $admin_key" -X P

## Example usage

You need to first setup a Route for an API that signs the token using the [public-api](public-api.md) Plugin:

```shell
curl http://127.0.0.1:9180/apisix/admin/routes/jas -H "X-API-KEY: $admin_key" -X PUT -d '
{
"uri": "/apisix/plugin/jwt/sign",
"plugins": {
"public-api": {}
}
}'
```

Now, we can get a token:

- Without extension payload:
You need first to issue a JWT token using some tool such as [JWT.io's debugger](https://jwt.io/#debugger-io) or a programming language.

```shell
curl http://127.0.0.1:9080/apisix/plugin/jwt/sign?key=user-key -i
```

```
HTTP/1.1 200 OK
Date: Wed, 24 Jul 2019 10:33:31 GMT
Content-Type: text/plain
Transfer-Encoding: chunked
Connection: keep-alive
Server: APISIX web server

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTU2NDA1MDgxMX0.Us8zh_4VjJXF-TmR5f8cif8mBU7SuefPlpxhH0jbPVI
```

- With extension payload:
:::note

```shell
curl -G --data-urlencode 'payload={"uid":10000,"uname":"test"}' http://127.0.0.1:9080/apisix/plugin/jwt/sign?key=user-key -i
```
When you are issuing a JWT token, you have to update the payload with `key` matching the credential key you would like to use; and `exp` or `nbf` in UNIX timestamp.

```
HTTP/1.1 200 OK
Date: Wed, 21 Apr 2021 06:43:59 GMT
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Server: APISIX/2.4
e.g. payload=`{"key": "user-key", "exp": 1727274983}`

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1bmFtZSI6InRlc3QiLCJ1aWQiOjEwMDAwLCJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTYxOTA3MzgzOX0.jI9-Rpz1gc3u8Y6lZy8I43RXyCu0nSHANCvfn0YZUCY
```
:::

You can now use this token while making requests:

Expand Down
Loading
Loading