Skip to content

Commit 703c48c

Browse files
Xin Lipanxiaoquan
authored andcommitted
feat(forward-auth): support attaching consumer by response header
1 parent e4de423 commit 703c48c

6 files changed

Lines changed: 355 additions & 27 deletions

File tree

.luarc.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json",
3+
"runtime": {
4+
"version": "LuaJIT",
5+
"path": [
6+
"?.lua",
7+
"?/init.lua",
8+
"apisix/?.lua",
9+
"apisix/?/init.lua",
10+
"t/lib/?.lua",
11+
"t/lib/?/init.lua"
12+
]
13+
},
14+
"diagnostics": {
15+
"globals": [
16+
"ngx",
17+
"jit",
18+
"arg",
19+
"lua_load"
20+
]
21+
},
22+
"workspace": {
23+
"checkThirdParty": false
24+
}
25+
}

apisix/consumer.lua

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,30 @@ local function get_anonymous_consumer_from_local_cache(name)
339339
end
340340

341341

342+
local function get_consumer_from_local_cache(name)
343+
local consumer_raw = consumers:get(name)
344+
345+
if not consumer_raw or not consumer_raw.value or
346+
not consumer_raw.value.id or not consumer_raw.modifiedIndex then
347+
return nil, nil, "failed to get consumer " .. name
348+
end
349+
350+
local consumer = consumer_raw.value
351+
consumer.consumer_name = consumer_raw.value.id
352+
consumer.modifiedIndex = consumer_raw.modifiedIndex
353+
354+
if consumer.labels then
355+
consumer.custom_id = consumer.labels["custom_id"]
356+
end
357+
358+
local consumer_conf = {
359+
conf_version = consumer_raw.modifiedIndex
360+
}
361+
362+
return consumer, consumer_conf
363+
end
364+
365+
342366
function _M.get_anonymous_consumer(name)
343367
local anon_consumer, anon_consumer_conf, err
344368
anon_consumer, anon_consumer_conf, err = get_anonymous_consumer_from_local_cache(name)
@@ -347,4 +371,12 @@ function _M.get_anonymous_consumer(name)
347371
end
348372

349373

374+
function _M.get_consumer_by_name(name)
375+
local consumer, consumer_conf, err
376+
consumer, consumer_conf, err = get_consumer_from_local_cache(name)
377+
378+
return consumer, consumer_conf, err
379+
end
380+
381+
350382
return _M

apisix/plugins/forward-auth.lua

Lines changed: 72 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,30 +18,31 @@
1818
local ipairs = ipairs
1919
local core = require("apisix.core")
2020
local http = require("resty.http")
21+
local consumer = require("apisix.consumer")
2122
local pairs = pairs
2223
local type = type
2324
local tostring = tostring
2425

25-
local schema = {
26+
local schema = {
2627
type = "object",
2728
properties = {
28-
uri = {type = "string"},
29-
allow_degradation = {type = "boolean", default = false},
30-
status_on_error = {type = "integer", minimum = 200, maximum = 599, default = 403},
29+
uri = { type = "string" },
30+
allow_degradation = { type = "boolean", default = false },
31+
status_on_error = { type = "integer", minimum = 200, maximum = 599, default = 403 },
3132
ssl_verify = {
3233
type = "boolean",
3334
default = true,
3435
},
3536
request_method = {
3637
type = "string",
3738
default = "GET",
38-
enum = {"GET", "POST"},
39+
enum = { "GET", "POST" },
3940
description = "the method for client to request the authorization service"
4041
},
4142
request_headers = {
4243
type = "array",
4344
default = {},
44-
items = {type = "string"},
45+
items = { type = "string" },
4546
description = "client request header that will be sent to the authorization service"
4647
},
4748
extra_headers = {
@@ -51,25 +52,31 @@ local schema = {
5152
["^[^:]+$"] = {
5253
type = "string",
5354
description = "header value as a string; may contain variables"
54-
.. "like $remote_addr, $request_uri"
55+
.. "like $remote_addr, $request_uri"
5556
}
5657
},
5758
description = "extra headers sent to the authorization service; "
58-
.. "values must be strings and can include variables"
59-
.. "like $remote_addr, $request_uri."
59+
.. "values must be strings and can include variables"
60+
.. "like $remote_addr, $request_uri."
6061
},
6162
upstream_headers = {
6263
type = "array",
6364
default = {},
64-
items = {type = "string"},
65+
items = { type = "string" },
6566
description = "authorization response header that will be sent to the upstream"
6667
},
6768
client_headers = {
6869
type = "array",
6970
default = {},
70-
items = {type = "string"},
71+
items = { type = "string" },
7172
description = "authorization response header that will be sent to"
72-
.. "the client when authorizing failed"
73+
.. "the client when authorizing failed"
74+
},
75+
consumer_header = {
76+
type = "string",
77+
minLength = 1,
78+
description = "authorization response header that contains the "
79+
.. "APISIX Consumer username to attach to the request"
7380
},
7481
timeout = {
7582
type = "integer",
@@ -78,11 +85,11 @@ local schema = {
7885
default = 3000,
7986
description = "timeout in milliseconds",
8087
},
81-
keepalive = {type = "boolean", default = true},
82-
keepalive_timeout = {type = "integer", minimum = 1000, default = 60000},
83-
keepalive_pool = {type = "integer", minimum = 1, default = 5},
88+
keepalive = { type = "boolean", default = true },
89+
keepalive_timeout = { type = "integer", minimum = 1000, default = 60000 },
90+
keepalive_pool = { type = "integer", minimum = 1, default = 5 },
8491
},
85-
required = {"uri"}
92+
required = { "uri" }
8693
}
8794

8895

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

96103

97104
function _M.check_schema(conf)
98-
local check = {"uri"}
105+
local check = { "uri" }
99106
core.utils.check_https(check, conf, _M.name)
100-
core.utils.check_tls_bool({"ssl_verify"}, conf, _M.name)
107+
core.utils.check_tls_bool({ "ssl_verify" }, conf, _M.name)
101108

102109
return core.schema.check(schema, conf)
103110
end
104111

112+
local function attach_consumer_by_header(conf, ctx, res)
113+
if not conf.consumer_header then
114+
return
115+
end
116+
117+
local consumer_name = res.headers[conf.consumer_header]
118+
if type(consumer_name) == "table" then
119+
consumer_name = consumer_name[1]
120+
end
121+
122+
if not consumer_name or consumer_name == "" then
123+
core.log.error("missing consumer header from auth response: ", conf.consumer_header)
124+
return 403, "consumer header missing in auth response"
125+
end
126+
127+
local auth_consumer, consumer_conf, err = consumer.get_consumer_by_name(consumer_name)
128+
if not auth_consumer then
129+
core.log.error("failed to fetch consumer by name from auth response header ",
130+
conf.consumer_header, ": ", err)
131+
return 403, "consumer not found"
132+
end
133+
134+
consumer.attach_consumer(ctx, auth_consumer, consumer_conf)
135+
end
136+
137+
138+
local function do_auth(conf, ctx)
139+
ctx.forward_auth_processed = true
105140

106-
function _M.access(conf, ctx)
107141
local auth_headers = {
108142
["X-Forwarded-Proto"] = core.request.get_scheme(ctx),
109143
["X-Forwarded-Method"] = core.request.get_method(),
@@ -130,7 +164,7 @@ function _M.access(conf, ctx)
130164
end
131165
if err then
132166
core.log.error("failed to resolve variable in extra header '",
133-
header, "': ",value,": ",err)
167+
header, "': ", value, ": ", err)
134168
end
135169
end
136170
end
@@ -184,8 +218,12 @@ function _M.access(conf, ctx)
184218
return res.status, res.body
185219
end
186220

187-
-- set headers from the auth response, clearing any client-supplied values
188-
-- for configured headers not present in the auth response
221+
local code, body = attach_consumer_by_header(conf, ctx, res)
222+
if code or body then
223+
return code, body
224+
end
225+
226+
-- append headers that need to be get from the auth response header
189227
for _, header in ipairs(conf.upstream_headers) do
190228
local header_value = res.headers[header]
191229
-- if header_value is nil, the client header's value will be removed if it exists
@@ -194,4 +232,16 @@ function _M.access(conf, ctx)
194232
end
195233

196234

235+
function _M.rewrite(conf, ctx)
236+
return do_auth(conf, ctx)
237+
end
238+
239+
function _M.access(conf, ctx)
240+
if ctx.forward_auth_processed then
241+
return
242+
end
243+
244+
return do_auth(conf, ctx)
245+
end
246+
197247
return _M

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ This Plugin moves the authentication and authorization logic to a dedicated exte
4545
| 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` |
4646
| 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. |
4747
| 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. |
48+
| consumer_header | string | False | | | 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. |
4849
| timeout | integer | False | 3000ms | [1, 60000]ms | Timeout for the authorization service HTTP call. |
4950
| keepalive | boolean | False | true | | When set to `true`, keeps the connection alive for multiple requests. |
5051
| keepalive_timeout | integer | False | 60000ms | [1000, ...]ms | Idle time after which the connection is closed. |
@@ -60,6 +61,8 @@ APISIX will generate and send the request headers listed below to the authorizat
6061
| ----------------- | ------------------ | ---------------- | --------------- | --------------- |
6162
| X-Forwarded-Proto | X-Forwarded-Method | X-Forwarded-Host | X-Forwarded-Uri | X-Forwarded-For |
6263

64+
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`.
65+
6366
## Example usage
6467

6568
First, you need to setup your external authorization service. The example below uses Apache APISIX's [serverless](./serverless.md) Plugin to mock the service:
@@ -167,6 +170,52 @@ HTTP/1.1 403 Forbidden
167170
Location: http://example.com/auth
168171
```
169172

173+
### Attach an APISIX Consumer from the authorization response
174+
175+
Create a Consumer that already exists in APISIX:
176+
177+
```shell
178+
curl -X PUT 'http://127.0.0.1:9180/apisix/admin/consumers' \
179+
-H "X-API-KEY: $admin_key" \
180+
-d '{
181+
"username": "demo-consumer",
182+
"plugins": {
183+
"limit-count": {
184+
"count": 1,
185+
"time_window": 60,
186+
"rejected_code": 429,
187+
"key": "remote_addr",
188+
"policy": "local"
189+
}
190+
}
191+
}'
192+
```
193+
194+
Have the authorization service return the Consumer username in a response header such as `X-Consumer-Username`, and configure the plugin to read it:
195+
196+
```shell
197+
curl -X PUT 'http://127.0.0.1:9180/apisix/admin/routes/consumer-route' \
198+
-H "X-API-KEY: $admin_key" \
199+
-d '{
200+
"uri": "/consumer-route",
201+
"plugins": {
202+
"forward-auth": {
203+
"uri": "http://127.0.0.1:9080/auth",
204+
"request_headers": ["Authorization"],
205+
"consumer_header": "X-Consumer-Username"
206+
}
207+
},
208+
"upstream": {
209+
"nodes": {
210+
"httpbin.org:80": 1
211+
},
212+
"type": "roundrobin"
213+
}
214+
}'
215+
```
216+
217+
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.
218+
170219
### Using data from POST body to make decision on Authorization service
171220

172221
::: note

docs/zh/latest/plugins/forward-auth.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ description: 本文介绍了关于 Apache APISIX `forward-auth` 插件的基本
4444
| extra_headers |object | False | | | 以键值格式传递给授权服务的额外标头。值可以是变量,例如“$request_uri”或“$post_arg.xyz”。 |
4545
| upstream_headers | array[string] || | | 认证通过时,设置 `authorization` 服务转发至 `upstream` 的请求头。如果不设置则不转发任何请求头。 |
4646
| client_headers | array[string] || | | 认证失败时,由 `authorization` 服务向 `client` 发送的响应头。如果不设置则不转发任何响应头。 |
47+
| consumer_header | string || | | 认证服务响应头中的 Consumer 用户名字段。配置后,当认证服务返回 2xx 响应时,APISIX 会用该响应头中的值绑定已存在的 APISIX Consumer。 |
4748
| timeout | integer || 3000ms | [1, 60000]ms | `authorization` 服务请求超时时间。 |
4849
| keepalive | boolean || true | [true, false] | HTTP 长连接。 |
4950
| keepalive_timeout | integer || 60000ms | [1000, ...]ms | 长连接超时时间。 |
@@ -59,6 +60,8 @@ APISIX 将生成并发送如下所示的请求头到认证服务:
5960
| ----------------- | ------------------ | ----------------- | --------------- | --------------- |
6061
| X-Forwarded-Proto | X-Forwarded-Method | X-Forwarded-Host | X-Forwarded-Uri | X-Forwarded-For |
6162

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

6467
首先,你需要设置一个外部认证服务。以下示例使用的是 Apache APISIX 无服务器插件模拟服务:
@@ -169,6 +172,52 @@ HTTP/1.1 403 Forbidden
169172
Location: http://example.com/auth
170173
```
171174

175+
### 通过认证响应绑定 APISIX Consumer
176+
177+
先在 APISIX 中创建一个已存在的 Consumer:
178+
179+
```shell
180+
curl -X PUT 'http://127.0.0.1:9180/apisix/admin/consumers' \
181+
-H "X-API-KEY: $admin_key" \
182+
-d '{
183+
"username": "demo-consumer",
184+
"plugins": {
185+
"limit-count": {
186+
"count": 1,
187+
"time_window": 60,
188+
"rejected_code": 429,
189+
"key": "remote_addr",
190+
"policy": "local"
191+
}
192+
}
193+
}'
194+
```
195+
196+
然后让认证服务在成功响应中返回类似 `X-Consumer-Username` 的响应头,并在插件中声明该响应头:
197+
198+
```shell
199+
curl -X PUT 'http://127.0.0.1:9180/apisix/admin/routes/consumer-route' \
200+
-H "X-API-KEY: $admin_key" \
201+
-d '{
202+
"uri": "/consumer-route",
203+
"plugins": {
204+
"forward-auth": {
205+
"uri": "http://127.0.0.1:9080/auth",
206+
"request_headers": ["Authorization"],
207+
"consumer_header": "X-Consumer-Username"
208+
}
209+
},
210+
"upstream": {
211+
"nodes": {
212+
"httpbin.org:80": 1
213+
},
214+
"type": "roundrobin"
215+
}
216+
}'
217+
```
218+
219+
在这个配置下,如果认证服务成功返回 `X-Consumer-Username: demo-consumer`,APISIX 就会把 `demo-consumer` 绑定到当前请求,并继续执行该 Consumer 上配置的插件,例如上面的 `limit-count`
220+
172221
### Using data from POST body to make decision on Authorization service
173222

174223
::: note

0 commit comments

Comments
 (0)