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 18 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
223 changes: 79 additions & 144 deletions apisix/plugins/jwt-auth.lua
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,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 @@ -138,14 +137,9 @@ function _M.check_schema(conf, schema_type)
end

if conf.algorithm == "RS256" or conf.algorithm == "ES256" then
nic-6443 marked this conversation as resolved.
Show resolved Hide resolved
-- 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
Expand Down Expand Up @@ -230,106 +224,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 +266,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 +287,87 @@ 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
local function get_real_payload(key, exp, payload)
local real_payload = {
key = key,
exp = ngx_time() + exp
}
if payload then
payload = ngx.unescape_uri(payload)
local extra_payload = core.json.decode(payload)
core.table.merge(extra_payload, real_payload)
return extra_payload
end
return real_payload
end

local consumer_conf = consumer_mod.plugin(plugin_name)
if not consumer_conf then
return core.response.exit(404)
local function sign_jwt_with_HS(key, auth_conf, payload)
nic-6443 marked this conversation as resolved.
Show resolved Hide resolved
local auth_secret, err = get_secret(auth_conf)
if not auth_secret then
core.log.error("failed to sign jwt, err: ", err)
return nil, "failed to sign jwt: failed to get auth_secret"
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)
local ok, jwt_token = pcall(jwt.sign, _M,
auth_secret,
{
header = {
typ = "JWT",
alg = auth_conf.algorithm
},
payload = get_real_payload(key, auth_conf.exp, payload)
}
)
if not ok then
core.log.error("failed to sign jwt, err: ", jwt_token.reason)
return nil, "failed to sign jwt"
end
return jwt_token
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)
local function sign_jwt_with_RS256_ES256(key, auth_conf, payload)
nic-6443 marked this conversation as resolved.
Show resolved Hide resolved
local ok, jwt_token = pcall(jwt.sign, _M,
auth_conf.private_key,
{
header = {
typ = "JWT",
alg = consumer.auth_conf.algorithm,
x5c = {
auth_conf.public_key,
}
},
payload = get_real_payload(key, auth_conf.exp, payload)
}
)
if not ok then
core.log.warn("failed to sign jwt, err: ", jwt_token.reason)
return nil, "failed to sign jwt"
end

return core.response.exit(404)
return jwt_token
end

local function get_sign_handler(algorithm)
if not algorithm or algorithm == "HS256" or algorithm == "HS512" then
return sign_jwt_with_HS
elseif algorithm == "RS256" or algorithm == "ES256" then
return sign_jwt_with_RS256_ES256
end
end

function _M.api()
return {
{
methods = {"GET"},
uri = "/apisix/plugin/jwt/sign",
handler = gen_token,
}
}
local function gen_token(auth_conf, payload)
if not auth_conf.exp then
auth_conf.exp = 86400
end
if not auth_conf.lifetime_grace_period then
auth_conf.lifetime_grace_period = 0
end
if not auth_conf.algorithm then
auth_conf.algorithm = "HS256"
end
local sign_handler = get_sign_handler(auth_conf.algorithm)
local jwt_token, err = sign_handler(auth_conf.key, auth_conf, payload)
return jwt_token, err
end

_M.gen_token = gen_token
dspo marked this conversation as resolved.
Show resolved Hide resolved


return _M
15 changes: 8 additions & 7 deletions docs/en/latest/plugin-develop.md
Original file line number Diff line number Diff line change
Expand Up @@ -439,20 +439,21 @@ 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 wolf-rbac plugin as an example, this plugin registers `POST /apisix/plugin/wolf-rbac/login` to allow a client to login and get the wolf rbac_token:
nic-6443 marked this conversation as resolved.
Show resolved Hide resolved

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

function _M.api()
return {
{
methods = {"GET"},
uri = "/apisix/plugin/jwt/sign",
handler = gen_token,
}
methods = {"POST"},
uri = "/apisix/plugin/wolf-rbac/login",
handler = wolf_rbac_login,
},
-- ...
}
end
```
Expand Down
56 changes: 1 addition & 55 deletions docs/en/latest/plugins/jwt-auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,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
Expand Down Expand Up @@ -148,53 +140,7 @@ 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:

```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:

```shell
curl -G --data-urlencode 'payload={"uid":10000,"uname":"test"}' http://127.0.0.1:9080/apisix/plugin/jwt/sign?key=user-key -i
```

```
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

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1bmFtZSI6InRlc3QiLCJ1aWQiOjEwMDAwLCJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTYxOTA3MzgzOX0.jI9-Rpz1gc3u8Y6lZy8I43RXyCu0nSHANCvfn0YZUCY
```
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.

You can now use this token while making requests:

Expand Down
Loading
Loading