Feishu is an enterprise-level collaborative office software owned by ByteDance. This article will introduce how to use Lua to implement an enterprise-level organizational structure login authentication gateway based on the identity verification capabilities of the Feishu open platform.
Login process
Let's first take a look at the overall process of Feishu's third-party website free registration:
Step 1: The backend of the webpage finds that the user is not logged in and requests authentication;
Step 2: After the user logs in, the open platform generates a login pre-authorization code, and 302 redirects to the redirect address;
Step 3: Call the backend of the web page to obtain the identity of the logged-in user to verify the legitimacy of the login pre-authorization code, and obtain the user's identity;
Step 4: If other user information is needed, the back end of the web page can call to obtain user information (identity verification).
Lua implementation
Partial realization of Feishu interface
Get the access_token of the application
function _M:get_app_access_token()
local url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/"
local body = {
app_id = self.app_id,
app_secret = self.app_secret
}
local res, err = http_post(url, body, nil)
if not res then
return nil, err
end
if res.status ~= 200 then
return nil, res.body
end
local data = json.decode(res.body)
if data["code"] ~= 0 then
return nil, res.body
end
return data["tenant_access_token"]
end
Get the login user information through the callback code
function _M:get_login_user(code)
local app_access_token, err = self:get_app_access_token()
if not app_access_token then
return nil, "get app_access_token failed: " .. err
end
local url = "https://open.feishu.cn/open-apis/authen/v1/access_token"
local headers = {
Authorization = "Bearer " .. app_access_token
}
local body = {
grant_type = "authorization_code",
code = code
}
ngx.log(ngx.ERR, json.encode(body))
local res, err = http_post(url, body, headers)
if not res then
return nil, err
end
local data = json.decode(res.body)
if data["code"] ~= 0 then
return nil, res.body
end
return data["data"]
end
Get user details
The user's department information cannot be obtained when obtaining the login user information, so here you need to use open_id
in the login user information to obtain the user's detailed information. At the same time, user_access_token
also comes from the obtained login user information.
function _M:get_user(user_access_token, open_id)
local url = "https://open.feishu.cn/open-apis/contact/v3/users/" .. open_id
local headers = {
Authorization = "Bearer " .. user_access_token
}
local res, err = http_get(url, nil, headers)
if not res then
return nil, err
end
local data = json.decode(res.body)
if data["code"] ~= 0 then
return nil, res.body
end
return data["data"]["user"], nil
end
login information
JWT login credentials
We use JWT as the login credential, which is also used to save the user's open_id
and department_ids
.
-- 生成 token
function _M:sign_token(user)
local open_id = user["open_id"]
if not open_id or open_id == "" then
return nil, "invalid open_id"
end
local department_ids = user["department_ids"]
if not department_ids or type(department_ids) ~= "table" then
return nil, "invalid department_ids"
end
return jwt:sign(
self.jwt_secret,
{
header = {
typ = "JWT",
alg = jwt_header_alg,
exp = ngx.time() + self.jwt_expire
},
payload = {
open_id = open_id,
department_ids = json.encode(department_ids)
}
}
)
end
-- 验证与解析 token
function _M:verify_token()
local token = ngx.var.cookie_feishu_auth_token
if not token then
return nil, "token not found"
end
local result = jwt:verify(self.jwt_secret, token)
ngx.log(ngx.ERR, "jwt_obj: ", json.encode(result))
if result["valid"] then
local payload = result["payload"]
if payload["department_ids"] and payload["open_id"] then
return payload
end
return nil, "invalid token: " .. json.encode(result)
end
return nil, "invalid token: " .. json.encode(result)
end
Use cookies to store login credentials
ngx.header["Set-Cookie"] = self.cookie_key .. "=" .. token
White list of organizational structure
We obtain the user's department information when the user logs in, or parse the department information in the login credentials when the user subsequently accesses the application, and determine whether the user has the permission to access the application according to the set department whitelist.
-- 部门白名单配置
_M.department_whitelist = {}
function _M:check_user_access(user)
if type(self.department_whitelist) ~= "table" then
ngx.log(ngx.ERR, "department_whitelist is not a table")
return false
end
if #self.department_whitelist == 0 then
return true
end
local department_ids = user["department_ids"]
if not department_ids or department_ids == "" then
return false
end
if type(department_ids) ~= "table" then
department_ids = json.decode(department_ids)
end
for i=1, #department_ids do
if has_value(self.department_whitelist, department_ids[i]) then
return true
end
end
return false
end
More gateway configuration
Supports IP blacklist and routing whitelist configuration at the same time.
-- IP 黑名单配置
_M.ip_blacklist = {}
-- 路由白名单配置
_M.uri_whitelist = {}
function _M:auth()
local request_uri = ngx.var.uri
ngx.log(ngx.ERR, "request uri: ", request_uri)
if has_value(self.uri_whitelist, request_uri) then
ngx.log(ngx.ERR, "uri in whitelist: ", request_uri)
return
end
local request_ip = ngx.var.remote_addr
if has_value(self.ip_blacklist, request_ip) then
ngx.log(ngx.ERR, "forbided ip: ", request_ip)
return ngx.exit(ngx.HTTP_FORBIDDEN)
end
if request_uri == self.logout_uri then
return self:logout()
end
local payload, err = self:verify_token()
if payload then
if self:check_user_access(payload) then
return
end
ngx.log(ngx.ERR, "user access not permitted")
self:clear_token()
return self:sso()
end
ngx.log(ngx.ERR, "verify token failed: ", err)
if request_uri ~= self.callback_uri then
return self:sso()
end
return self:sso_callback()
end
use
This article will not go into details about the installation of OpenResty, you can refer to my other article "Installing OpenResty with Source Code on Ubuntu" .
download
cd /path/to
git clone git@github.com:ledgetech/lua-resty-http.git
git clone git@github.com:SkyLothar/lua-resty-jwt.git
git clone git@github.com:k8scat/lua-resty-feishu-auth.git
Configuration
lua_package_path "/path/to/lua-resty-feishu-auth/lib/?.lua;/path/to/lua-resty-jwt/lib/?.lua;/path/to/lua-resty-http/lib/?.lua;/path/to/lua-resty-redis/lib/?.lua;/path/to/lua-resty-redis-lock/lib/?.lua;;";
server {
access_by_lua_block {
local feishu_auth = require "resty.feishu_auth"
feishu_auth.app_id = ""
feishu_auth.app_secret = ""
feishu_auth.callback_uri = "/feishu_auth_callback"
feishu_auth.logout_uri = "/feishu_auth_logout"
feishu_auth.app_domain = "feishu-auth.example.com"
feishu_auth.jwt_secret = "thisisjwtsecret"
feishu_auth.ip_blacklist = {"47.1.2.3"}
feishu_auth.uri_whitelist = {"/"}
feishu_auth.department_whitelist = {"0"}
feishu_auth:auth()
}
}
Configuration instructions
app_id
used to set the self-built application ofApp ID
app_secret
used to set the self-built application ofApp Secret
callback_uri
used to set the callback address after login on Feishu webpage (redirect URL needs to be set in the security settings of Feishu enterprise self-built application)logout_uri
used to set the logout addressapp_domain
used to set the access domain name (must be consistent with the access domain name of the business service)jwt_secret
used to set the JWT secretip_blacklist
used to set the IP blacklisturi_whitelist
used to set the address whitelist, for example, the homepage does not require login authenticationdepartment_whitelist
used to set the department whitelist (string)
Application permission description
- Obtain basic department information
- Obtain departmental organization information
- Read the address book as an application
- Get user organization structure information
- Get basic user information
Open source
This project has been completed and has been open sourced on GitHub: k8scat/lua-resty-feishu-auth , I hope everyone can move their fingers to the Star to express their affirmation and support for this project!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。