Copilot commented on code in PR #13382:
URL: https://github.com/apache/apisix/pull/13382#discussion_r3255995791


##########
apisix/plugins/feishu-auth.lua:
##########
@@ -0,0 +1,291 @@
+--
+-- 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 core = require("apisix.core")
+local http = require("resty.http")
+local session = require("resty.session")
+
+local base64_encode = ngx.encode_base64
+local ngx_time = ngx.time
+local type = type
+
+local DEFAULT_TOKEN_URL = 
"https://open.feishu.cn/open-apis/authen/v2/oauth/token";
+local DEFAULT_USERINFO_URL = 
"https://open.feishu.cn/open-apis/authen/v1/user_info";
+
+local schema = {
+    type = "object",
+    properties = {
+        app_id = {type = "string", minLength = 1},
+        app_secret = {type = "string", minLength = 1},
+        code_header = {
+            type = "string",
+            description = "Header name to extract authorization code from.",
+            default = "X-Feishu-Code"
+        },
+        code_query = {
+            type = "string",
+            description = "Query parameter name to extract authorization code 
from.",
+            default = "code"
+        },
+        userinfo_url = {
+            type = "string",
+            default = DEFAULT_USERINFO_URL
+        },
+        access_token_url = {
+            type = "string",
+            default = DEFAULT_TOKEN_URL
+        },
+        set_userinfo_header = {
+            type = "boolean",
+            description = "Whether to set feishu user information in request 
headers",
+            default = true
+        },
+        auth_redirect_uri = {
+            type = "string",
+            description = "Redirect URI for initiating Feishu OAuth flow",
+        },
+        redirect_uri = {type = "string"},
+        timeout = {type = "integer", default = 6000},
+        ssl_verify = {type = "boolean", default = true},
+        secret = {
+            type = "string",
+            description = "Secret used for key derivation.",
+            minLength = 8,
+            maxLength = 32,
+        },
+        secret_fallbacks = {
+            type = "array",
+            items = {
+                type = "string",
+                minLength = 8,
+                maxLength = 32,
+            },
+            description = "List of secrets for alternative secrets used when 
doing key rotation"
+        },
+        cookie_expires_in = {
+            type = "integer",
+            description = "Valid duration (in seconds) for the authorization 
cookie."
+                        .. "This value defines how long the cookie remains 
valid after creation.",
+            default = 86400,
+        },
+
+    },
+    encrypt_fields = {"app_secret", "secret"},
+    required = {"app_id", "app_secret", "secret", "auth_redirect_uri", 
"redirect_uri"},
+}
+
+local _M = {
+    version = 0.1,
+    priority = 2420,
+    name = "feishu-auth",
+    schema = schema,
+}
+
+
+function _M.check_schema(conf)
+    return core.schema.check(schema, conf)
+end
+
+
+local function fetch_access_token(conf, code)
+    local httpc = http.new()
+    httpc:set_timeout(conf.timeout)
+
+    local body = {
+        grant_type = "authorization_code",
+        client_id = conf.app_id,
+        client_secret = conf.app_secret,
+        redirect_uri = conf.auth_redirect_uri,
+        code = code,
+    }
+
+    local res, err = httpc:request_uri(conf.access_token_url, {
+        method = "POST",
+        headers = {
+            ["Content-Type"] = "application/json"
+        },
+        body = core.json.encode(body),
+        ssl_verify = conf.ssl_verify
+    })
+
+    if not res then
+        core.log.error("failed to get feishu token: ", err)
+        return nil, nil, err
+    end
+
+    core.log.debug("request feishu access token response status: ",
+                                    res.status)
+
+    if res.status ~= 200 then
+        core.log.warn("unexpected http response status from feishu: ",
+                                        res.status, ", body: ", res.body)
+        return nil, nil, "unexpected response status: " .. res.status
+                                            .. ", body: " .. res.body
+    end
+
+    local data, err = core.json.decode(res.body)
+    if not data then
+        core.log.error("failed to decode feishu token response: ", err)
+        return nil, nil, "failed to decode response: " .. (err or "nil")
+    end
+
+    if not data.access_token or type(data.expires_in) ~= "number" then
+        core.log.error("feishu token response missing access_token or 
expires_in: ", res.body)
+        return nil, nil, "missing access_token or expires_in in response"
+    end
+
+    return data.access_token, data.expires_in, nil
+end
+
+
+local function fetch_userinfo(conf, access_token)
+    local httpc = http.new()
+    httpc:set_timeout(conf.timeout)
+
+    local res, err = httpc:request_uri(conf.userinfo_url, {
+        method = "GET",
+        headers = {
+            ["Content-Type"] = "application/json",
+            ["Authorization"] = "Bearer " .. access_token,
+        },
+        ssl_verify = conf.ssl_verify
+    })
+
+    if not res then
+        core.log.error("failed to verify feishu user: ", err)
+        return nil, err
+    end
+
+    core.log.debug("request feishu userinfo response status: ", res.status, ", 
body: ", res.body)
+
+    if res.status ~= 200 then
+        core.log.error("unexpected http response status from feishu: ",
+                            res.status, ", body: ", res.body)
+        return nil, "unexpected http response status: " .. res.status
+    end
+
+    local data, err = core.json.decode(res.body)
+    if not data then
+        core.log.error("failed to decode feishu userinfo response: ", err, ", 
body: ", res.body)
+        return nil, "failed to decode response: " .. err
+    end
+
+    if data.code ~= 0 then
+        core.log.warn("failed to get feishu userinfo: ", res.body)
+        return nil, "unexpected error code: " .. data.code
+                            .. ", errmsg: " .. (data.msg or "nil")
+    end
+
+    return data.data, nil
+end
+
+
+local function get_code(conf, ctx)
+    local code = core.request.header(ctx, conf.code_header)
+    if not code then
+        local uri_args = core.request.get_uri_args(ctx) or {}
+        code = uri_args[conf.code_query]
+    end
+

Review Comment:
   The authorization code is read from the client request but never removed 
from either the query string or request header before proxying. After a 
successful exchange, the upstream can receive and log the OAuth code via 
`code_query`/`code_header`; clear these credentials before forwarding the 
request.
   



##########
apisix/plugins/feishu-auth.lua:
##########
@@ -0,0 +1,291 @@
+--
+-- 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 core = require("apisix.core")
+local http = require("resty.http")
+local session = require("resty.session")
+
+local base64_encode = ngx.encode_base64
+local ngx_time = ngx.time
+local type = type
+
+local DEFAULT_TOKEN_URL = 
"https://open.feishu.cn/open-apis/authen/v2/oauth/token";
+local DEFAULT_USERINFO_URL = 
"https://open.feishu.cn/open-apis/authen/v1/user_info";
+
+local schema = {
+    type = "object",
+    properties = {
+        app_id = {type = "string", minLength = 1},
+        app_secret = {type = "string", minLength = 1},
+        code_header = {
+            type = "string",
+            description = "Header name to extract authorization code from.",
+            default = "X-Feishu-Code"
+        },
+        code_query = {
+            type = "string",
+            description = "Query parameter name to extract authorization code 
from.",
+            default = "code"
+        },
+        userinfo_url = {
+            type = "string",
+            default = DEFAULT_USERINFO_URL
+        },
+        access_token_url = {
+            type = "string",
+            default = DEFAULT_TOKEN_URL
+        },
+        set_userinfo_header = {
+            type = "boolean",
+            description = "Whether to set feishu user information in request 
headers",
+            default = true
+        },
+        auth_redirect_uri = {
+            type = "string",
+            description = "Redirect URI for initiating Feishu OAuth flow",
+        },
+        redirect_uri = {type = "string"},
+        timeout = {type = "integer", default = 6000},
+        ssl_verify = {type = "boolean", default = true},
+        secret = {
+            type = "string",
+            description = "Secret used for key derivation.",
+            minLength = 8,
+            maxLength = 32,
+        },
+        secret_fallbacks = {
+            type = "array",
+            items = {
+                type = "string",
+                minLength = 8,
+                maxLength = 32,
+            },
+            description = "List of secrets for alternative secrets used when 
doing key rotation"
+        },
+        cookie_expires_in = {
+            type = "integer",
+            description = "Valid duration (in seconds) for the authorization 
cookie."
+                        .. "This value defines how long the cookie remains 
valid after creation.",
+            default = 86400,
+        },
+
+    },
+    encrypt_fields = {"app_secret", "secret"},
+    required = {"app_id", "app_secret", "secret", "auth_redirect_uri", 
"redirect_uri"},
+}
+
+local _M = {
+    version = 0.1,
+    priority = 2420,
+    name = "feishu-auth",
+    schema = schema,
+}
+
+
+function _M.check_schema(conf)
+    return core.schema.check(schema, conf)
+end
+
+
+local function fetch_access_token(conf, code)
+    local httpc = http.new()
+    httpc:set_timeout(conf.timeout)
+
+    local body = {
+        grant_type = "authorization_code",
+        client_id = conf.app_id,
+        client_secret = conf.app_secret,
+        redirect_uri = conf.auth_redirect_uri,
+        code = code,
+    }
+
+    local res, err = httpc:request_uri(conf.access_token_url, {
+        method = "POST",
+        headers = {
+            ["Content-Type"] = "application/json"
+        },
+        body = core.json.encode(body),
+        ssl_verify = conf.ssl_verify
+    })
+
+    if not res then
+        core.log.error("failed to get feishu token: ", err)
+        return nil, nil, err
+    end
+
+    core.log.debug("request feishu access token response status: ",
+                                    res.status)
+
+    if res.status ~= 200 then
+        core.log.warn("unexpected http response status from feishu: ",
+                                        res.status, ", body: ", res.body)
+        return nil, nil, "unexpected response status: " .. res.status
+                                            .. ", body: " .. res.body
+    end
+
+    local data, err = core.json.decode(res.body)
+    if not data then
+        core.log.error("failed to decode feishu token response: ", err)
+        return nil, nil, "failed to decode response: " .. (err or "nil")
+    end
+
+    if not data.access_token or type(data.expires_in) ~= "number" then
+        core.log.error("feishu token response missing access_token or 
expires_in: ", res.body)
+        return nil, nil, "missing access_token or expires_in in response"
+    end
+
+    return data.access_token, data.expires_in, nil

Review Comment:
   The Feishu OAuth token API returns `access_token` and `expires_in` inside 
the response `data` object for `/authen/v2/oauth/token`, but this code checks 
for those fields at the top level. Real successful Feishu responses will be 
rejected as missing the token, so authentication will fail outside the mock 
test server.
   



##########
t/plugin/feishu-auth.t:
##########
@@ -0,0 +1,299 @@
+#
+# 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);
+log_level('warn');
+no_long_string();
+no_root_location();

Review Comment:
   These tests depend on state created by earlier blocks (for example, TEST 2 
uses the route configured in TEST 1 and TEST 10/11 depend on TEST 9), but the 
file does not call `no_shuffle()`. If Test::Nginx shuffles blocks, the suite 
can become order-dependent and fail or test the wrong route configuration; most 
stateful plugin suites in `t/plugin` disable shuffling.
   



##########
apisix/plugins/feishu-auth.lua:
##########
@@ -0,0 +1,291 @@
+--
+-- 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 core = require("apisix.core")
+local http = require("resty.http")
+local session = require("resty.session")
+
+local base64_encode = ngx.encode_base64
+local ngx_time = ngx.time
+local type = type
+
+local DEFAULT_TOKEN_URL = 
"https://open.feishu.cn/open-apis/authen/v2/oauth/token";
+local DEFAULT_USERINFO_URL = 
"https://open.feishu.cn/open-apis/authen/v1/user_info";
+
+local schema = {
+    type = "object",
+    properties = {
+        app_id = {type = "string", minLength = 1},
+        app_secret = {type = "string", minLength = 1},
+        code_header = {
+            type = "string",
+            description = "Header name to extract authorization code from.",
+            default = "X-Feishu-Code"
+        },
+        code_query = {
+            type = "string",
+            description = "Query parameter name to extract authorization code 
from.",
+            default = "code"
+        },
+        userinfo_url = {
+            type = "string",
+            default = DEFAULT_USERINFO_URL
+        },
+        access_token_url = {
+            type = "string",
+            default = DEFAULT_TOKEN_URL
+        },
+        set_userinfo_header = {
+            type = "boolean",
+            description = "Whether to set feishu user information in request 
headers",
+            default = true
+        },
+        auth_redirect_uri = {
+            type = "string",
+            description = "Redirect URI for initiating Feishu OAuth flow",
+        },
+        redirect_uri = {type = "string"},
+        timeout = {type = "integer", default = 6000},
+        ssl_verify = {type = "boolean", default = true},
+        secret = {
+            type = "string",
+            description = "Secret used for key derivation.",
+            minLength = 8,
+            maxLength = 32,
+        },
+        secret_fallbacks = {
+            type = "array",
+            items = {
+                type = "string",
+                minLength = 8,
+                maxLength = 32,
+            },
+            description = "List of secrets for alternative secrets used when 
doing key rotation"
+        },
+        cookie_expires_in = {
+            type = "integer",
+            description = "Valid duration (in seconds) for the authorization 
cookie."
+                        .. "This value defines how long the cookie remains 
valid after creation.",
+            default = 86400,
+        },
+
+    },
+    encrypt_fields = {"app_secret", "secret"},
+    required = {"app_id", "app_secret", "secret", "auth_redirect_uri", 
"redirect_uri"},
+}
+
+local _M = {
+    version = 0.1,
+    priority = 2420,
+    name = "feishu-auth",
+    schema = schema,
+}
+
+
+function _M.check_schema(conf)
+    return core.schema.check(schema, conf)
+end
+
+
+local function fetch_access_token(conf, code)
+    local httpc = http.new()
+    httpc:set_timeout(conf.timeout)
+
+    local body = {
+        grant_type = "authorization_code",
+        client_id = conf.app_id,
+        client_secret = conf.app_secret,
+        redirect_uri = conf.auth_redirect_uri,
+        code = code,
+    }
+
+    local res, err = httpc:request_uri(conf.access_token_url, {
+        method = "POST",
+        headers = {
+            ["Content-Type"] = "application/json"
+        },
+        body = core.json.encode(body),
+        ssl_verify = conf.ssl_verify
+    })
+
+    if not res then
+        core.log.error("failed to get feishu token: ", err)
+        return nil, nil, err
+    end
+
+    core.log.debug("request feishu access token response status: ",
+                                    res.status)
+
+    if res.status ~= 200 then
+        core.log.warn("unexpected http response status from feishu: ",
+                                        res.status, ", body: ", res.body)
+        return nil, nil, "unexpected response status: " .. res.status
+                                            .. ", body: " .. res.body
+    end
+
+    local data, err = core.json.decode(res.body)
+    if not data then
+        core.log.error("failed to decode feishu token response: ", err)
+        return nil, nil, "failed to decode response: " .. (err or "nil")
+    end
+
+    if not data.access_token or type(data.expires_in) ~= "number" then
+        core.log.error("feishu token response missing access_token or 
expires_in: ", res.body)
+        return nil, nil, "missing access_token or expires_in in response"
+    end
+
+    return data.access_token, data.expires_in, nil
+end
+
+
+local function fetch_userinfo(conf, access_token)
+    local httpc = http.new()
+    httpc:set_timeout(conf.timeout)
+
+    local res, err = httpc:request_uri(conf.userinfo_url, {
+        method = "GET",
+        headers = {
+            ["Content-Type"] = "application/json",
+            ["Authorization"] = "Bearer " .. access_token,
+        },
+        ssl_verify = conf.ssl_verify
+    })
+
+    if not res then
+        core.log.error("failed to verify feishu user: ", err)
+        return nil, err
+    end
+
+    core.log.debug("request feishu userinfo response status: ", res.status, ", 
body: ", res.body)
+
+    if res.status ~= 200 then
+        core.log.error("unexpected http response status from feishu: ",
+                            res.status, ", body: ", res.body)
+        return nil, "unexpected http response status: " .. res.status
+    end
+
+    local data, err = core.json.decode(res.body)
+    if not data then
+        core.log.error("failed to decode feishu userinfo response: ", err, ", 
body: ", res.body)
+        return nil, "failed to decode response: " .. err
+    end
+
+    if data.code ~= 0 then
+        core.log.warn("failed to get feishu userinfo: ", res.body)
+        return nil, "unexpected error code: " .. data.code
+                            .. ", errmsg: " .. (data.msg or "nil")
+    end
+
+    return data.data, nil
+end
+
+
+local function get_code(conf, ctx)
+    local code = core.request.header(ctx, conf.code_header)
+    if not code then
+        local uri_args = core.request.get_uri_args(ctx) or {}
+        code = uri_args[conf.code_query]
+    end
+
+    return code
+end
+
+
+function _M.rewrite(conf, ctx)
+    local userinfo, err
+
+    local sess, sess_err = session.open(
+        {
+            secret = conf.secret,
+            secret_fallbacks = conf.secret_fallbacks,
+            cookie_name = "feishu_session",
+            absolute_timeout = conf.cookie_expires_in,
+        }
+    )
+    if not sess then
+        core.log.error("failed to open session: ", sess_err)
+        return 500, {message = "Failed to open session"}
+    end
+
+    local raw = sess:get("userinfo")
+    if raw then
+        userinfo, err = core.json.decode(raw)
+        if not userinfo then
+            sess:destroy()
+            core.log.error("failed to decode userinfo in session: ", err)
+            return 500, {message = "Invalid userinfo in session"}
+        end
+    else
+        local code = get_code(conf, ctx)
+        if not code then
+            core.response.set_header("Location", conf.redirect_uri)
+            return 302
+        end
+
+        local refreshed = true
+        local access_token = sess:get("access_token")
+        if access_token then
+            local expires_at = sess:get("access_token_expires_at")
+            if expires_at and ngx_time() < expires_at then
+                refreshed = false
+            else
+                sess:delete("access_token")
+                sess:delete("access_token_expires_at")
+            end
+        end
+
+        if refreshed then
+            local new_access_token, expires_in, err = fetch_access_token(conf, 
code)
+            if not new_access_token then
+                core.log.warn("failed to get feishu access token: ", err)
+                return 401, {
+                    message = "Invalid authorization code",
+                }
+            end
+            access_token = new_access_token
+            sess:set("access_token", access_token)
+            sess:set("access_token_expires_at", ngx_time() + expires_in - 60)
+        end
+
+        local new_userinfo, err = fetch_userinfo(conf, access_token)
+        if not new_userinfo then
+            core.log.warn("failed to get feishu userinfo: ", err)
+            sess:destroy()
+            return 401, {
+                message = "Invalid authorization code",
+            }
+        end
+        userinfo = new_userinfo
+        local raw, err = core.json.encode(userinfo)
+        if not raw then
+            core.log.error("failed to encode userinfo: ", err)
+            return 500, {message = "Invalid userinfo"}
+        end
+
+        sess:set("userinfo", raw)
+        sess:save()
+        core.log.info("verified feishu user, code: ", code,
+                        ", app_id: ", conf.app_id)

Review Comment:
   This logs the raw OAuth authorization code at info level. Authorization 
codes are bearer credentials during their validity window and should not be 
written to access/error logs; log only non-sensitive identifiers or a redacted 
value.
   



##########
apisix/plugins/feishu-auth.lua:
##########
@@ -0,0 +1,291 @@
+--
+-- 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 core = require("apisix.core")
+local http = require("resty.http")
+local session = require("resty.session")
+
+local base64_encode = ngx.encode_base64
+local ngx_time = ngx.time
+local type = type
+
+local DEFAULT_TOKEN_URL = 
"https://open.feishu.cn/open-apis/authen/v2/oauth/token";
+local DEFAULT_USERINFO_URL = 
"https://open.feishu.cn/open-apis/authen/v1/user_info";
+
+local schema = {
+    type = "object",
+    properties = {
+        app_id = {type = "string", minLength = 1},
+        app_secret = {type = "string", minLength = 1},
+        code_header = {
+            type = "string",
+            description = "Header name to extract authorization code from.",
+            default = "X-Feishu-Code"
+        },
+        code_query = {
+            type = "string",
+            description = "Query parameter name to extract authorization code 
from.",
+            default = "code"
+        },
+        userinfo_url = {
+            type = "string",
+            default = DEFAULT_USERINFO_URL
+        },
+        access_token_url = {
+            type = "string",
+            default = DEFAULT_TOKEN_URL
+        },
+        set_userinfo_header = {
+            type = "boolean",
+            description = "Whether to set feishu user information in request 
headers",
+            default = true
+        },
+        auth_redirect_uri = {
+            type = "string",
+            description = "Redirect URI for initiating Feishu OAuth flow",
+        },
+        redirect_uri = {type = "string"},
+        timeout = {type = "integer", default = 6000},
+        ssl_verify = {type = "boolean", default = true},
+        secret = {
+            type = "string",
+            description = "Secret used for key derivation.",
+            minLength = 8,
+            maxLength = 32,
+        },
+        secret_fallbacks = {
+            type = "array",
+            items = {
+                type = "string",
+                minLength = 8,
+                maxLength = 32,
+            },
+            description = "List of secrets for alternative secrets used when 
doing key rotation"
+        },
+        cookie_expires_in = {
+            type = "integer",
+            description = "Valid duration (in seconds) for the authorization 
cookie."
+                        .. "This value defines how long the cookie remains 
valid after creation.",
+            default = 86400,
+        },
+
+    },
+    encrypt_fields = {"app_secret", "secret"},
+    required = {"app_id", "app_secret", "secret", "auth_redirect_uri", 
"redirect_uri"},
+}
+
+local _M = {
+    version = 0.1,
+    priority = 2420,
+    name = "feishu-auth",
+    schema = schema,
+}
+
+
+function _M.check_schema(conf)
+    return core.schema.check(schema, conf)
+end
+
+
+local function fetch_access_token(conf, code)
+    local httpc = http.new()
+    httpc:set_timeout(conf.timeout)
+
+    local body = {
+        grant_type = "authorization_code",
+        client_id = conf.app_id,
+        client_secret = conf.app_secret,
+        redirect_uri = conf.auth_redirect_uri,
+        code = code,
+    }
+
+    local res, err = httpc:request_uri(conf.access_token_url, {
+        method = "POST",
+        headers = {
+            ["Content-Type"] = "application/json"
+        },
+        body = core.json.encode(body),
+        ssl_verify = conf.ssl_verify
+    })
+
+    if not res then
+        core.log.error("failed to get feishu token: ", err)
+        return nil, nil, err
+    end
+
+    core.log.debug("request feishu access token response status: ",
+                                    res.status)
+
+    if res.status ~= 200 then
+        core.log.warn("unexpected http response status from feishu: ",
+                                        res.status, ", body: ", res.body)
+        return nil, nil, "unexpected response status: " .. res.status
+                                            .. ", body: " .. res.body
+    end
+
+    local data, err = core.json.decode(res.body)
+    if not data then
+        core.log.error("failed to decode feishu token response: ", err)
+        return nil, nil, "failed to decode response: " .. (err or "nil")
+    end
+
+    if not data.access_token or type(data.expires_in) ~= "number" then
+        core.log.error("feishu token response missing access_token or 
expires_in: ", res.body)
+        return nil, nil, "missing access_token or expires_in in response"
+    end
+
+    return data.access_token, data.expires_in, nil
+end
+
+
+local function fetch_userinfo(conf, access_token)
+    local httpc = http.new()
+    httpc:set_timeout(conf.timeout)
+
+    local res, err = httpc:request_uri(conf.userinfo_url, {
+        method = "GET",
+        headers = {
+            ["Content-Type"] = "application/json",
+            ["Authorization"] = "Bearer " .. access_token,
+        },
+        ssl_verify = conf.ssl_verify
+    })
+
+    if not res then
+        core.log.error("failed to verify feishu user: ", err)
+        return nil, err
+    end
+
+    core.log.debug("request feishu userinfo response status: ", res.status, ", 
body: ", res.body)
+
+    if res.status ~= 200 then
+        core.log.error("unexpected http response status from feishu: ",
+                            res.status, ", body: ", res.body)
+        return nil, "unexpected http response status: " .. res.status
+    end
+
+    local data, err = core.json.decode(res.body)
+    if not data then
+        core.log.error("failed to decode feishu userinfo response: ", err, ", 
body: ", res.body)
+        return nil, "failed to decode response: " .. err
+    end
+
+    if data.code ~= 0 then
+        core.log.warn("failed to get feishu userinfo: ", res.body)
+        return nil, "unexpected error code: " .. data.code
+                            .. ", errmsg: " .. (data.msg or "nil")
+    end
+
+    return data.data, nil
+end
+
+
+local function get_code(conf, ctx)
+    local code = core.request.header(ctx, conf.code_header)
+    if not code then
+        local uri_args = core.request.get_uri_args(ctx) or {}
+        code = uri_args[conf.code_query]
+    end
+
+    return code
+end
+
+
+function _M.rewrite(conf, ctx)
+    local userinfo, err
+
+    local sess, sess_err = session.open(
+        {
+            secret = conf.secret,
+            secret_fallbacks = conf.secret_fallbacks,
+            cookie_name = "feishu_session",
+            absolute_timeout = conf.cookie_expires_in,
+        }
+    )
+    if not sess then
+        core.log.error("failed to open session: ", sess_err)
+        return 500, {message = "Failed to open session"}
+    end
+
+    local raw = sess:get("userinfo")
+    if raw then
+        userinfo, err = core.json.decode(raw)
+        if not userinfo then
+            sess:destroy()
+            core.log.error("failed to decode userinfo in session: ", err)
+            return 500, {message = "Invalid userinfo in session"}
+        end
+    else
+        local code = get_code(conf, ctx)
+        if not code then
+            core.response.set_header("Location", conf.redirect_uri)
+            return 302

Review Comment:
   This OAuth callback path accepts any authorization code without binding it 
to a per-session `state` value. A browser-based OAuth flow needs state 
generation and validation to prevent login CSRF/session fixation; otherwise an 
attacker can cause a victim to complete the callback with a code from the 
attacker's authorization flow.



##########
apisix/plugins/feishu-auth.lua:
##########
@@ -0,0 +1,291 @@
+--
+-- 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 core = require("apisix.core")
+local http = require("resty.http")
+local session = require("resty.session")
+
+local base64_encode = ngx.encode_base64
+local ngx_time = ngx.time
+local type = type
+
+local DEFAULT_TOKEN_URL = 
"https://open.feishu.cn/open-apis/authen/v2/oauth/token";
+local DEFAULT_USERINFO_URL = 
"https://open.feishu.cn/open-apis/authen/v1/user_info";
+
+local schema = {
+    type = "object",
+    properties = {
+        app_id = {type = "string", minLength = 1},
+        app_secret = {type = "string", minLength = 1},
+        code_header = {
+            type = "string",
+            description = "Header name to extract authorization code from.",
+            default = "X-Feishu-Code"
+        },
+        code_query = {
+            type = "string",
+            description = "Query parameter name to extract authorization code 
from.",
+            default = "code"
+        },
+        userinfo_url = {
+            type = "string",
+            default = DEFAULT_USERINFO_URL
+        },
+        access_token_url = {
+            type = "string",
+            default = DEFAULT_TOKEN_URL
+        },
+        set_userinfo_header = {
+            type = "boolean",
+            description = "Whether to set feishu user information in request 
headers",
+            default = true
+        },
+        auth_redirect_uri = {
+            type = "string",
+            description = "Redirect URI for initiating Feishu OAuth flow",
+        },
+        redirect_uri = {type = "string"},
+        timeout = {type = "integer", default = 6000},
+        ssl_verify = {type = "boolean", default = true},
+        secret = {
+            type = "string",
+            description = "Secret used for key derivation.",
+            minLength = 8,
+            maxLength = 32,
+        },
+        secret_fallbacks = {
+            type = "array",
+            items = {
+                type = "string",
+                minLength = 8,
+                maxLength = 32,
+            },
+            description = "List of secrets for alternative secrets used when 
doing key rotation"
+        },
+        cookie_expires_in = {
+            type = "integer",
+            description = "Valid duration (in seconds) for the authorization 
cookie."
+                        .. "This value defines how long the cookie remains 
valid after creation.",
+            default = 86400,
+        },
+
+    },
+    encrypt_fields = {"app_secret", "secret"},

Review Comment:
   `secret_fallbacks` contains the same kind of session-signing secrets as 
`secret`, but it is not included in `encrypt_fields`. When data encryption is 
enabled, these fallback keys would remain plaintext in etcd while the primary 
secret is encrypted.
   



##########
docs/en/latest/plugins/feishu-auth.md:
##########
@@ -0,0 +1,118 @@
+---
+title: feishu-auth
+keywords:
+  - Apache APISIX
+  - API Gateway
+  - Plugin
+  - Feishu Auth
+  - feishu-auth
+description: This document contains information about the Apache APISIX 
feishu-auth Plugin.
+---
+
+<!--
+#
+# 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.
+#
+-->
+

Review Comment:
   New plugin docs should include the canonical `<head>` link used by the 
existing plugin documentation (for example, 
`docs/en/latest/plugins/key-auth.md:31-33` and 
`docs/en/latest/plugins/cors.md:29-31`). This page currently starts the content 
directly after the license block, so the canonical URL is missing.
   



##########
apisix/plugins/feishu-auth.lua:
##########
@@ -0,0 +1,291 @@
+--
+-- 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 core = require("apisix.core")
+local http = require("resty.http")
+local session = require("resty.session")
+
+local base64_encode = ngx.encode_base64
+local ngx_time = ngx.time
+local type = type
+
+local DEFAULT_TOKEN_URL = 
"https://open.feishu.cn/open-apis/authen/v2/oauth/token";
+local DEFAULT_USERINFO_URL = 
"https://open.feishu.cn/open-apis/authen/v1/user_info";
+
+local schema = {
+    type = "object",
+    properties = {
+        app_id = {type = "string", minLength = 1},
+        app_secret = {type = "string", minLength = 1},
+        code_header = {
+            type = "string",
+            description = "Header name to extract authorization code from.",
+            default = "X-Feishu-Code"
+        },
+        code_query = {
+            type = "string",
+            description = "Query parameter name to extract authorization code 
from.",
+            default = "code"
+        },
+        userinfo_url = {
+            type = "string",
+            default = DEFAULT_USERINFO_URL
+        },
+        access_token_url = {
+            type = "string",
+            default = DEFAULT_TOKEN_URL
+        },
+        set_userinfo_header = {
+            type = "boolean",
+            description = "Whether to set feishu user information in request 
headers",
+            default = true
+        },
+        auth_redirect_uri = {
+            type = "string",
+            description = "Redirect URI for initiating Feishu OAuth flow",
+        },
+        redirect_uri = {type = "string"},
+        timeout = {type = "integer", default = 6000},
+        ssl_verify = {type = "boolean", default = true},
+        secret = {
+            type = "string",
+            description = "Secret used for key derivation.",
+            minLength = 8,
+            maxLength = 32,
+        },
+        secret_fallbacks = {
+            type = "array",
+            items = {
+                type = "string",
+                minLength = 8,
+                maxLength = 32,
+            },
+            description = "List of secrets for alternative secrets used when 
doing key rotation"
+        },
+        cookie_expires_in = {
+            type = "integer",
+            description = "Valid duration (in seconds) for the authorization 
cookie."
+                        .. "This value defines how long the cookie remains 
valid after creation.",
+            default = 86400,
+        },
+
+    },
+    encrypt_fields = {"app_secret", "secret"},
+    required = {"app_id", "app_secret", "secret", "auth_redirect_uri", 
"redirect_uri"},
+}
+
+local _M = {
+    version = 0.1,
+    priority = 2420,
+    name = "feishu-auth",
+    schema = schema,
+}
+
+
+function _M.check_schema(conf)
+    return core.schema.check(schema, conf)
+end
+
+
+local function fetch_access_token(conf, code)
+    local httpc = http.new()
+    httpc:set_timeout(conf.timeout)
+
+    local body = {
+        grant_type = "authorization_code",
+        client_id = conf.app_id,
+        client_secret = conf.app_secret,
+        redirect_uri = conf.auth_redirect_uri,
+        code = code,
+    }
+
+    local res, err = httpc:request_uri(conf.access_token_url, {
+        method = "POST",
+        headers = {
+            ["Content-Type"] = "application/json"
+        },
+        body = core.json.encode(body),
+        ssl_verify = conf.ssl_verify
+    })
+
+    if not res then
+        core.log.error("failed to get feishu token: ", err)
+        return nil, nil, err
+    end
+
+    core.log.debug("request feishu access token response status: ",
+                                    res.status)
+
+    if res.status ~= 200 then
+        core.log.warn("unexpected http response status from feishu: ",
+                                        res.status, ", body: ", res.body)
+        return nil, nil, "unexpected response status: " .. res.status
+                                            .. ", body: " .. res.body
+    end
+
+    local data, err = core.json.decode(res.body)
+    if not data then
+        core.log.error("failed to decode feishu token response: ", err)
+        return nil, nil, "failed to decode response: " .. (err or "nil")
+    end
+
+    if not data.access_token or type(data.expires_in) ~= "number" then
+        core.log.error("feishu token response missing access_token or 
expires_in: ", res.body)
+        return nil, nil, "missing access_token or expires_in in response"
+    end
+
+    return data.access_token, data.expires_in, nil
+end
+
+
+local function fetch_userinfo(conf, access_token)
+    local httpc = http.new()
+    httpc:set_timeout(conf.timeout)
+
+    local res, err = httpc:request_uri(conf.userinfo_url, {
+        method = "GET",
+        headers = {
+            ["Content-Type"] = "application/json",
+            ["Authorization"] = "Bearer " .. access_token,
+        },
+        ssl_verify = conf.ssl_verify
+    })
+
+    if not res then
+        core.log.error("failed to verify feishu user: ", err)
+        return nil, err
+    end
+
+    core.log.debug("request feishu userinfo response status: ", res.status, ", 
body: ", res.body)
+
+    if res.status ~= 200 then
+        core.log.error("unexpected http response status from feishu: ",
+                            res.status, ", body: ", res.body)
+        return nil, "unexpected http response status: " .. res.status
+    end
+
+    local data, err = core.json.decode(res.body)
+    if not data then
+        core.log.error("failed to decode feishu userinfo response: ", err, ", 
body: ", res.body)
+        return nil, "failed to decode response: " .. err
+    end
+
+    if data.code ~= 0 then
+        core.log.warn("failed to get feishu userinfo: ", res.body)
+        return nil, "unexpected error code: " .. data.code
+                            .. ", errmsg: " .. (data.msg or "nil")
+    end
+
+    return data.data, nil
+end
+
+
+local function get_code(conf, ctx)
+    local code = core.request.header(ctx, conf.code_header)
+    if not code then
+        local uri_args = core.request.get_uri_args(ctx) or {}
+        code = uri_args[conf.code_query]
+    end
+
+    return code
+end
+
+
+function _M.rewrite(conf, ctx)
+    local userinfo, err
+
+    local sess, sess_err = session.open(
+        {
+            secret = conf.secret,
+            secret_fallbacks = conf.secret_fallbacks,
+            cookie_name = "feishu_session",
+            absolute_timeout = conf.cookie_expires_in,
+        }
+    )
+    if not sess then
+        core.log.error("failed to open session: ", sess_err)
+        return 500, {message = "Failed to open session"}
+    end
+
+    local raw = sess:get("userinfo")
+    if raw then
+        userinfo, err = core.json.decode(raw)
+        if not userinfo then
+            sess:destroy()
+            core.log.error("failed to decode userinfo in session: ", err)
+            return 500, {message = "Invalid userinfo in session"}
+        end
+    else
+        local code = get_code(conf, ctx)
+        if not code then
+            core.response.set_header("Location", conf.redirect_uri)
+            return 302
+        end
+
+        local refreshed = true
+        local access_token = sess:get("access_token")
+        if access_token then
+            local expires_at = sess:get("access_token_expires_at")
+            if expires_at and ngx_time() < expires_at then
+                refreshed = false
+            else
+                sess:delete("access_token")
+                sess:delete("access_token_expires_at")
+            end
+        end
+
+        if refreshed then
+            local new_access_token, expires_in, err = fetch_access_token(conf, 
code)
+            if not new_access_token then
+                core.log.warn("failed to get feishu access token: ", err)
+                return 401, {
+                    message = "Invalid authorization code",
+                }
+            end
+            access_token = new_access_token
+            sess:set("access_token", access_token)
+            sess:set("access_token_expires_at", ngx_time() + expires_in - 60)
+        end
+
+        local new_userinfo, err = fetch_userinfo(conf, access_token)
+        if not new_userinfo then
+            core.log.warn("failed to get feishu userinfo: ", err)
+            sess:destroy()
+            return 401, {
+                message = "Invalid authorization code",
+            }
+        end
+        userinfo = new_userinfo
+        local raw, err = core.json.encode(userinfo)
+        if not raw then
+            core.log.error("failed to encode userinfo: ", err)
+            return 500, {message = "Invalid userinfo"}
+        end
+
+        sess:set("userinfo", raw)
+        sess:save()
+        core.log.info("verified feishu user, code: ", code,
+                        ", app_id: ", conf.app_id)
+    end
+
+    if userinfo and conf.set_userinfo_header ~= false then
+        core.request.set_header(ctx, "X-Userinfo", 
base64_encode(core.json.encode(userinfo)))

Review Comment:
   When `set_userinfo_header` is disabled, a client-supplied `X-Userinfo` 
header is left untouched and can reach the upstream. Since this plugin 
advertises `X-Userinfo` as its authenticated user output, clear the header 
before authentication (as `openid-connect` does) and only set it from verified 
session data when enabled.



##########
docs/zh/latest/plugins/feishu-auth.md:
##########
@@ -0,0 +1,121 @@
+---
+title: feishu-auth
+keywords:
+  - Apache APISIX
+  - API 网关
+  - Plugin
+  - Feishu Auth
+  - feishu-auth
+description: 本篇文档介绍了 Apache APISIX feishu-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.
+#
+-->
+

Review Comment:
   New plugin docs should include the canonical `<head>` link used by the 
existing plugin documentation (for example, 
`docs/zh/latest/plugins/key-auth.md:31-33`). This page currently starts the 
content directly after the license block, so the canonical URL is missing.
   



##########
apisix/plugins/feishu-auth.lua:
##########
@@ -0,0 +1,291 @@
+--
+-- 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 core = require("apisix.core")
+local http = require("resty.http")
+local session = require("resty.session")
+
+local base64_encode = ngx.encode_base64
+local ngx_time = ngx.time
+local type = type
+
+local DEFAULT_TOKEN_URL = 
"https://open.feishu.cn/open-apis/authen/v2/oauth/token";
+local DEFAULT_USERINFO_URL = 
"https://open.feishu.cn/open-apis/authen/v1/user_info";
+
+local schema = {
+    type = "object",
+    properties = {
+        app_id = {type = "string", minLength = 1},
+        app_secret = {type = "string", minLength = 1},
+        code_header = {
+            type = "string",
+            description = "Header name to extract authorization code from.",
+            default = "X-Feishu-Code"
+        },
+        code_query = {
+            type = "string",
+            description = "Query parameter name to extract authorization code 
from.",
+            default = "code"
+        },
+        userinfo_url = {
+            type = "string",
+            default = DEFAULT_USERINFO_URL
+        },
+        access_token_url = {
+            type = "string",
+            default = DEFAULT_TOKEN_URL
+        },
+        set_userinfo_header = {
+            type = "boolean",
+            description = "Whether to set feishu user information in request 
headers",
+            default = true
+        },
+        auth_redirect_uri = {
+            type = "string",
+            description = "Redirect URI for initiating Feishu OAuth flow",
+        },
+        redirect_uri = {type = "string"},
+        timeout = {type = "integer", default = 6000},
+        ssl_verify = {type = "boolean", default = true},
+        secret = {
+            type = "string",
+            description = "Secret used for key derivation.",
+            minLength = 8,
+            maxLength = 32,
+        },
+        secret_fallbacks = {
+            type = "array",
+            items = {
+                type = "string",
+                minLength = 8,
+                maxLength = 32,
+            },
+            description = "List of secrets for alternative secrets used when 
doing key rotation"
+        },
+        cookie_expires_in = {
+            type = "integer",
+            description = "Valid duration (in seconds) for the authorization 
cookie."
+                        .. "This value defines how long the cookie remains 
valid after creation.",
+            default = 86400,
+        },
+
+    },
+    encrypt_fields = {"app_secret", "secret"},
+    required = {"app_id", "app_secret", "secret", "auth_redirect_uri", 
"redirect_uri"},
+}
+
+local _M = {
+    version = 0.1,
+    priority = 2420,
+    name = "feishu-auth",
+    schema = schema,
+}
+
+
+function _M.check_schema(conf)
+    return core.schema.check(schema, conf)

Review Comment:
   `check_schema` does not call the HTTPS/TLS warning helpers for 
`auth_redirect_uri`, `redirect_uri`, `access_token_url`, or `userinfo_url`. 
Other auth integrations such as `authz-casdoor` and `openid-connect` call 
`core.utils.check_https` (and `check_tls_bool` for `ssl_verify`) so operators 
are warned before sending OAuth codes, client secrets, or access tokens over 
non-HTTPS endpoints.
   



##########
docs/en/latest/plugins/feishu-auth.md:
##########
@@ -0,0 +1,118 @@
+---
+title: feishu-auth
+keywords:
+  - Apache APISIX
+  - API Gateway
+  - Plugin
+  - Feishu Auth
+  - feishu-auth
+description: This document contains information about the Apache APISIX 
feishu-auth Plugin.
+---
+
+<!--
+#
+# 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.
+#
+-->
+
+## Description
+
+The `feishu-auth` Plugin authenticates requests using the [Feishu (Lark) OAuth 
2.0](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/authentication-management/access-token/oauth-2.0-overview)
 authorization flow. Users are redirected to the Feishu login page when 
unauthenticated. After a successful login, Feishu user information is stored in 
an encrypted session cookie and optionally forwarded to upstream services via 
the `X-Userinfo` header.
+
+## Attributes
+
+| Name                   | Type     | Required | Default                       
                                    | Description                               
                                                                  |
+|------------------------|----------|----------|-------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------|
+| `app_id`               | string   | True     |                               
                                    | Feishu application App ID.                
                                                                  |
+| `app_secret`           | string   | True     |                               
                                    | Feishu application App Secret.            
                                                                  |
+| `secret`               | string   | True     |                               
                                    | Secret used for signing the session 
cookie (8–32 characters). Must remain stable across restarts.          |
+| `auth_redirect_uri`    | string   | True     |                               
                                    | Redirect URI registered in the Feishu 
application for the OAuth flow.                                       |
+| `redirect_uri`         | string   | True     |                               
                                    | URI to redirect the user to when no 
authorization code is present (i.e. to start the OAuth flow).          |
+| `code_header`          | string   | False    | `"X-Feishu-Code"`             
                                    | HTTP header name to extract the Feishu 
authorization code from.                                             |
+| `code_query`           | string   | False    | `"code"`                      
                                    | Query parameter name to extract the 
Feishu authorization code from.                                         |
+| `access_token_url`     | string   | False    | 
`"https://open.feishu.cn/open-apis/authen/v2/oauth/token"`       | URL to 
exchange the authorization code for an access token.                            
                     |
+| `userinfo_url`         | string   | False    | 
`"https://open.feishu.cn/open-apis/authen/v1/user_info"`         | URL to 
retrieve user information using the access token.                               
                     |
+| `set_userinfo_header`  | boolean  | False    | `true`                        
                                    | When enabled, sets the `X-Userinfo` 
request header with Base64-encoded Feishu user information.            |
+| `cookie_expires_in`    | integer  | False    | `86400`                       
                                    | Validity duration (in seconds) for the 
session cookie.                                                      |
+| `secret_fallbacks`     | array    | False    |                               
                                    | List of fallback secrets used during key 
rotation.                                                          |
+| `timeout`              | integer  | False    | `6000`                        
                                    | Timeout (in milliseconds) for HTTP 
requests to Feishu endpoints.                                            |
+| `ssl_verify`           | boolean  | False    | `true`                        
                                    | When enabled, verifies the SSL 
certificate when connecting to Feishu endpoints.                             |
+

Review Comment:
   The English plugin docs omit the note that sensitive fields are encrypted 
with `encrypt_fields`, while the schema encrypts `app_secret` and `secret` and 
the Chinese doc includes this note. Add the same encryption-storage note here 
so English readers know how these credentials are stored.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to