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/01/11 02:38:43 UTC
[apisix] branch master updated: feat: add forward-auth plugin (#6037)
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 8dbdd1f feat: add forward-auth plugin (#6037)
8dbdd1f is described below
commit 8dbdd1f68812482004509085e66987aafff07f6e
Author: Zeping Bai <bz...@apache.org>
AuthorDate: Tue Jan 11 10:38:35 2022 +0800
feat: add forward-auth plugin (#6037)
Co-authored-by: 罗泽轩 <sp...@gmail.com>
---
apisix/plugins/forward-auth.lua | 140 +++++++++++++++++++
conf/config-default.yaml | 1 +
docs/en/latest/config.json | 3 +-
docs/en/latest/plugins/forward-auth.md | 139 ++++++++++++++++++
t/admin/plugins.t | 1 +
t/plugin/forward-auth.t | 248 +++++++++++++++++++++++++++++++++
6 files changed, 531 insertions(+), 1 deletion(-)
diff --git a/apisix/plugins/forward-auth.lua b/apisix/plugins/forward-auth.lua
new file mode 100644
index 0000000..ed3baef
--- /dev/null
+++ b/apisix/plugins/forward-auth.lua
@@ -0,0 +1,140 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements. See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License. You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+local ipairs = ipairs
+local core = require("apisix.core")
+local http = require("resty.http")
+
+local schema = {
+ type = "object",
+ properties = {
+ host = {type = "string"},
+ ssl_verify = {
+ type = "boolean",
+ default = true,
+ },
+ request_headers = {
+ type = "array",
+ default = {},
+ items = {type = "string"},
+ description = "client request header that will be sent to the authorization service"
+ },
+ upstream_headers = {
+ type = "array",
+ default = {},
+ items = {type = "string"},
+ description = "authorization response header that will be sent to the upstream"
+ },
+ client_headers = {
+ type = "array",
+ default = {},
+ items = {type = "string"},
+ description = "authorization response header that will be sent to"
+ .. "the client when authorizing failed"
+ },
+ timeout = {
+ type = "integer",
+ minimum = 1,
+ maximum = 60000,
+ 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},
+ },
+ required = {"host"}
+}
+
+
+local _M = {
+ version = 0.1,
+ priority = 2002,
+ name = "forward-auth",
+ schema = schema,
+}
+
+
+function _M.check_schema(conf)
+ return core.schema.check(schema, conf)
+end
+
+
+function _M.access(conf, ctx)
+ local auth_headers = {
+ ["X-Forwarded-Proto"] = core.request.get_scheme(ctx),
+ ["X-Forwarded-Method"] = core.request.get_method(),
+ ["X-Forwarded-Host"] = core.request.get_host(ctx),
+ ["X-Forwarded-Uri"] = ctx.var.request_uri,
+ ["X-Forwarded-For"] = core.request.get_remote_client_ip(ctx),
+ }
+
+ -- 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
+ if not auth_headers[header] then
+ auth_headers[header] = core.request.header(ctx, header)
+ end
+ end
+ end
+
+ local params = {
+ headers = auth_headers,
+ keepalive = conf.keepalive,
+ ssl_verify = conf.ssl_verify
+ }
+
+ if conf.keepalive then
+ params.keepalive_timeout = conf.keepalive_timeout
+ params.keepalive_pool = conf.keepalive_pool
+ end
+
+ local httpc = http.new()
+ httpc:set_timeout(conf.timeout)
+
+ local res, err = httpc:request_uri(conf.host, params)
+
+ -- block by default when authorization service is unavailable
+ if not res then
+ core.log.error("failed to process forward auth, err: ", err)
+ return 403
+ end
+
+ if res.status >= 300 then
+ local client_headers = {}
+
+ if #conf.client_headers > 0 then
+ for _, header in ipairs(conf.client_headers) do
+ client_headers[header] = res.headers[header]
+ end
+ end
+
+ core.response.set_header(client_headers)
+ return res.status, res.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 then
+ core.request.set_header(ctx, header, header_value)
+ end
+ end
+end
+
+
+return _M
diff --git a/conf/config-default.yaml b/conf/config-default.yaml
index e1ae179..e2757d8 100644
--- a/conf/config-default.yaml
+++ b/conf/config-default.yaml
@@ -344,6 +344,7 @@ plugins: # plugin list (sorted by priority)
- jwt-auth # priority: 2510
- key-auth # priority: 2500
- consumer-restriction # priority: 2400
+ - forward-auth # priority: 2002
- opa # priority: 2001
- authz-keycloak # priority: 2000
#- error-log-logger # priority: 1091
diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json
index 3f86445..93eba03 100644
--- a/docs/en/latest/config.json
+++ b/docs/en/latest/config.json
@@ -68,7 +68,8 @@
"plugins/hmac-auth",
"plugins/authz-casbin",
"plugins/ldap-auth",
- "plugins/opa"
+ "plugins/opa",
+ "plugins/forward-auth"
]
},
{
diff --git a/docs/en/latest/plugins/forward-auth.md b/docs/en/latest/plugins/forward-auth.md
new file mode 100644
index 0000000..6dad713
--- /dev/null
+++ b/docs/en/latest/plugins/forward-auth.md
@@ -0,0 +1,139 @@
+---
+title: forward-auth
+---
+
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+## Summary
+
+- [**Description**](#description)
+- [**Attributes**](#attributes)
+- [**Data Definition**](#data-definition)
+- [**Example**](#example)
+
+## Description
+
+The `forward-auth` plugin implements a classic external authentication model. We can implement a custom error return or user redirection to the authentication page if the authentication fails.
+
+Forward Auth cleverly moves the authentication and authorization logic to a dedicated external service, where the gateway forwards the user's request to the authentication service and blocks the original request, and replaces the result when the authentication service responds with a non-2xx status.
+
+## Attributes
+
+| Name | Type | Requirement | Default | Valid | Description |
+| -- | -- | -- | -- | -- | -- |
+| host | string | required | | | Authorization service host (eg. https://localhost:9188) |
+| ssl_verify | boolean | optional | true | | Whether to verify the certificate |
+| request_headers | array[string] | optional | | | `client` request header that will be sent to the `authorization` service. When it is not set, no `client` request headers are sent to the `authorization` service, except for those provided by APISIX (X-Forwarded-XXX). |
+| upstream_headers | array[string] | optional | | | `authorization` service response header that will be sent to the `upstream`. When it is not set, will not forward the `authorization` service response header to the `upstream`. |
+| client_headers | array[string] | optional | | | `authorization` response header that will be sent to the `client` when authorize failure. When it is not set, will not forward the `authorization` service response header to the `client`. |
+| timeout | integer | optional | 3000ms | [1, 60000]ms | Authorization service HTTP call timeout |
+| keepalive | boolean | optional | true | | HTTP keepalive |
+| keepalive_timeout | integer | optional | 60000ms | [1000, ...]ms | keepalive idle timeout |
+| keepalive_pool | integer | optional | 5 | [1, ...]ms | Connection pool limit |
+
+## Data Definition
+
+The request headers in the following list will have APISIX generated and sent to the `authorization` service.
+
+| Scheme | HTTP Method | Host | URI | Source IP |
+| -- | -- | -- | -- | -- |
+| X-Forwarded-Proto | X-Forwarded-Method | X-Forwarded-Host | X-Forwarded-Uri | X-Forwarded-For |
+
+## Example
+
+First, you need to setup an external authorization service. Here is an example of using Apache APISIX's serverless plugin to mock.
+
+```shell
+$ curl -X PUT 'http://127.0.0.1:9080/apisix/admin/routes/auth' \
+ -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' \
+ -H 'Content-Type: application/json' \
+ -d '{
+ "uri": "/auth",
+ "plugins": {
+ "serverless-pre-function": {
+ "phase": "rewrite",
+ "functions": [
+ "return function (conf, ctx) local core = require(\"apisix.core\"); local authorization = core.request.header(ctx, \"Authorization\"); if authorization == \"123\" then core.response.exit(200); elseif authorization == \"321\" then core.response.set_header(\"X-User-ID\", \"i-am-user\"); core.response.exit(200); else core.response.set_header(\"Location\", \"http://example.com/auth\"); core.response.exit(403); end end"
+ ]
+ }
+ }
+}'
+```
+
+Next, we create a route for testing.
+
+```shell
+$ curl -X PUT http://127.0.0.1:9080/apisix/admin/routes/1
+ -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1'
+ -d '{
+ "uri": "/headers",
+ "plugins": {
+ "forward-auth": {
+ "host": "http://127.0.0.1:9080/auth",
+ "request_headers": ["Authorization"],
+ "upstream_headers": ["X-User-ID"],
+ "client_headers": ["Location"]
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "httpbin.org:80": 1
+ },
+ "type": "roundrobin"
+ }
+}'
+```
+
+We can perform the following three tests.
+
+1. **request_headers** Send Authorization header from `client` to `authorization` service
+
+```shell
+$ curl http://127.0.0.1:9080/headers -H 'Authorization: 123'
+{
+ "headers": {
+ "Authorization": "123",
+ "Next": "More-headers"
+ }
+}
+```
+
+2. **upstream_headers** Send `authorization` service response header to the `upstream`
+
+```shell
+$ curl http://127.0.0.1:9080/headers -H 'Authorization: 321'
+{
+ "headers": {
+ "Authorization": "321",
+ "X-User-ID": "i-am-user",
+ "Next": "More-headers"
+ }
+}
+```
+
+3. **client_headers** Send `authorization` service response header to `client` when authorizing failed
+
+```shell
+$ curl -i http://127.0.0.1:9080/headers
+HTTP/1.1 403 Forbidden
+Location: http://example.com/auth
+```
+
+Finally, you can disable the `forward-auth` plugin by removing it from the route.
diff --git a/t/admin/plugins.t b/t/admin/plugins.t
index 29038ea..d203c37 100644
--- a/t/admin/plugins.t
+++ b/t/admin/plugins.t
@@ -83,6 +83,7 @@ basic-auth
jwt-auth
key-auth
consumer-restriction
+forward-auth
opa
authz-keycloak
proxy-mirror
diff --git a/t/plugin/forward-auth.t b/t/plugin/forward-auth.t
new file mode 100644
index 0000000..f379fff
--- /dev/null
+++ b/t/plugin/forward-auth.t
@@ -0,0 +1,248 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+use t::APISIX 'no_plan';
+
+repeat_each(1);
+no_long_string();
+no_root_location();
+
+add_block_preprocessor(sub {
+ my ($block) = @_;
+
+ if ((!defined $block->error_log) && (!defined $block->no_error_log)) {
+ $block->set_value("no_error_log", "[error]");
+ }
+
+ if (!defined $block->request) {
+ $block->set_value("request", "GET /t");
+ }
+});
+
+run_tests();
+
+__DATA__
+
+=== TEST 1: sanity
+--- config
+ location /t {
+ content_by_lua_block {
+ local test_cases = {
+ {host = "http://127.0.0.1:8199"},
+ {request_headers = {"test"}},
+ {host = 3233},
+ {host = "http://127.0.0.1:8199", request_headers = "test"}
+ }
+ local plugin = require("apisix.plugins.forward-auth")
+
+ for _, case in ipairs(test_cases) do
+ local ok, err = plugin.check_schema(case)
+ ngx.say(ok and "done" or err)
+ end
+ }
+ }
+--- response_body
+done
+property "host" is required
+property "host" validation failed: wrong type: expected string, got number
+property "request_headers" validation failed: wrong type: expected array, got string
+
+
+
+=== TEST 2: setup route with plugin
+--- config
+ location /t {
+ content_by_lua_block {
+ local datas = {
+ {
+ url = "/apisix/admin/upstreams/u1",
+ data = [[{
+ "nodes": {
+ "127.0.0.1:1984": 1
+ },
+ "type": "roundrobin"
+ }]],
+ },
+ {
+ url = "/apisix/admin/routes/auth",
+ data = [[{
+ "plugins": {
+ "serverless-pre-function": {
+ "phase": "rewrite",
+ "functions": [
+ "return function(conf, ctx)
+ local core = require(\"apisix.core\");
+ if core.request.header(ctx, \"Authorization\") == \"111\" then
+ core.response.exit(200);
+ end
+ end",
+ "return function(conf, ctx)
+ local core = require(\"apisix.core\");
+ if core.request.header(ctx, \"Authorization\") == \"222\" then
+ core.response.set_header(\"X-User-ID\", \"i-am-an-user\");
+ core.response.exit(200);
+ end
+ end",]] .. [[
+ "return function(conf, ctx)
+ local core = require(\"apisix.core\");
+ if core.request.header(ctx, \"Authorization\") == \"333\" then
+ core.response.set_header(\"Location\", \"http://example.com/auth\");
+ core.response.exit(403);
+ end
+ end",
+ "return function(conf, ctx)
+ local core = require(\"apisix.core\");
+ if core.request.header(ctx, \"Authorization\") == \"444\" then
+ core.response.exit(403, core.request.headers(ctx));
+ end
+ end"
+ ]
+ }
+ },
+ "uri": "/auth"
+ }]],
+ },
+ {
+ url = "/apisix/admin/routes/echo",
+ data = [[{
+ "plugins": {
+ "serverless-pre-function": {
+ "phase": "rewrite",
+ "functions": [
+ "return function (conf, ctx)
+ local core = require(\"apisix.core\");
+ core.response.exit(200, core.request.headers(ctx));
+ end"
+ ]
+ }
+ },
+ "uri": "/echo"
+ }]],
+ },
+ {
+ url = "/apisix/admin/routes/1",
+ data = [[{
+ "plugins": {
+ "forward-auth": {
+ "host": "http://127.0.0.1:1984/auth",
+ "request_headers": ["Authorization"],
+ "upstream_headers": ["X-User-ID"],
+ "client_headers": ["Location"]
+ },
+ "proxy-rewrite": {
+ "uri": "/echo"
+ }
+ },
+ "upstream_id": "u1",
+ "uri": "/hello"
+ }]],
+ },
+ {
+ url = "/apisix/admin/routes/2",
+ data = [[{
+ "plugins": {
+ "forward-auth": {
+ "host": "http://127.0.0.1:1984/auth",
+ "request_headers": ["Authorization"]
+ },
+ "proxy-rewrite": {
+ "uri": "/echo"
+ }
+ },
+ "upstream_id": "u1",
+ "uri": "/empty"
+ }]],
+ },
+ }
+
+ local t = require("lib.test_admin").test
+
+ for _, data in ipairs(datas) do
+ local code, body = t(data.url, ngx.HTTP_PUT, data.data)
+ ngx.say(code..body)
+ end
+ }
+ }
+--- response_body eval
+"201passed\n" x 5
+
+
+
+=== TEST 3: hit route (test request_headers)
+--- request
+GET /hello
+--- more_headers
+Authorization: 111
+--- response_body_like eval
+qr/\"authorization\":\"111\"/
+
+
+
+=== TEST 4: hit route (test upstream_headers)
+--- request
+GET /hello
+--- more_headers
+Authorization: 222
+--- response_body_like eval
+qr/\"x-user-id\":\"i-am-an-user\"/
+
+
+
+=== TEST 5: hit route (test client_headers)
+--- request
+GET /hello
+--- more_headers
+Authorization: 333
+--- error_code: 403
+--- response_headers
+Location: http://example.com/auth
+
+
+
+=== TEST 6: hit route (check APISIX generated headers and ignore client headers)
+--- request
+GET /hello
+--- more_headers
+Authorization: 444
+X-Forwarded-Host: apisix.apache.org
+--- error_code: 403
+--- response_body eval
+qr/\"x-forwarded-proto\":\"http\"/ and qr/\"x-forwarded-method\":\"GET\"/ and
+qr/\"x-forwarded-host\":\"localhost\"/ and qr/\"x-forwarded-uri\":\"\\\/hello\"/ and
+qr/\"x-forwarded-for\":\"127.0.0.1\"/
+--- response_body_unlike eval
+qr/\"x-forwarded-host\":\"apisix.apache.org\"/
+
+
+
+=== TEST 7: hit route (not send upstream headers)
+--- request
+GET /empty
+--- more_headers
+Authorization: 222
+--- response_body_unlike eval
+qr/\"x-user-id\":\"i-am-an-user\"/
+
+
+
+=== TEST 8: hit route (not send client headers)
+--- request
+GET /empty
+--- more_headers
+Authorization: 333
+--- error_code: 403
+--- response_headers
+!Location