头图

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 of App ID
  • app_secret used to set the self-built application of App 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 address
  • app_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 secret
  • ip_blacklist used to set the IP blacklist
  • uri_whitelist used to set the address whitelist, for example, the homepage does not require login authentication
  • department_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!


K8sCat
270 声望737 粉丝