Skip to content

Commit

Permalink
feat: Jwt-auth plugin no longer requires a private_key to be uploaded. (
Browse files Browse the repository at this point in the history
  • Loading branch information
dspo authored Sep 27, 2024
1 parent 0e97e91 commit 1773655
Show file tree
Hide file tree
Showing 16 changed files with 498 additions and 1,035 deletions.
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"},
},
}
}
},
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

0 comments on commit 1773655

Please sign in to comment.