Skip to content

Commit 1773655

Browse files
authored
feat: Jwt-auth plugin no longer requires a private_key to be uploaded. (#11597)
1 parent 0e97e91 commit 1773655

File tree

16 files changed

+498
-1035
lines changed

16 files changed

+498
-1035
lines changed

apisix/plugins/jwt-auth.lua

Lines changed: 9 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,11 @@ local new_tab = require ("table.new")
2323
local ngx_encode_base64 = ngx.encode_base64
2424
local ngx_decode_base64 = ngx.decode_base64
2525
local ngx = ngx
26-
local ngx_time = ngx.time
2726
local sub_str = string.sub
2827
local table_insert = table.insert
2928
local table_concat = table.concat
3029
local ngx_re_gmatch = ngx.re.gmatch
3130
local plugin_name = "jwt-auth"
32-
local pcall = pcall
3331

3432

3533
local schema = {
@@ -90,17 +88,16 @@ local consumer_schema = {
9088
{
9189
properties = {
9290
public_key = {type = "string"},
93-
private_key= {type = "string"},
9491
algorithm = {
9592
enum = {"RS256", "ES256"},
9693
},
9794
},
98-
required = {"public_key", "private_key"},
95+
required = {"public_key"},
9996
},
10097
}
10198
}
10299
},
103-
encrypt_fields = {"secret", "private_key"},
100+
encrypt_fields = {"secret"},
104101
required = {"key"},
105102
}
106103

@@ -137,17 +134,6 @@ function _M.check_schema(conf, schema_type)
137134
end
138135
end
139136

140-
if conf.algorithm == "RS256" or conf.algorithm == "ES256" then
141-
-- Possible options are a) public key is missing
142-
-- b) private key is missing
143-
if not conf.public_key then
144-
return false, "missing valid public key"
145-
end
146-
if not conf.private_key then
147-
return false, "missing valid private key"
148-
end
149-
end
150-
151137
return true
152138
end
153139

@@ -230,106 +216,12 @@ local function get_secret(conf)
230216
return secret
231217
end
232218

233-
234-
local function get_rsa_or_ecdsa_keypair(conf)
235-
local public_key = conf.public_key
236-
local private_key = conf.private_key
237-
238-
if public_key and private_key then
239-
return public_key, private_key
240-
elseif public_key and not private_key then
241-
return nil, nil, "missing private key"
242-
elseif not public_key and private_key then
243-
return nil, nil, "missing public key"
244-
else
245-
return nil, nil, "public and private keys are missing"
246-
end
247-
end
248-
249-
250-
local function get_real_payload(key, auth_conf, payload)
251-
local real_payload = {
252-
key = key,
253-
exp = ngx_time() + auth_conf.exp
254-
}
255-
if payload then
256-
local extra_payload = core.json.decode(payload)
257-
core.table.merge(extra_payload, real_payload)
258-
return extra_payload
259-
end
260-
return real_payload
261-
end
262-
263-
264-
local function sign_jwt_with_HS(key, consumer, payload)
265-
local auth_secret, err = get_secret(consumer.auth_conf)
266-
if not auth_secret then
267-
core.log.error("failed to sign jwt, err: ", err)
268-
core.response.exit(503, "failed to sign jwt")
269-
end
270-
local ok, jwt_token = pcall(jwt.sign, _M,
271-
auth_secret,
272-
{
273-
header = {
274-
typ = "JWT",
275-
alg = consumer.auth_conf.algorithm
276-
},
277-
payload = get_real_payload(key, consumer.auth_conf, payload)
278-
}
279-
)
280-
if not ok then
281-
core.log.warn("failed to sign jwt, err: ", jwt_token.reason)
282-
core.response.exit(500, "failed to sign jwt")
283-
end
284-
return jwt_token
285-
end
286-
287-
288-
local function sign_jwt_with_RS256_ES256(key, consumer, payload)
289-
local public_key, private_key, err = get_rsa_or_ecdsa_keypair(
290-
consumer.auth_conf
291-
)
292-
if not public_key then
293-
core.log.error("failed to sign jwt, err: ", err)
294-
core.response.exit(503, "failed to sign jwt")
295-
end
296-
297-
local ok, jwt_token = pcall(jwt.sign, _M,
298-
private_key,
299-
{
300-
header = {
301-
typ = "JWT",
302-
alg = consumer.auth_conf.algorithm,
303-
x5c = {
304-
public_key,
305-
}
306-
},
307-
payload = get_real_payload(key, consumer.auth_conf, payload)
308-
}
309-
)
310-
if not ok then
311-
core.log.warn("failed to sign jwt, err: ", jwt_token.reason)
312-
core.response.exit(500, "failed to sign jwt")
313-
end
314-
return jwt_token
315-
end
316-
317-
-- introducing method_only flag (returns respective signing method) to save http API calls.
318-
local function algorithm_handler(consumer, method_only)
319-
if not consumer.auth_conf.algorithm or consumer.auth_conf.algorithm == "HS256"
320-
or consumer.auth_conf.algorithm == "HS512" then
321-
if method_only then
322-
return sign_jwt_with_HS
323-
end
324-
325-
return get_secret(consumer.auth_conf)
326-
elseif consumer.auth_conf.algorithm == "RS256" or consumer.auth_conf.algorithm == "ES256" then
327-
if method_only then
328-
return sign_jwt_with_RS256_ES256
329-
end
330-
331-
local public_key, _, err = get_rsa_or_ecdsa_keypair(consumer.auth_conf)
332-
return public_key, err
219+
local function get_auth_secret(auth_conf)
220+
if not auth_conf.algorithm or auth_conf.algorithm == "HS256"
221+
or auth_conf.algorithm == "HS512" then
222+
return get_secret(auth_conf)
223+
elseif auth_conf.algorithm == "RS256" or auth_conf.algorithm == "ES256" then
224+
return auth_conf.public_key
333225
end
334226
end
335227

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

369-
local auth_secret, err = algorithm_handler(consumer)
261+
local auth_secret, err = get_auth_secret(consumer.auth_conf)
370262
if not auth_secret then
371263
core.log.error("failed to retrieve secrets, err: ", err)
372264
return 503, {message = "failed to verify jwt"}
@@ -387,52 +279,4 @@ function _M.rewrite(conf, ctx)
387279
end
388280

389281

390-
local function gen_token()
391-
local args = core.request.get_uri_args()
392-
if not args or not args.key then
393-
return core.response.exit(400)
394-
end
395-
396-
local key = args.key
397-
local payload = args.payload
398-
if payload then
399-
payload = ngx.unescape_uri(payload)
400-
end
401-
402-
local consumer_conf = consumer_mod.plugin(plugin_name)
403-
if not consumer_conf then
404-
return core.response.exit(404)
405-
end
406-
407-
local consumers = consumer_mod.consumers_kv(plugin_name, consumer_conf, "key")
408-
409-
core.log.info("consumers: ", core.json.delay_encode(consumers))
410-
local consumer = consumers[key]
411-
if not consumer then
412-
return core.response.exit(404)
413-
end
414-
415-
core.log.info("consumer: ", core.json.delay_encode(consumer))
416-
417-
local sign_handler = algorithm_handler(consumer, true)
418-
local jwt_token = sign_handler(key, consumer, payload)
419-
if jwt_token then
420-
return core.response.exit(200, jwt_token)
421-
end
422-
423-
return core.response.exit(404)
424-
end
425-
426-
427-
function _M.api()
428-
return {
429-
{
430-
methods = {"GET"},
431-
uri = "/apisix/plugin/jwt/sign",
432-
handler = gen_token,
433-
}
434-
}
435-
end
436-
437-
438282
return _M

docs/en/latest/plugin-develop.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -439,19 +439,20 @@ end
439439

440440
## register public API
441441

442-
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:
442+
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:
443443

444444
```lua
445-
local function gen_token()
446-
--...
445+
function batch_requests()
446+
-- ...
447447
end
448448
449449
function _M.api()
450+
-- ...
450451
return {
451452
{
452-
methods = {"GET"},
453-
uri = "/apisix/plugin/jwt/sign",
454-
handler = gen_token,
453+
methods = {"POST"},
454+
uri = "/apisix/batch-requests",
455+
handler = batch_requests,
455456
}
456457
}
457458
end

docs/en/latest/plugins/jwt-auth.md

Lines changed: 7 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,12 @@ For Consumer:
4343
| key | string | True | | | Unique key for a Consumer. |
4444
| 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. |
4545
| 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. |
46-
| 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. |
4746
| algorithm | string | False | "HS256" | ["HS256", "HS512", "RS256", "ES256"] | Encryption algorithm. |
4847
| exp | integer | False | 86400 | [1,...] | Expiry time of the token in seconds. |
4948
| base64_secret | boolean | False | false | | Set to true if the secret is base64 encoded. |
5049
| 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. |
5150

52-
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).
51+
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).
5352

5453
For Route:
5554

@@ -62,16 +61,6 @@ For Route:
6261

6362
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.
6463

65-
## API
66-
67-
This Plugin adds `/apisix/plugin/jwt/sign` as an endpoint.
68-
69-
:::note
70-
71-
You may need to use the [public-api](public-api.md) plugin to expose this endpoint.
72-
73-
:::
74-
7564
## Enable Plugin
7665

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

10392
:::note
10493

105-
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:
94+
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:
10695

10796
```shell
10897
curl http://127.0.0.1:9180/apisix/admin/consumers -H "X-API-KEY: $admin_key" -X PUT -d '
@@ -112,7 +101,6 @@ curl http://127.0.0.1:9180/apisix/admin/consumers -H "X-API-KEY: $admin_key" -X
112101
"jwt-auth": {
113102
"key": "user-key",
114103
"public_key": "-----BEGIN PUBLIC KEY-----\n……\n-----END PUBLIC KEY-----",
115-
"private_key": "-----BEGIN RSA PRIVATE KEY-----\n……\n-----END RSA PRIVATE KEY-----",
116104
"algorithm": "RS256"
117105
}
118106
}
@@ -148,53 +136,15 @@ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H "X-API-KEY: $admin_key" -X P
148136

149137
## Example usage
150138

151-
You need to first setup a Route for an API that signs the token using the [public-api](public-api.md) Plugin:
152-
153-
```shell
154-
curl http://127.0.0.1:9180/apisix/admin/routes/jas -H "X-API-KEY: $admin_key" -X PUT -d '
155-
{
156-
"uri": "/apisix/plugin/jwt/sign",
157-
"plugins": {
158-
"public-api": {}
159-
}
160-
}'
161-
```
162-
163-
Now, we can get a token:
164-
165-
- Without extension payload:
139+
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.
166140

167-
```shell
168-
curl http://127.0.0.1:9080/apisix/plugin/jwt/sign?key=user-key -i
169-
```
170-
171-
```
172-
HTTP/1.1 200 OK
173-
Date: Wed, 24 Jul 2019 10:33:31 GMT
174-
Content-Type: text/plain
175-
Transfer-Encoding: chunked
176-
Connection: keep-alive
177-
Server: APISIX web server
178-
179-
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTU2NDA1MDgxMX0.Us8zh_4VjJXF-TmR5f8cif8mBU7SuefPlpxhH0jbPVI
180-
```
181-
182-
- With extension payload:
141+
:::note
183142

184-
```shell
185-
curl -G --data-urlencode 'payload={"uid":10000,"uname":"test"}' http://127.0.0.1:9080/apisix/plugin/jwt/sign?key=user-key -i
186-
```
143+
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.
187144

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

196-
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1bmFtZSI6InRlc3QiLCJ1aWQiOjEwMDAwLCJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTYxOTA3MzgzOX0.jI9-Rpz1gc3u8Y6lZy8I43RXyCu0nSHANCvfn0YZUCY
197-
```
147+
:::
198148

199149
You can now use this token while making requests:
200150

0 commit comments

Comments
 (0)