You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by sp...@apache.org on 2022/07/25 06:55:28 UTC

[apisix] branch master updated: feat(jwt-auth): support setting clock skew for verifying (#7500)

This is an automated email from the ASF dual-hosted git repository.

spacewander pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git


The following commit(s) were added to refs/heads/master by this push:
     new a2144b5eb feat(jwt-auth): support setting clock skew for verifying (#7500)
a2144b5eb is described below

commit a2144b5ebd9a6a10855d6f01dee40f21913885a3
Author: tzssangglass <tz...@gmail.com>
AuthorDate: Mon Jul 25 14:55:20 2022 +0800

    feat(jwt-auth): support setting clock skew for verifying (#7500)
---
 apisix/plugins/jwt-auth.lua        |  10 +-
 docs/en/latest/plugins/jwt-auth.md |   1 +
 docs/zh/latest/plugins/jwt-auth.md |   1 +
 t/plugin/jwt-auth.t                |   4 +-
 t/plugin/jwt-auth2.t               | 319 +++++++++++++++++++++++++++++++++++++
 5 files changed, 332 insertions(+), 3 deletions(-)

diff --git a/apisix/plugins/jwt-auth.lua b/apisix/plugins/jwt-auth.lua
index 82c12c95b..3cd601778 100644
--- a/apisix/plugins/jwt-auth.lua
+++ b/apisix/plugins/jwt-auth.lua
@@ -71,6 +71,11 @@ local consumer_schema = {
         vault = {
             type = "object",
             properties = {}
+        },
+        lifetime_grace_period = {
+            type = "integer",
+            minimum = 0,
+            default = 0
         }
     },
     dependencies = {
@@ -389,7 +394,10 @@ function _M.rewrite(conf, ctx)
         core.log.error("failed to retrieve secrets, err: ", err)
         return 503, {message = "failed to verify jwt"}
     end
-    jwt_obj = jwt:verify_jwt_obj(auth_secret, jwt_obj)
+    local claim_specs = jwt:get_default_validation_options(jwt_obj)
+    claim_specs.lifetime_grace_period = consumer.auth_conf.lifetime_grace_period
+
+    jwt_obj = jwt:verify_jwt_obj(auth_secret, jwt_obj, claim_specs)
     core.log.info("jwt object: ", core.json.delay_encode(jwt_obj))
 
     if not jwt_obj.verified then
diff --git a/docs/en/latest/plugins/jwt-auth.md b/docs/en/latest/plugins/jwt-auth.md
index f6591a199..2019d1a6b 100644
--- a/docs/en/latest/plugins/jwt-auth.md
+++ b/docs/en/latest/plugins/jwt-auth.md
@@ -49,6 +49,7 @@ For Consumer:
 | 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.                                                                                                                                                |
 | vault         | object  | False                                                 |         |                             | Set to true to use Vault for storing and retrieving secret (secret for HS256/HS512  or public_key and private_key for RS256). By default, the Vault path is `kv/apisix/consumer/<consumer_name>/jwt-auth`. |
+| 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. |
 
 :::info IMPORTANT
 
diff --git a/docs/zh/latest/plugins/jwt-auth.md b/docs/zh/latest/plugins/jwt-auth.md
index cefb574cf..b8828ed90 100644
--- a/docs/zh/latest/plugins/jwt-auth.md
+++ b/docs/zh/latest/plugins/jwt-auth.md
@@ -49,6 +49,7 @@ Consumer 端:
 | exp           | integer | 否    | 86400   | [1,...]                     | token 的超时时间。                                                                                              |
 | base64_secret | boolean | 否    | false   |                             | 当设置为 `true` 时,密钥为 base64 编码。                                                                                         |
 | vault         | object  | 否    |         |                             | 是否使用 Vault 作为存储和检索密钥(HS256/HS512 的密钥或 RS256 的公钥和私钥)的方式。该插件默认使用 `kv/apisix/consumer/<consumer name>/jwt-auth` 路径进行密钥检索。 |
+| lifetime_grace_period | integer | 否    | 0  | [0,...]                  | 定义生成 JWT 的服务器和验证 JWT 的服务器之间的时钟偏移。该值应该是零(0)或一个正整数。 |
 
 :::info IMPORTANT
 
diff --git a/t/plugin/jwt-auth.t b/t/plugin/jwt-auth.t
index 197a42594..bfee5cf0d 100644
--- a/t/plugin/jwt-auth.t
+++ b/t/plugin/jwt-auth.t
@@ -54,7 +54,7 @@ __DATA__
         }
     }
 --- response_body_like eval
-qr/{"algorithm":"HS256","base64_secret":false,"exp":86400,"key":"123","secret":"[a-zA-Z0-9+\\\/]+={0,2}"}/
+qr/{"algorithm":"HS256","base64_secret":false,"exp":86400,"key":"123","lifetime_grace_period":0,"secret":"[a-zA-Z0-9+\\\/]+={0,2}"}/
 
 
 
@@ -957,7 +957,7 @@ qr/failed to sign jwt/
         }
     }
 --- response_body_like eval
-qr/{"algorithm":"HS512","base64_secret":false,"exp":86400,"key":"123","secret":"[a-zA-Z0-9+\\\/]+={0,2}"}/
+qr/{"algorithm":"HS512","base64_secret":false,"exp":86400,"key":"123","lifetime_grace_period":0,"secret":"[a-zA-Z0-9+\\\/]+={0,2}"}/
 
 
 
diff --git a/t/plugin/jwt-auth2.t b/t/plugin/jwt-auth2.t
index a6c6f31ad..622513387 100644
--- a/t/plugin/jwt-auth2.t
+++ b/t/plugin/jwt-auth2.t
@@ -147,3 +147,322 @@ GET /hello
 jwt-header: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTg3OTMxODU0MX0.fNtFJnNmJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs
 --- response_body
 hello world
+
+
+
+=== TEST 8: use lifetime_grace_period default value
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+
+            -- in order to modify the system_leeway in jwt-validators module
+            local code, body = t('/apisix/admin/routes/1',
+                 ngx.HTTP_PUT,
+                 [[{ "plugins": {
+                            "openid-connect": {
+                                "client_id": "kbyuFDidLLm280LIwVFiazOqjO3ty8KH",
+                                "client_secret": "60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa",
+                                "discovery": "https://samples.auth0.com/.well-known/openid-configuration",
+                                "redirect_uri": "https://iresty.com",
+                                "ssl_verify": false,
+                                "timeout": 10,
+                                "bearer_only": true,
+                                "scope": "apisix",
+                                "public_key": "-----BEGIN PUBLIC KEY-----\n]] ..
+                                    [[MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANW16kX5SMrMa2t7F2R1w6Bk/qpjS4QQ\n]] ..
+                                    [[hnrbED3Dpsl9JXAx90MYsIWp51hBxJSE/EPVK8WF/sjHK1xQbEuDfEECAwEAAQ==\n]] ..
+                                    [[-----END PUBLIC KEY-----",
+                                "token_signing_alg_values_expected": "RS256"
+                            }
+                        },
+                        "upstream": {
+                            "nodes": {
+                                "127.0.0.1:1980": 1
+                            },
+                            "type": "roundrobin"
+                        },
+                        "uri": "/hello"
+                }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+                ngx.say(body)
+                return
+            end
+
+            local http = require "resty.http"
+            local httpc = http.new()
+            local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello"
+            local res, err = httpc:request_uri(uri, {
+                method = "GET",
+                    headers = {
+                        ["Authorization"] = "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9" ..
+                        ".eyJkYXRhMSI6IkRhdGEgMSIsImlhdCI6MTU4NTEyMjUwMiwiZXhwIjoxOTAwNjk" ..
+                        "4NTAyLCJhdWQiOiJodHRwOi8vbXlzb2Z0Y29ycC5pbiIsImlzcyI6Ik15c29mdCB" ..
+                        "jb3JwIiwic3ViIjoic29tZUB1c2VyLmNvbSJ9.u1ISx7JbuK_GFRIUqIMP175FqX" ..
+                        "RyF9V7y86480Q4N3jNxs3ePbc51TFtIHDrKttstU4Tub28PYVSlr-HXfjo7w",
+                    }
+                })
+            ngx.status = res.status
+            if res.status >= 300 then
+                ngx.status = res.status
+                ngx.say(res.body)
+                return
+            end
+
+            -- add consumer
+            local code, body, res_data = t('/apisix/admin/consumers',
+                ngx.HTTP_PUT,
+                [[{
+                    "username": "kerouac",
+                    "plugins": {
+                        "jwt-auth": {
+                            "exp": 1,
+                            "algorithm": "HS256",
+                            "base64_secret": false,
+                            "secret": "test-jwt-secret",
+                            "key": "test-jwt-a"
+                        }
+                    }
+                }]]
+             )
+
+            if code >= 300 then
+                ngx.status = code
+                ngx.say(body)
+            end
+
+            -- add route
+            code, body = t('/apisix/admin/routes/1',
+                ngx.HTTP_PUT,
+                [[{
+                    "plugins": {
+                        "jwt-auth": {
+                            "query": "jwt",
+                            "header": "Mytoken",
+                            "cookie": "jwt"
+                        }
+                    },
+                    "upstream": {
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        },
+                        "type": "roundrobin"
+                    },
+                    "uri": "/hello"
+                }]]
+            )
+
+            if code >= 300 then
+                ngx.status = code
+                ngx.say(body)
+            end
+
+            -- resgiter jwt sign api
+            local code, body = t('/apisix/admin/routes/2',
+                 ngx.HTTP_PUT,
+                 [[{
+                        "plugins": {
+                            "public-api": {}
+                        },
+                        "uri": "/apisix/plugin/jwt/sign"
+                 }]]
+            )
+            if code >= 300 then
+                ngx.status = code
+                ngx.say(body)
+            end
+
+            -- get JWT token
+            local code, err, sign = t('/apisix/plugin/jwt/sign?key=test-jwt-a',
+                ngx.HTTP_GET
+            )
+
+            if code > 200 then
+                ngx.status = code
+                ngx.say(err)
+                return
+            end
+
+            -- verify JWT token
+            local http = require("resty.http")
+            local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello"
+            local httpc = http.new()
+            local res, err = httpc:request_uri(uri, {headers={Mytoken=sign}})
+
+            -- the JWT has not expired, so it should be valid
+            if res.status >= 300 then
+                ngx.status = res.status
+                ngx.say(res.body)
+                return
+            end
+
+            -- after 1.1 seconds, the JWT should be expired, because the exp is only 1 second
+            ngx.sleep(1.1)
+            res, err = httpc:request_uri(uri, {headers={Mytoken=sign}})
+            ngx.status = res.status
+            ngx.print(res.body)
+        }
+    }
+--- error_code: 401
+--- response_body eval
+qr/failed to verify jwt/
+--- error_log eval
+qr/ailed to verify jwt: 'exp' claim expired at/
+
+
+
+=== TEST 9: lifetime_grace_period is 2 seconds
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+
+            -- in order to modify the system_leeway in jwt-validators module
+            local code, body = t('/apisix/admin/routes/1',
+                 ngx.HTTP_PUT,
+                 [[{ "plugins": {
+                            "openid-connect": {
+                                "client_id": "kbyuFDidLLm280LIwVFiazOqjO3ty8KH",
+                                "client_secret": "60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa",
+                                "discovery": "https://samples.auth0.com/.well-known/openid-configuration",
+                                "redirect_uri": "https://iresty.com",
+                                "ssl_verify": false,
+                                "timeout": 10,
+                                "bearer_only": true,
+                                "scope": "apisix",
+                                "public_key": "-----BEGIN PUBLIC KEY-----\n]] ..
+                                    [[MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANW16kX5SMrMa2t7F2R1w6Bk/qpjS4QQ\n]] ..
+                                    [[hnrbED3Dpsl9JXAx90MYsIWp51hBxJSE/EPVK8WF/sjHK1xQbEuDfEECAwEAAQ==\n]] ..
+                                    [[-----END PUBLIC KEY-----",
+                                "token_signing_alg_values_expected": "RS256"
+                            }
+                        },
+                        "upstream": {
+                            "nodes": {
+                                "127.0.0.1:1980": 1
+                            },
+                            "type": "roundrobin"
+                        },
+                        "uri": "/hello"
+                }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+                ngx.say(body)
+                return
+            end
+
+            local http = require "resty.http"
+            local httpc = http.new()
+            local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello"
+            local res, err = httpc:request_uri(uri, {
+                method = "GET",
+                    headers = {
+                        ["Authorization"] = "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9" ..
+                        ".eyJkYXRhMSI6IkRhdGEgMSIsImlhdCI6MTU4NTEyMjUwMiwiZXhwIjoxOTAwNjk" ..
+                        "4NTAyLCJhdWQiOiJodHRwOi8vbXlzb2Z0Y29ycC5pbiIsImlzcyI6Ik15c29mdCB" ..
+                        "jb3JwIiwic3ViIjoic29tZUB1c2VyLmNvbSJ9.u1ISx7JbuK_GFRIUqIMP175FqX" ..
+                        "RyF9V7y86480Q4N3jNxs3ePbc51TFtIHDrKttstU4Tub28PYVSlr-HXfjo7w",
+                    }
+                })
+            ngx.status = res.status
+            if res.status >= 300 then
+                ngx.status = res.status
+                ngx.say(res.body)
+                return
+            end
+
+            -- add consumer
+            local code, body, res_data = t('/apisix/admin/consumers',
+                ngx.HTTP_PUT,
+                [[{
+                    "username": "kerouac",
+                    "plugins": {
+                        "jwt-auth": {
+                            "exp": 1,
+                            "algorithm": "HS256",
+                            "base64_secret": false,
+                            "secret": "test-jwt-secret",
+                            "key": "test-jwt-a",
+                            "lifetime_grace_period": 2
+                        }
+                    }
+                }]]
+             )
+
+            if code >= 300 then
+                ngx.status = code
+                ngx.say(body)
+            end
+
+            -- add route
+            code, body = t('/apisix/admin/routes/1',
+                ngx.HTTP_PUT,
+                [[{
+                    "plugins": {
+                        "jwt-auth": {
+                            "query": "jwt",
+                            "header": "Mytoken",
+                            "cookie": "jwt"
+                        }
+                    },
+                    "upstream": {
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        },
+                        "type": "roundrobin"
+                    },
+                    "uri": "/hello"
+                }]]
+            )
+
+            if code >= 300 then
+                ngx.status = code
+                ngx.say(body)
+            end
+
+            -- resgiter jwt sign api
+            local code, body = t('/apisix/admin/routes/2',
+                 ngx.HTTP_PUT,
+                 [[{
+                        "plugins": {
+                            "public-api": {}
+                        },
+                        "uri": "/apisix/plugin/jwt/sign"
+                 }]]
+            )
+            if code >= 300 then
+                ngx.status = code
+                ngx.say(body)
+            end
+
+            -- get JWT token
+            local code, err, sign = t('/apisix/plugin/jwt/sign?key=test-jwt-a',
+                ngx.HTTP_GET
+            )
+
+            if code > 200 then
+                ngx.status = code
+                ngx.say(err)
+                return
+            end
+
+            -- verify JWT token
+            local http = require("resty.http")
+            local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello"
+            local httpc = http.new()
+
+            -- after 1.1 seconds, since lifetime_grace_period is 2 seconds,
+            -- so the JWT has not expired, it should be valid
+            ngx.sleep(1.1)
+            local res, err = httpc:request_uri(uri, {headers={Mytoken=sign}})
+            ngx.status = res.status
+            ngx.print(res.body)
+        }
+    }
+--- response_body
+hello world