Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add forward-auth plugin #6037

Merged
merged 40 commits into from
Jan 11, 2022
Merged

Conversation

bzp2010
Copy link
Contributor

@bzp2010 bzp2010 commented Jan 6, 2022

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
end

local params = {
method = "GET",
Copy link
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
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
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
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
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
Contributor Author

Choose a reason for hiding this comment

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

removed

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
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
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
Member

Choose a reason for hiding this comment

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

I read through each branch of https://github.com/api7/lua-resty-http/blob/b63a9bbf2a3361836b2322e3c689d6d7fd83ec55/lib/resty/http.lua#L900, look like there are only two cases:

  1. nil, err
  2. res, nil, while res is a table

Copy link
Contributor Author

@bzp2010 bzp2010 Jan 7, 2022

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
Contributor Author

Choose a reason for hiding this comment

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

changed

Copy link
Contributor Author

@bzp2010 bzp2010 Jan 7, 2022

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
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
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
Contributor Author

Choose a reason for hiding this comment

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

changed

@@ -278,6 +278,15 @@ function _M.get_path(ctx)
end


function _M.get_uri(ctx)
Copy link
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.

Copy link
Contributor Author

@bzp2010 bzp2010 Jan 7, 2022

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
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
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.

Copy link
Contributor Author

@bzp2010 bzp2010 Jan 7, 2022

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.

type = "array",
default = {},
items = {type = "string"},
description = "client request header that will be sent to the authorization"
Copy link
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
Contributor Author

Choose a reason for hiding this comment

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

changed

default = {},
items = {type = "string"},
description = "authorization response header that will be sent to"
.. "the client when authorize failure"
Copy link
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
Contributor Author

Choose a reason for hiding this comment

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

changed

-- 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
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

Copy link
Contributor Author

@bzp2010 bzp2010 Jan 7, 2022

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

auth_headers[header] = core.request.header(ctx, header)
end
else
auth_headers = core.table.merge(core.request.headers(), auth_headers)
Copy link
Member

Choose a reason for hiding this comment

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

Ditto

Copy link
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.

}
```

3. **client_headers** Send `authorization` service response header to `client` when authorize failure
Copy link
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
Contributor Author

Choose a reason for hiding this comment

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

changed

"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
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
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.

GET /hello
--- error_code: 403
--- response_headers
Location: http://example.com/auth
Copy link
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
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.

| -- | -- | -- | -- | -- | -- |
| 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
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
Contributor Author

Choose a reason for hiding this comment

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

added

| 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
Member

Choose a reason for hiding this comment

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

Ditto

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added

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

Copy link
Contributor Author

@bzp2010 bzp2010 Jan 10, 2022

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
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

Copy link
Contributor Author

@bzp2010 bzp2010 Jan 10, 2022

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
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
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.

client_headers[header] = res.headers[header]
end
else
client_headers = res.headers
Copy link
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.

Copy link
Contributor Author

@bzp2010 bzp2010 Jan 10, 2022

Choose a reason for hiding this comment

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

Ditto, changed

| -- | -- | -- | -- | -- | -- |
| 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
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
Contributor Author

Choose a reason for hiding this comment

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

updated

"serverless-pre-function": {
"phase": "rewrite",
"functions": [
"return function(conf, ctx)
Copy link
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

Copy link
Contributor Author

@bzp2010 bzp2010 Jan 10, 2022

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

| -- | -- | -- | -- | -- | -- |
| 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
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

Copy link
Contributor Author

@bzp2010 bzp2010 Jan 11, 2022

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

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