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

wenming 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 59b50b64f feat(openid-connect): allow set headers in introspection 
request (#11090)
59b50b64f is described below

commit 59b50b64f8d744c8ca11851aa205d4688c1e4d13
Author: yuweizzz <[email protected]>
AuthorDate: Tue Apr 16 17:17:40 2024 +0800

    feat(openid-connect): allow set headers in introspection request (#11090)
---
 apisix/plugins/openid-connect.lua        |  29 ++++-
 docs/en/latest/plugins/openid-connect.md |   1 +
 docs/zh/latest/plugins/openid-connect.md |   1 +
 t/plugin/openid-connect6.t               | 208 +++++++++++++++++++++++++++++++
 4 files changed, 237 insertions(+), 2 deletions(-)

diff --git a/apisix/plugins/openid-connect.lua 
b/apisix/plugins/openid-connect.lua
index da334ebfb..c3d79fa45 100644
--- a/apisix/plugins/openid-connect.lua
+++ b/apisix/plugins/openid-connect.lua
@@ -21,8 +21,8 @@ local openidc = require("resty.openidc")
 local random  = require("resty.random")
 local string  = string
 local ngx     = ngx
-local ipairs = ipairs
-local concat = table.concat
+local ipairs  = ipairs
+local concat  = table.concat
 
 local ngx_encode_base64 = ngx.encode_base64
 
@@ -260,6 +260,15 @@ local schema = {
             description = "Name of the expiry claim that controls the cached 
access token TTL.",
             type = "string"
         },
+        introspection_addon_headers = {
+            description = "Extra http headers in introspection",
+            type = "array",
+            minItems = 1,
+            items = {
+                type = "string",
+                pattern = "^[^:]+$"
+            }
+        },
         required_scopes = {
             description = "List of scopes that are required to be granted to 
the access token",
             type = "array",
@@ -386,7 +395,23 @@ local function introspect(ctx, conf)
     else
         -- Validate token against introspection endpoint.
         -- TODO: Same as above for public key validation.
+        if conf.introspection_addon_headers then
+            -- http_request_decorator option provided by lua-resty-openidc
+            conf.http_request_decorator = function(req)
+                local h = req.headers or {}
+                for _, name in ipairs(conf.introspection_addon_headers) do
+                    local value = core.request.header(ctx, name)
+                    if value then
+                        h[name] = value
+                    end
+                end
+                req.headers = h
+                return req
+            end
+        end
+
         local res, err = openidc.introspect(conf)
+        conf.http_request_decorator = nil
 
         if err then
             ngx.header["WWW-Authenticate"] = 'Bearer realm="' .. conf.realm ..
diff --git a/docs/en/latest/plugins/openid-connect.md 
b/docs/en/latest/plugins/openid-connect.md
index 951e8b6d5..4483abd65 100644
--- a/docs/en/latest/plugins/openid-connect.md
+++ b/docs/en/latest/plugins/openid-connect.md
@@ -89,6 +89,7 @@ description: OpenID Connect allows the client to obtain user 
information from th
 | cache_segment | string | False |  |  | Optional name of a cache segment, 
used to separate and differentiate caches used by token introspection or JWT 
verification. |
 | introspection_interval | integer | False | 0 |  | TTL of the cached and 
introspected access token in seconds. |
 | introspection_expiry_claim | string | False |  |  | Name of the expiry 
claim, which controls the TTL of the cached and introspected access token. The 
default value is 0, which means this option is not used and the plugin defaults 
to use the TTL passed by expiry claim defined in `introspection_expiry_claim`. 
If `introspection_interval` is larger than 0 and less than the TTL passed by 
expiry claim defined in `introspection_expiry_claim`, use 
`introspection_interval`. |
+| introspection_addon_headers | string[] | False |  |  | Array of strings. 
Used to append additional header values to the introspection HTTP request. If 
the specified header does not exist in origin request, value will not be 
appended. |
 
 NOTE: `encrypt_fields = {"client_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).
 
diff --git a/docs/zh/latest/plugins/openid-connect.md 
b/docs/zh/latest/plugins/openid-connect.md
index c325ac4a2..391f0f9b0 100644
--- a/docs/zh/latest/plugins/openid-connect.md
+++ b/docs/zh/latest/plugins/openid-connect.md
@@ -89,6 +89,7 @@ description: OpenID Connect(OIDC)是基于 OAuth 2.0 的身份认证协议
 | cache_segment                   | string  | 否    |               |           
  | 可选的缓存段的名称,用于区分和区分用于令牌内省或 JWT 验证的缓存。 |
 | introspection_interval          | integer | 否    | 0             |           
  | 以秒为单位的缓存和内省访问令牌的 TTL。   |
 | introspection_expiry_claim      | string  | 否    |               |           
  | 过期声明的名称,用于控制缓存和内省访问令牌的 TTL。 |
+| introspection_addon_headers     | string[] | 否    |               |          
   | `introspection_addon_headers` 是字符串列表,用于配置额外添加到内省 HTTP 
请求中的请求头,如果配置的请求头不存在于源请求中,它将被忽略。|
 
 注意:schema 中还定义了 `encrypt_fields = {"client_secret"}`,这意味着该字段将会被加密存储在 etcd 
中。具体参考 [加密存储字段](../plugin-develop.md#加密存储字段)。
 
diff --git a/t/plugin/openid-connect6.t b/t/plugin/openid-connect6.t
index 6497f3baa..2406c1353 100644
--- a/t/plugin/openid-connect6.t
+++ b/t/plugin/openid-connect6.t
@@ -155,3 +155,211 @@ passed
     }
 --- response_body
 passed
+
+
+
+=== TEST 4: Update route with Keycloak introspection endpoint and 
introspection addon headers.
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1',
+                 ngx.HTTP_PUT,
+                 [[{
+                        "plugins": {
+                            "openid-connect": {
+                                "client_id": "course_management",
+                                "client_secret": 
"d1ec69e9-55d2-4109-a3ea-befa071579d5",
+                                "discovery": 
"http://127.0.0.1:8080/realms/University/.well-known/openid-configuration";,
+                                "redirect_uri": "http://localhost:3000";,
+                                "ssl_verify": false,
+                                "timeout": 10,
+                                "bearer_only": true,
+                                "realm": "University",
+                                "introspection_endpoint_auth_method": 
"client_secret_post",
+                                "introspection_endpoint": 
"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token/introspect";,
+                                "introspection_addon_headers": 
["X-Addon-Header-A", "X-Addon-Header-B"]
+                            }
+                        },
+                        "upstream": {
+                            "nodes": {
+                                "127.0.0.1:1980": 1
+                            },
+                            "type": "roundrobin"
+                        },
+                        "uri": "/hello"
+                }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 5: Obtain valid token and access route with it, introspection work as 
expected when configured extras headers.
+--- config
+    location /t {
+        content_by_lua_block {
+            -- Obtain valid access token from Keycloak using known username 
and password.
+            local json_decode = require("toolkit.json").decode
+            local http = require "resty.http"
+            local httpc = http.new()
+            local uri = 
"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token";
+            local res, err = httpc:request_uri(uri, {
+                    method = "POST",
+                    body = 
"grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&[email protected]&password=123456",
+                    headers = {
+                        ["Content-Type"] = "application/x-www-form-urlencoded"
+                    }
+                })
+
+            -- Check response from keycloak and fail quickly if there's no 
response.
+            if not res then
+                ngx.say(err)
+                return
+            end
+
+            -- Check if response code was ok.
+            if res.status == 200 then
+                -- Get access token from JSON response body.
+                local body = json_decode(res.body)
+                local accessToken = body["access_token"]
+
+                -- Access route using access token. Should work.
+                uri = "http://127.0.0.1:"; .. ngx.var.server_port .. "/hello"
+                local res, err = httpc:request_uri(uri, {
+                    method = "GET",
+                    headers = {
+                        ["Authorization"] = "Bearer " .. body["access_token"],
+                        ["X-Addon-Header-A"] = "Value-A",
+                        ["X-Addon-Header-B"] = "Value-b"
+                    }
+                 })
+
+                if res.status == 200 then
+                    -- Route accessed successfully.
+                    ngx.say(true)
+                else
+                    -- Couldn't access route.
+                    ngx.say(false)
+                end
+            else
+                -- Response from Keycloak not ok.
+                ngx.say(false)
+            end
+        }
+    }
+--- response_body
+true
+--- error_log
+token validate successfully by introspection
+
+
+
+=== TEST 6: Access route with an invalid token, should fail.
+--- config
+    location /t {
+        content_by_lua_block {
+            -- Access route using a fake access token.
+            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 " .. "fake access token",
+                    ["X-Addon-Header-A"] = "Value-A",
+                    ["X-Addon-Header-B"] = "Value-b"
+                }
+             })
+
+            if res.status == 200 then
+                ngx.say(true)
+            else
+                ngx.say(false)
+            end
+        }
+    }
+--- response_body
+false
+--- error_log
+OIDC introspection failed: invalid token
+
+
+
+=== TEST 7: Update route with fake Keycloak introspection endpoint and 
introspection addon headers
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1',
+                 ngx.HTTP_PUT,
+                 [[{
+                        "plugins": {
+                            "openid-connect": {
+                                "client_id": "course_management",
+                                "client_secret": 
"d1ec69e9-55d2-4109-a3ea-befa071579d5",
+                                "discovery": 
"http://127.0.0.1:8080/realms/University/.well-known/openid-configuration";,
+                                "redirect_uri": "http://localhost:3000";,
+                                "ssl_verify": false,
+                                "timeout": 10,
+                                "bearer_only": true,
+                                "realm": "University",
+                                "introspection_endpoint_auth_method": 
"client_secret_post",
+                                "introspection_endpoint": 
"http://127.0.0.1:1980/log_request";,
+                                "introspection_addon_headers": 
["X-Addon-Header-A", "X-Addon-Header-B"]
+                            }
+                        },
+                        "upstream": {
+                            "nodes": {
+                                "127.0.0.1:1980": 1
+                            },
+                            "type": "roundrobin"
+                        },
+                        "uri": "/hello"
+                }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 8: Check http headers from fake introspection endpoint.
+--- config
+    location /t {
+        content_by_lua_block {
+            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 " .. "fake access token",
+                        ["X-Addon-Header-A"] = "Value-A",
+                        ["X-Addon-Header-B"] = "Value-b"
+                    }
+                })
+            ngx.status = res.status
+        }
+    }
+--- error_code: 401
+--- error_log
+OIDC introspection failed: JSON decoding failed
+--- grep_error_log eval
+qr/x-addon-header-.{10}/
+--- grep_error_log_out
+x-addon-header-a: Value-A
+x-addon-header-b: Value-b

Reply via email to