This is an automated email from the ASF dual-hosted git repository. ashishtiwari 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 eed93f315 feat: add support for extra_headers in forward-auth plugin (#12405) eed93f315 is described below commit eed93f315c917e7bedcddf18d7df56af556cb918 Author: Ashish Tiwari <ashishjaitiwari15112...@gmail.com> AuthorDate: Mon Jul 14 12:35:21 2025 +0530 feat: add support for extra_headers in forward-auth plugin (#12405) --- apisix/plugins/forward-auth.lua | 39 +++++++++++- docs/en/latest/plugins/forward-auth.md | 107 ++++++++++++++++++++++++++++++++- docs/zh/latest/plugins/forward-auth.md | 107 ++++++++++++++++++++++++++++++++- t/plugin/forward-auth.t | 74 ++++++++++++++++++++++- 4 files changed, 321 insertions(+), 6 deletions(-) diff --git a/apisix/plugins/forward-auth.lua b/apisix/plugins/forward-auth.lua index bd58364b2..aa220f745 100644 --- a/apisix/plugins/forward-auth.lua +++ b/apisix/plugins/forward-auth.lua @@ -15,9 +15,12 @@ -- limitations under the License. -- -local ipairs = ipairs -local core = require("apisix.core") -local http = require("resty.http") +local ipairs = ipairs +local core = require("apisix.core") +local http = require("resty.http") +local pairs = pairs +local type = type +local tostring = tostring local schema = { type = "object", @@ -41,6 +44,20 @@ local schema = { items = {type = "string"}, description = "client request header that will be sent to the authorization service" }, + extra_headers = { + type = "object", + minProperties = 1, + patternProperties = { + ["^[^:]+$"] = { + type = "string", + description = "header value as a string; may contain variables" + .. "like $remote_addr, $request_uri" + } + }, + description = "extra headers sent to the authorization service; " + .. "values must be strings and can include variables" + .. "like $remote_addr, $request_uri." + }, upstream_headers = { type = "array", default = {}, @@ -102,6 +119,22 @@ function _M.access(conf, ctx) auth_headers["Content-Encoding"] = core.request.header(ctx, "content-encoding") end + if conf.extra_headers then + for header, value in pairs(conf.extra_headers) do + if type(value) == "number" then + value = tostring(value) + end + local resolve_value, err = core.utils.resolve_var(value, ctx.var) + if not err then + auth_headers[header] = resolve_value + end + if err then + core.log.error("failed to resolve variable in extra header '", + header, "': ",value,": ",err) + end + end + end + -- append headers that need to be get from the client request header if #conf.request_headers > 0 then for _, header in ipairs(conf.request_headers) do diff --git a/docs/en/latest/plugins/forward-auth.md b/docs/en/latest/plugins/forward-auth.md index b1aacc807..b57e92418 100644 --- a/docs/en/latest/plugins/forward-auth.md +++ b/docs/en/latest/plugins/forward-auth.md @@ -40,8 +40,9 @@ This Plugin moves the authentication and authorization logic to a dedicated exte | ----------------- | ------------- | -------- | ------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | uri | string | True | | | URI of the authorization service. | | ssl_verify | boolean | False | true | | When set to `true`, verifies the SSL certificate. | -| request_method | string | False | GET | ["GET","POST"] | HTTP method for a client to send requests to the authorization service. When set to `POST` the request body is send to the authorization service. | +| request_method | string | False | GET | ["GET","POST"] | HTTP method for a client to send requests to the authorization service. When set to `POST` the request body is sent to the authorization service. (not recommended - see section on [Using data from POST body](#using-data-from-post-body-to-make-decision-on-authorization-service)) | | request_headers | array[string] | False | | | Client request headers to be sent to the authorization service. If not set, only the headers provided by APISIX are sent (for example, `X-Forwarded-XXX`). | +| extra_headers |object | False | | | Extra headers to be sent to the authorization service passed in key-value format. The value can be a variable like `$request_uri`, `$post_arg.xyz` | | upstream_headers | array[string] | False | | | Authorization service response headers to be forwarded to the Upstream. If not set, no headers are forwarded to the Upstream service. | | client_headers | array[string] | False | | | Authorization service response headers to be sent to the client when authorization fails. If not set, no headers will be sent to the client. | | timeout | integer | False | 3000ms | [1, 60000]ms | Timeout for the authorization service HTTP call. | @@ -166,6 +167,110 @@ HTTP/1.1 403 Forbidden Location: http://example.com/auth ``` +### Using data from POST body to make decision on Authorization service + +::: note +When the decision is to be made on the basis of POST body, then it is recommended to use `$post_arg.*` with `extra_headers` field and make the decision on Authorization service on basis of headers rather than using POST `request_method` to pass the entire request body to Authorization service. +::: + +Create a serverless function on the `/auth` route that checks for the presence of the `tenant_id` header. If present, the route responds with HTTP 200 and sets the `X-User-ID` header to a fixed value `i-am-an-user`. If `tenant_id` is missing, it returns HTTP 400 with an error message. + +```shell +curl -X PUT 'http://127.0.0.1:9180/apisix/admin/routes/auth' \ + -H "X-API-KEY: $admin_key" \ + -H 'Content-Type: application/json' \ + -d '{ + "uri": "/auth", + "plugins": { + "serverless-pre-function": { + "phase": "rewrite", + "functions": [ + "return function(conf, ctx) + local core = require(\"apisix.core\") + if core.request.header(ctx, \"tenant_id\") then + core.response.set_header(\"X-User-ID\", \"i-am-an-user\"); + core.response.exit(200); + else + core.response.exit(400, \"tenant_id is required\") + end + end" + ] + } + } +}' +``` + +Create a route that accepts POST requests and uses the `forward-auth` plugin to call the auth endpoint with the `tenant_id` from the request. The request is forwarded to the upstream service only if the auth check returns 200. + +```shell +curl -X PUT 'http://127.0.0.1:9180/apisix/admin/routes/1' \ + -H "X-API-KEY: $admin_key" \ + -d '{ + "uri": "/post", + "methods": ["POST"], + "plugins": { + "forward-auth": { + "uri": "http://127.0.0.1:9080/auth", + "request_method": "GET", + "extra_headers": {"tenant_id": "$post_arg.tenant_id"} + } + }, + "upstream": { + "nodes": { + "httpbin.org:80": 1 + }, + "type": "roundrobin" + } +}' +``` + +Send a POST request with the `tenant_id` header: + +```shell +curl -i http://127.0.0.1:9080/post -X POST -d '{ + "tenant_id": 123 +}' +``` + +You should receive an `HTTP/1.1 200 OK` response similar to the following: + +```json +{ + "args": {}, + "data": "", + "files": {}, + "form": { + "{\n \"tenant_id\": 123\n}": "" + }, + "headers": { + "Accept": "*/*", + "Content-Length": "23", + "Content-Type": "application/x-www-form-urlencoded", + "Host": "127.0.0.1", + "User-Agent": "curl/8.13.0", + "X-Amzn-Trace-Id": "Root=1-686b6e3f-2fdeff70183e71551f5c5729", + "X-Forwarded-Host": "127.0.0.1" + }, + "json": null, + "origin": "127.0.0.1, 106.215.83.33", + "url": "http://127.0.0.1/post" +} +``` + +Send a POST request without the `tenant_id` header: + +```shell + curl -i http://127.0.0.1:9080/post -X POST -d '{ + "abc": 123 +}' +``` + +You should receive an `HTTP/1.1 400 Bad Request` response with the following message: + +```shell +tenant_id is required +``` + ## Delete Plugin To remove the `forward-auth` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect. diff --git a/docs/zh/latest/plugins/forward-auth.md b/docs/zh/latest/plugins/forward-auth.md index a030a6f8f..58c462a2c 100644 --- a/docs/zh/latest/plugins/forward-auth.md +++ b/docs/zh/latest/plugins/forward-auth.md @@ -39,8 +39,9 @@ description: 本文介绍了关于 Apache APISIX `forward-auth` 插件的基本 | ----------------- | ------------- | ------| ------- | -------------- | -------------------------------------------------------------------------------------------------------------------- | | uri | string | 是 | | | 设置 `authorization` 服务的地址 (例如:https://localhost:9188)。 | | ssl_verify | boolean | 否 | true | [true, false] | 当设置为 `true` 时,验证 SSL 证书。 | -| request_method | string | 否 | GET | ["GET","POST"] | 客户端向 `authorization` 服务发送请求的方法。当设置为 POST 时,会将 `request body` 转发至 `authorization` 服务。 | +| request_method | string | 否 | GET | ["GET","POST"] | 客户端向 authorization 服务发送请求的方法。当设置为 POST 时,会将 request body 转发至 authorization 服务。 | | request_headers | array[string] | 否 | | | 设置需要由客户端转发到 `authorization` 服务的请求头。如果没有设置,则只发送 APISIX 提供的 headers (例如:X-Forwarded-XXX)。 | +| extra_headers |object | False | | | 以键值格式传递给授权服务的额外标头。值可以是变量,例如“$request_uri”或“$post_arg.xyz”。 | | upstream_headers | array[string] | 否 | | | 认证通过时,设置 `authorization` 服务转发至 `upstream` 的请求头。如果不设置则不转发任何请求头。 | | client_headers | array[string] | 否 | | | 认证失败时,由 `authorization` 服务向 `client` 发送的响应头。如果不设置则不转发任何响应头。 | | timeout | integer | 否 | 3000ms | [1, 60000]ms | `authorization` 服务请求超时时间。 | @@ -168,6 +169,110 @@ HTTP/1.1 403 Forbidden Location: http://example.com/auth ``` +### Using data from POST body to make decision on Authorization service + +::: note +当要根据 POST 正文做出决定时,建议使用带有 `extra_headers` 字段的 `$post_arg.*` 并根据标头对授权服务做出决定,而不是使用 POST `request_method` 将整个请求正文传递给授权服务。 +::: + +在 `/auth` 路由上创建一个无服务器函数,用于检查 `tenant_id` 标头是否存在。如果存在,路由会使用 HTTP 200 进行响应,并将 `X-User-ID` 标头设置为固定值 `i-am-an-user`。如果缺少 `tenant_id`,则会返回 HTTP 400 和错误消息。 + +```shell +curl -X PUT 'http://127.0.0.1:9180/apisix/admin/routes/auth' \ + -H "X-API-KEY: $admin_key" \ + -H 'Content-Type: application/json' \ + -d '{ + "uri": "/auth", + "plugins": { + "serverless-pre-function": { + "phase": "rewrite", + "functions": [ + "return function(conf, ctx) + local core = require(\"apisix.core\") + if core.request.header(ctx, \"tenant_id\") then + core.response.set_header(\"X-User-ID\", \"i-am-an-user\"); + core.response.exit(200); + else + core.response.exit(400, \"tenant_id is required\") + end + end" + ] + } + } +}' +``` + +创建一个接受 POST 请求的路由,并使用 `forward-auth` 插件通过请求中的 `tenant_id` 调用身份验证端点。只有当身份验证检查返回 200 时,请求才会转发到上游服务。 + +```shell +curl -X PUT 'http://127.0.0.1:9180/apisix/admin/routes/1' \ + -H "X-API-KEY: $admin_key" \ + -d '{ + "uri": "/post", + "methods": ["POST"], + "plugins": { + "forward-auth": { + "uri": "http://127.0.0.1:9080/auth", + "request_method": "GET", + "extra_headers": {"tenant_id": "$post_arg.tenant_id"} + } + }, + "upstream": { + "nodes": { + "httpbin.org:80": 1 + }, + "type": "roundrobin" + } +}' +``` + +发送带有 `tenant_id` 标头的 POST 请求: + +```shell +curl -i http://127.0.0.1:9080/post -X POST -d '{ + "tenant_id": 123 +}' +``` + +您应该收到类似以下内容的 `HTTP/1.1 200 OK` 响应: + +```json +{ + "args": {}, + "data": "", + "files": {}, + "form": { + "{\n \"tenant_id\": 123\n}": "" + }, + "headers": { + "Accept": "*/*", + "Content-Length": "23", + "Content-Type": "application/x-www-form-urlencoded", + "Host": "127.0.0.1", + "User-Agent": "curl/8.13.0", + "X-Amzn-Trace-Id": "Root=1-686b6e3f-2fdeff70183e71551f5c5729", + "X-Forwarded-Host": "127.0.0.1" + }, + "json": null, + "origin": "127.0.0.1, 106.215.83.33", + "url": "http://127.0.0.1/post" +} +``` + +发送不带 `tenant_id` 标头的 POST 请求: + +```shell + curl -i http://127.0.0.1:9080/post -X POST -d '{ + "abc": 123 +}' +``` + +您应该收到包含以下消息的 `HTTP/1.1 400 Bad Request` 响应: + +```shell +tenant_id is required +``` + ## 删除插件 当你需要禁用 `forward-auth` 插件时,可以通过以下命令删除相应的 JSON 配置,APISIX 将会自动重新加载相关配置,无需重启服务: diff --git a/t/plugin/forward-auth.t b/t/plugin/forward-auth.t index d6f657537..ce8dd05c4 100644 --- a/t/plugin/forward-auth.t +++ b/t/plugin/forward-auth.t @@ -109,6 +109,17 @@ property "request_method" validation failed: matches none of the enum values core.response.exit(403, core.request.headers(ctx)); end end]], + [[return function(conf, ctx) + local core = require("apisix.core"); + if core.request.header(ctx, "Authorization") == "777" then + if core.request.header(ctx, "tenant_id") then + core.response.set_header("X-User-ID", "i-am-an-user"); + core.response.exit(200); + else + core.response.exit(400, "tenant_id is required"); + end + end + end]], [[return function(conf, ctx) local core = require("apisix.core") if core.request.get_method() == "POST" then @@ -274,6 +285,46 @@ property "request_method" validation failed: matches none of the enum values "upstream_id": "u1", "uri": "/onerror" }]], + }, + { + url = "/apisix/admin/routes/9", + data = [[{ + "plugins": { + "forward-auth": { + "uri": "http://127.0.0.1:1984/auth", + "request_method": "GET", + "request_headers": ["Authorization"], + "upstream_headers": ["X-User-ID"], + "client_headers": ["Location"], + "extra_headers": {"tenant_id": "$post_arg.tenant_id"} + }, + "proxy-rewrite": { + "uri": "/echo" + } + }, + "upstream_id": "u1", + "uri": "/ping2" + }]] + }, + { + url = "/apisix/admin/routes/10", + data = [[{ + "plugins": { + "forward-auth": { + "uri": "http://127.0.0.1:1984/auth", + "request_method": "GET", + "request_headers": ["Authorization"], + "upstream_headers": ["X-User-ID"], + "client_headers": ["Location"], + "extra_headers": {"tenant_id": "abcd"} + }, + "proxy-rewrite": { + "uri": "/echo" + } + }, + "upstream_id": "u1", + "uri": "/ping3" + }]] } } @@ -286,7 +337,7 @@ property "request_method" validation failed: matches none of the enum values } } --- response_body eval -"passed\n" x 10 +"passed\n" x 12 @@ -403,3 +454,24 @@ GET /onerror --- more_headers Authorization: 333 --- error_code: 503 + + + +=== TEST 14: hit route (test extra_headers when use post method) +--- request +POST /ping2 +{"tenant_id": 123} +--- more_headers +Authorization: 777 +--- response_body_like eval +qr/\"x-user-id\":\"i-am-an-user\"/ + + + +=== TEST 15: hit route (test extra_headers when extra headers has fixed value) +--- request +GET /ping3 +--- more_headers +Authorization: 777 +--- response_body_like eval +qr/\"x-user-id\":\"i-am-an-user\"/