Skip to content

feat: add forward-auth plugin#6037

Merged
spacewander merged 40 commits into
apache:masterfrom
bzp2010:feat-forward-auth
Jan 11, 2022
Merged

feat: add forward-auth plugin#6037
spacewander merged 40 commits into
apache:masterfrom
bzp2010:feat-forward-auth

Conversation

@bzp2010

@bzp2010 bzp2010 commented Jan 6, 2022

Copy link
Copy Markdown
Contributor

What this PR does / why we need it:

Add the forward-auth plugin.

Fix #6007
Fix #5475

Pre-submission checklist:

  • Did you explain what problem does this PR solve? Or what new features have been added?
  • Have you added corresponding test cases?
  • Have you modified the corresponding document?
  • Is this PR backward compatible? If it is not backward compatible, please discuss on the mailing list first

@bzp2010 bzp2010 added enhancement New feature or request plugin labels Jan 6, 2022
@bzp2010 bzp2010 self-assigned this Jan 6, 2022
Comment thread apisix/plugins/forward-auth.lua Outdated
end

local params = {
method = "GET",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the request method be mapped to the client request method?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's needed unless we provide other special features for this, the implementation in other software is the same.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In our or other forward-auth plugins, authentication-related data is sent via fixed requests, not POST data, so I think that's enough too.

ping @shuaijinchao

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, use GET as a fixed request method you should remove it because the default request method is GET

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, use GET as a fixed request method you should remove it because the default request method is GET

Oh, I get it. Modify will be made later.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

Comment thread apisix/plugins/forward-auth.lua Outdated
Comment on lines +113 to +116
if not res or err then
core.log.error("failed to process forward auth, err: ", err)
return 403
end

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if not res or err then
core.log.error("failed to process forward auth, err: ", err)
return 403
end
if not res then
core.log.error("failed to process forward auth, err: ", err)
return 403
end

Would it be better to judge this way?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand how to do it really right, I noticed that in other plugin implementations there are those that use not res alone, those that use err alone and those that use not res or err, which one should we take?

local res, error = httpc:request_uri(conf.discovery, params)
if not res then
err = "Accessing discovery URL (" .. conf.discovery .. ") failed: " .. error

local res, err = httpc:request_uri(conf.function_uri, params)
if not res or err then
core.log.error("failed to process ", plugin_name, ", err: ", err)

local res, err = httpc:request_uri(uri, params)
if err then
core.log.error("FAIL REQUEST [ ",core.json.delay_encode(

I hope to get your guidance.
cc @spacewander

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bzp2010 bzp2010 Jan 7, 2022

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Referring to the way it is usually used, I will check if err is not nil.

if err then
    xxx
end

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed

@bzp2010 bzp2010 Jan 7, 2022

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I think there needs to make a rules that developers should follow to handle error checking (at least on the http client), the mix of different usages is confusing.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, we have to enforce it with code review. Maybe we can invest in linter in the future.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be used

if res then
     err
end

instead of

if err then
     err
end

this way of writing may be more suitable for languages such as Go

the common exception function return in OpenResty is return nil, err, the first value is enough to know whether to handle err, and you can refer to more code and test cases in lua-resty-core, such as: https://github.com/openresty/lua-resty-core/blob/e5217414669c100b334940b250d5340911a02dd0/t/ctx.t#L133-L143

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed

Comment thread apisix/core/request.lua Outdated
end


function _M.get_uri(ctx)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not add a common method that is only used by a place. Actually, I think we should remove the get_path too.

It is strange that get_path fetches $uri but get_uri fetches $request_uri. Look like it is a premature optimization that brings inconsistent names.

@bzp2010 bzp2010 Jan 7, 2022

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I should just use ctx.var.request_uri instead of wrapping it? Can I merge the functionality of get_path into get_uri and add a with_args parameter indicating whether or not I need to carry queries.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't make thing complex!
The name of request_uri and uri already show the difference. There is no need to wrap them into a function which is rarely used. Premature optimization is the source of evil.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can remove the get_path in the next PR. Better to get rid of it before the release, so that people won't ask why we use get_path in one place but use ctx.var.uri in the most elsewhere.

@bzp2010 bzp2010 Jan 7, 2022

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed, I will remove get_path in a later PR.

Comment thread apisix/plugins/forward-auth.lua Outdated
type = "array",
default = {},
items = {type = "string"},
description = "client request header that will be sent to the authorization"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
description = "client request header that will be sent to the authorization"
description = "client request header that will be sent to the authorization service"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed

Comment thread apisix/plugins/forward-auth.lua Outdated
default = {},
items = {type = "string"},
description = "authorization response header that will be sent to"
.. "the client when authorize failure"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.. "the client when authorize failure"
.. "the client when authorizing failed"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed

Comment thread apisix/plugins/forward-auth.lua Outdated
-- 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
auth_headers[header] = core.request.header(ctx, header)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better not trust the client's X-Forwarded-XX by default

@bzp2010 bzp2010 Jan 7, 2022

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed, because auth_headers has been set in our concern headers, after the judgment, the request header from the client can not override them, that's not trust the client header. L89-L91

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"] = core.request.get_uri(ctx),
["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
else

Comment thread apisix/plugins/forward-auth.lua Outdated
auth_headers[header] = core.request.header(ctx, header)
end
else
auth_headers = core.table.merge(core.request.headers(), auth_headers)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because, merge the list of defined headers into the client request header, any key headers passed in by the client are overwritten, so I implement the untrusted client header.

Comment thread docs/en/latest/plugins/forward-auth.md Outdated
}
```

3. **client_headers** Send `authorization` service response header to `client` when authorize failure

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
3. **client_headers** Send `authorization` service response header to `client` when authorize failure
3. **client_headers** Send `authorization` service response header to `client` when authorizing failed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed

Comment thread t/plugin/forward-auth.t Outdated
"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-an-user\"); core.response.exit(200); else core.response.set_header(\"Location\", \"http://example.com/auth\"); core.response.exit(403); end end"]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's log down the X-Forwarded-XX received by the service. And better to break down the very long code.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used another method to record these heads. I also added a simple echo function under /auth route.

Comment thread t/plugin/forward-auth.t
GET /hello
--- error_code: 403
--- response_headers
Location: http://example.com/auth

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a test that the client passes X-Forwarded-XX by itself

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added, I added a test case to demonstrate that these client-side incoming key headers are indeed dropped in APISIX.

Comment thread docs/en/latest/plugins/forward-auth.md Outdated
| -- | -- | -- | -- | -- | -- |
| 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 |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's doc the behavior when this field is empty

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

Comment thread docs/en/latest/plugins/forward-auth.md Outdated
| 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 |
| upstream_headers | array[string] | optional | | | `authorization` service response header that will be sent to the `upstream` |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

Comment thread t/plugin/forward-auth.t Outdated
"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",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's indent the code. Like:

diff --git t/plugin/forward-auth.t t/plugin/forward-auth.t
index dca35b76..285e60fb 100644
--- t/plugin/forward-auth.t
+++ t/plugin/forward-auth.t
@@ -74,7 +74,12 @@ property "request_headers" validation failed: wrong type: expected array, got st
                             "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\") == \"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"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be better to log down the "X-Forwarded-XX" headers and check it

@bzp2010 bzp2010 Jan 10, 2022

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Log check for X-Forwarded-XX, I will add it later.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Splitting the single line long code is done, which uses ]]..[[ truncates the data code, the code is so long that it cannot be parsed, so I modified it by referring to the explanation in this issue.

openresty/lua-nginx-module#1622

@bzp2010 bzp2010 Jan 10, 2022

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Log check for X-Forwarded-XX, I will add it later.

In the session of adding more cases, I found out that we won't actually use it, and that the function to print the request header is already implemented using /echo.

},
request_headers = {
type = "array",
default = {},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Look like we can't support forward no headers from the client?
If this field is empty, all headers are forwarded. What about don't provide a default value? (Use empty array if no headers are need)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's rightfully so, and that's an oversight on my part. I have reversed the meaning of request_headers and client_headers, if the user does not set this parameter, APISIX will send nothing.

Comment thread apisix/plugins/forward-auth.lua Outdated
client_headers[header] = res.headers[header]
end
else
client_headers = res.headers

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO, would be better to use allowlist? If no client_headers, no headers will be returned to the client. Some headers are not expected to be overridden, like Server / Date.

@bzp2010 bzp2010 Jan 10, 2022

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto, changed

@bzp2010
bzp2010 requested a review from spacewander January 10, 2022 06:54
Comment thread docs/en/latest/plugins/forward-auth.md Outdated
| -- | -- | -- | -- | -- | -- |
| 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, all `client` request headers are sent to the `authorization` service, except for those provided by APISIX (X-Forwarded-XXX). |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to update the doc

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

Comment thread t/plugin/forward-auth.t
"serverless-pre-function": {
"phase": "rewrite",
"functions": [
"return function(conf, ctx)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be better to log down the APISIX generated "X-Forwarded-XX" headers and check it

@bzp2010 bzp2010 Jan 10, 2022

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've merged it with the checkcase for overriding user input requests, which will check all APISIX generated headers at once. TEST 6: hit route (check APISIX generated headers and ignore client headers), It is provided by our echo function.

--- 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\"/

@bzp2010
bzp2010 requested a review from spacewander January 10, 2022 09:07
| -- | -- | -- | -- | -- | -- |
| 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). |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's provide a list of the X-Forwarded-XXX headers

@bzp2010 bzp2010 Jan 11, 2022

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the Data Definition section added

@bzp2010
bzp2010 requested a review from spacewander January 11, 2022 01:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request plugin

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Proposal: support Forward Auth authenticate plugin request help: Support forward-auth

3 participants