Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .luarc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json",
"runtime": {
"version": "LuaJIT",
"path": [
"?.lua",
"?/init.lua",
"apisix/?.lua",
"apisix/?/init.lua",
"t/lib/?.lua",
"t/lib/?/init.lua"
]
},
"diagnostics": {
"globals": [
"ngx",
"jit",
"arg",
"lua_load"
]
},
"workspace": {
"checkThirdParty": false
}
}
32 changes: 32 additions & 0 deletions apisix/consumer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,30 @@ local function get_anonymous_consumer_from_local_cache(name)
end


local function get_consumer_from_local_cache(name)
local consumer_raw = consumers:get(name)

if not consumer_raw or not consumer_raw.value or
not consumer_raw.value.id or not consumer_raw.modifiedIndex then
return nil, nil, "failed to get consumer " .. name
end

local consumer = consumer_raw.value
consumer.consumer_name = consumer_raw.value.id
consumer.modifiedIndex = consumer_raw.modifiedIndex

if consumer.labels then
consumer.custom_id = consumer.labels["custom_id"]
end

local consumer_conf = {
conf_version = consumer_raw.modifiedIndex
}

return consumer, consumer_conf
end


function _M.get_anonymous_consumer(name)
local anon_consumer, anon_consumer_conf, err
anon_consumer, anon_consumer_conf, err = get_anonymous_consumer_from_local_cache(name)
Expand All @@ -347,4 +371,12 @@ function _M.get_anonymous_consumer(name)
end


function _M.get_consumer_by_name(name)
local consumer, consumer_conf, err
consumer, consumer_conf, err = get_consumer_from_local_cache(name)

return consumer, consumer_conf, err
end


return _M
94 changes: 72 additions & 22 deletions apisix/plugins/forward-auth.lua
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,31 @@
local ipairs = ipairs
local core = require("apisix.core")
local http = require("resty.http")
local consumer = require("apisix.consumer")
local pairs = pairs
local type = type
local tostring = tostring

local schema = {
local schema = {
type = "object",
properties = {
uri = {type = "string"},
allow_degradation = {type = "boolean", default = false},
status_on_error = {type = "integer", minimum = 200, maximum = 599, default = 403},
uri = { type = "string" },
allow_degradation = { type = "boolean", default = false },
status_on_error = { type = "integer", minimum = 200, maximum = 599, default = 403 },
ssl_verify = {
type = "boolean",
default = true,
},
request_method = {
type = "string",
default = "GET",
enum = {"GET", "POST"},
enum = { "GET", "POST" },
description = "the method for client to request the authorization service"
},
request_headers = {
type = "array",
default = {},
items = {type = "string"},
items = { type = "string" },
description = "client request header that will be sent to the authorization service"
},
extra_headers = {
Expand All @@ -51,25 +52,31 @@ local schema = {
["^[^:]+$"] = {
type = "string",
description = "header value as a string; may contain variables"
.. "like $remote_addr, $request_uri"
.. "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."
.. "values must be strings and can include variables"
.. "like $remote_addr, $request_uri."
},
upstream_headers = {
type = "array",
default = {},
items = {type = "string"},
items = { type = "string" },
description = "authorization response header that will be sent to the upstream"
},
client_headers = {
type = "array",
default = {},
items = {type = "string"},
items = { type = "string" },
description = "authorization response header that will be sent to"
.. "the client when authorizing failed"
.. "the client when authorizing failed"
},
consumer_header = {
type = "string",
minLength = 1,
description = "authorization response header that contains the "
.. "APISIX Consumer username to attach to the request"
},
timeout = {
type = "integer",
Expand All @@ -78,11 +85,11 @@ local schema = {
default = 3000,
description = "timeout in milliseconds",
},
keepalive = {type = "boolean", default = true},
keepalive_timeout = {type = "integer", minimum = 1000, default = 60000},
keepalive_pool = {type = "integer", minimum = 1, default = 5},
keepalive = { type = "boolean", default = true },
keepalive_timeout = { type = "integer", minimum = 1000, default = 60000 },
keepalive_pool = { type = "integer", minimum = 1, default = 5 },
},
required = {"uri"}
required = { "uri" }
}


Expand All @@ -95,15 +102,42 @@ local _M = {


function _M.check_schema(conf)
local check = {"uri"}
local check = { "uri" }
core.utils.check_https(check, conf, _M.name)
core.utils.check_tls_bool({"ssl_verify"}, conf, _M.name)
core.utils.check_tls_bool({ "ssl_verify" }, conf, _M.name)

return core.schema.check(schema, conf)
end

local function attach_consumer_by_header(conf, ctx, res)
if not conf.consumer_header then
return
end

local consumer_name = res.headers[conf.consumer_header]
if type(consumer_name) == "table" then
consumer_name = consumer_name[1]
end

if not consumer_name or consumer_name == "" then
core.log.error("missing consumer header from auth response: ", conf.consumer_header)
return 403, "consumer header missing in auth response"
end

local auth_consumer, consumer_conf, err = consumer.get_consumer_by_name(consumer_name)
if not auth_consumer then
core.log.error("failed to fetch consumer by name from auth response header ",
conf.consumer_header, ": ", err)
return 403, "consumer not found"
end

consumer.attach_consumer(ctx, auth_consumer, consumer_conf)
end


local function do_auth(conf, ctx)
ctx.forward_auth_processed = true

function _M.access(conf, ctx)
local auth_headers = {
["X-Forwarded-Proto"] = core.request.get_scheme(ctx),
["X-Forwarded-Method"] = core.request.get_method(),
Expand All @@ -130,7 +164,7 @@ function _M.access(conf, ctx)
end
if err then
core.log.error("failed to resolve variable in extra header '",
header, "': ",value,": ",err)
header, "': ", value, ": ", err)
end
end
end
Expand Down Expand Up @@ -184,8 +218,12 @@ function _M.access(conf, ctx)
return res.status, res.body
end

-- set headers from the auth response, clearing any client-supplied values
-- for configured headers not present in the auth response
local code, body = attach_consumer_by_header(conf, ctx, res)
if code or body then
return code, body
end

-- append headers that need to be get from the auth response header
for _, header in ipairs(conf.upstream_headers) do
local header_value = res.headers[header]
-- if header_value is nil, the client header's value will be removed if it exists
Expand All @@ -194,4 +232,16 @@ function _M.access(conf, ctx)
end


function _M.rewrite(conf, ctx)
return do_auth(conf, ctx)
Comment thread
panxiaoquan marked this conversation as resolved.
end

function _M.access(conf, ctx)
if ctx.forward_auth_processed then
return
end

return do_auth(conf, ctx)
end

return _M
49 changes: 49 additions & 0 deletions docs/en/latest/plugins/forward-auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ The `forward-auth` Plugin supports the integration with an external authorizatio
| request_headers | array | False | | | Client request headers that should be forwarded to the external authorization service. If not configured, only headers added by APISIX are forwarded, such as `X-Forwarded-*`. |
| upstream_headers | array | False | | | External authorization service response headers that should be forwarded to the Upstream service. If not configured, no headers are forwarded to the Upstream service. |
| client_headers | array | False | | | External authorization service response headers that should be forwarded to the client when authentication fails. If not configured, no headers are forwarded to the client. |
| consumer_header | string | False | | | External authorization service response header that contains an existing APISIX Consumer username. When set and the auth response is 2xx, APISIX attaches that Consumer to the request. If the header is missing or the Consumer does not exist, the request is rejected with HTTP `403`. |
| extra_headers | object | False | | | Additional headers to send to the authorization service. Support [NGINX variables](https://nginx.org/en/docs/http/ngx_http_core_module.html) in values. |
| timeout | integer | False | 3000 | between 1 and 60000 inclusive | Timeout for the external authorization service HTTP call in milliseconds. |
| keepalive | boolean | False | true | | If true, keep the connections open for multiple requests. |
Expand All @@ -67,6 +68,8 @@ The `forward-auth` Plugin supports the integration with an external authorizatio

The examples below demonstrate how you can use `forward-auth` for different scenarios.

If `consumer_header` is configured and the authorization service returns a 2xx response, APISIX reads the configured response header as a Consumer username and attaches the corresponding Consumer to the current request. This allows Consumer and Consumer Group plugins to take effect for the request. If the header is missing or the Consumer does not exist in APISIX, the request is rejected with HTTP `403`.

:::note

You can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:
Expand Down Expand Up @@ -506,6 +509,52 @@ You should see an `HTTP/1.1 200 OK` response of the following, showing the heade
}
```

### Attach an APISIX Consumer from the Authorization Response

Create a Consumer that already exists in APISIX:

```shell
curl -X PUT 'http://127.0.0.1:9180/apisix/admin/consumers' \
-H "X-API-KEY: ${admin_key}" \
-d '{
"username": "demo-consumer",
"plugins": {
"limit-count": {
"count": 1,
"time_window": 60,
"rejected_code": 429,
"key": "remote_addr",
"policy": "local"
}
}
}'
```

Have the authorization service return the Consumer username in a response header such as `X-Consumer-Username`, and configure the Plugin to read it:

```shell
curl -X PUT 'http://127.0.0.1:9180/apisix/admin/routes/consumer-route' \
-H "X-API-KEY: ${admin_key}" \
-d '{
"uri": "/consumer-route",
"plugins": {
"forward-auth": {
"uri": "http://127.0.0.1:9080/auth",
"request_headers": ["Authorization"],
"consumer_header": "X-Consumer-Username"
}
},
"upstream": {
"nodes": {
"httpbin.org:80": 1
},
"type": "roundrobin"
}
}'
```

With this configuration, a successful auth response like `X-Consumer-Username: demo-consumer` attaches `demo-consumer` to the request. APISIX then applies Consumer-scoped plugins, such as the `limit-count` policy above.

### Return Designated Headers to Clients on Authentication Failure

The following example demonstrates how you can configure `forward-auth` on a Route to regulate client access to the Upstream resources. It also passes a specific header returned by the authorization service to the client when the authentication fails.
Expand Down
49 changes: 49 additions & 0 deletions docs/zh/latest/plugins/forward-auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import TabItem from '@theme/TabItem';
| request_headers | array | 否 | | | 需要转发给外部授权服务的客户端请求头。如果未配置,则只转发 APISIX 添加的请求头,例如 `X-Forwarded-*`。 |
| upstream_headers | array | 否 | | | 认证通过时,需要转发到 Upstream 服务的外部授权服务响应头。如果未配置,则不转发任何请求头。 |
| client_headers | array | 否 | | | 认证失败时,需要转发给客户端的外部授权服务响应头。如果未配置,则不转发任何响应头。 |
| consumer_header | string | 否 | | | 外部授权服务响应头中包含已存在 APISIX Consumer 用户名的字段。配置后,当认证服务返回 2xx 响应时,APISIX 会把该 Consumer 绑定到当前请求。如果响应头缺失,或 APISIX 中不存在对应 Consumer,请求会被拒绝并返回 HTTP `403`。 |
| extra_headers | object | 否 | | | 发送给授权服务的额外请求头,支持在值中使用 [NGINX 变量](https://nginx.org/en/docs/http/ngx_http_core_module.html)。 |
| timeout | integer | 否 | 3000 | 1 到 60000 之间(含) | 外部授权服务 HTTP 调用的超时时间(毫秒)。 |
| keepalive | boolean | 否 | true | | 如果为 true,则保持连接以处理多个请求。 |
Expand All @@ -67,6 +68,8 @@ import TabItem from '@theme/TabItem';

以下示例演示了如何针对不同场景使用 `forward-auth`。

如果配置了 `consumer_header`,并且认证服务返回了 2xx 响应,APISIX 会把该响应头的值当作 Consumer 用户名,从本地已有的 Consumer 配置中查找并绑定到当前请求。这样 Consumer 和 Consumer Group 上配置的插件也会对该请求生效。如果响应头缺失,或 APISIX 中不存在对应 Consumer,请求会被拒绝并返回 HTTP `403`。

:::note

你可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量:
Expand Down Expand Up @@ -506,6 +509,52 @@ curl "http://127.0.0.1:9080/headers" -H 'Authorization: 321'
}
```

### 通过认证响应绑定 APISIX Consumer

先在 APISIX 中创建一个已存在的 Consumer:

```shell
curl -X PUT 'http://127.0.0.1:9180/apisix/admin/consumers' \
-H "X-API-KEY: ${admin_key}" \
-d '{
"username": "demo-consumer",
"plugins": {
"limit-count": {
"count": 1,
"time_window": 60,
"rejected_code": 429,
"key": "remote_addr",
"policy": "local"
}
}
}'
```

然后让认证服务在成功响应中返回类似 `X-Consumer-Username` 的响应头,并在插件中声明该响应头:

```shell
curl -X PUT 'http://127.0.0.1:9180/apisix/admin/routes/consumer-route' \
-H "X-API-KEY: ${admin_key}" \
-d '{
"uri": "/consumer-route",
"plugins": {
"forward-auth": {
"uri": "http://127.0.0.1:9080/auth",
"request_headers": ["Authorization"],
"consumer_header": "X-Consumer-Username"
}
},
"upstream": {
"nodes": {
"httpbin.org:80": 1
},
"type": "roundrobin"
}
}'
```

在这个配置下,如果认证服务成功返回 `X-Consumer-Username: demo-consumer`,APISIX 就会把 `demo-consumer` 绑定到当前请求,并继续执行该 Consumer 上配置的插件,例如上面的 `limit-count`。

### 认证失败时向客户端返回指定请求头

以下示例演示了如何在路由上配置 `forward-auth`,控制客户端对上游资源的访问,并在认证失败时将授权服务返回的特定请求头传递给客户端。
Expand Down
Loading
Loading