From c05b27c38f850c384c7c69302afc352f0840a5e9 Mon Sep 17 00:00:00 2001
From: sh1luo <690898835@qq.com>
Date: Sat, 14 Aug 2021 16:00:38 +0800
Subject: [PATCH] feat: add lark provider (#264)
Signed-off-by: sh1luo <690898835@qq.com>
---
idp/lark.go | 216 ++++++++++++++++++++++++++++++++
idp/provider.go | 2 +
object/user.go | 1 +
web/src/ProviderEditPage.js | 1 +
web/src/auth/LarkLoginButton.js | 32 +++++
web/src/auth/LoginPage.js | 3 +
web/src/auth/Provider.js | 12 +-
7 files changed, 265 insertions(+), 2 deletions(-)
create mode 100644 idp/lark.go
create mode 100644 web/src/auth/LarkLoginButton.js
diff --git a/idp/lark.go b/idp/lark.go
new file mode 100644
index 00000000..dcf18551
--- /dev/null
+++ b/idp/lark.go
@@ -0,0 +1,216 @@
+// Copyright 2021 The casbin Authors. All Rights Reserved.
+//
+// Licensed 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.
+
+package idp
+
+import (
+ "encoding/json"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "strings"
+ "time"
+
+ "golang.org/x/oauth2"
+)
+
+type LarkIdProvider struct {
+ Client *http.Client
+ Config *oauth2.Config
+}
+
+func NewLarkIdProvider(clientId string, clientSecret string, redirectUrl string) *LarkIdProvider {
+ idp := &LarkIdProvider{}
+
+ config := idp.getConfig(clientId, clientSecret, redirectUrl)
+ idp.Config = config
+
+ return idp
+}
+
+func (idp *LarkIdProvider) SetHttpClient(client *http.Client) {
+ idp.Client = client
+}
+
+// getConfig return a point of Config, which describes a typical 3-legged OAuth2 flow
+func (idp *LarkIdProvider) getConfig(clientId string, clientSecret string, redirectUrl string) *oauth2.Config {
+ var endpoint = oauth2.Endpoint{
+ TokenURL: "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
+ }
+
+ var config = &oauth2.Config{
+ Scopes: []string{},
+ Endpoint: endpoint,
+ ClientID: clientId,
+ ClientSecret: clientSecret,
+ RedirectURL: redirectUrl,
+ }
+
+ return config
+}
+
+/*
+{
+ "code": 0,
+ "msg": "success",
+ "tenant_access_token": "t-caecc734c2e3328a62489fe0648c4b98779515d3",
+ "expire": 7140
+}
+*/
+
+type LarkAccessToken struct {
+ Code int `json:"code"`
+ Msg string `json:"msg"`
+ TenantAccessToken string `json:"tenant_access_token"`
+ Expire int `json:"expire"`
+}
+
+// GetToken use code get access_token (*operation of getting code ought to be done in front)
+// get more detail via: https://docs.microsoft.com/en-us/linkedIn/shared/authentication/authorization-code-flow?context=linkedIn%2Fcontext&tabs=HTTPS
+func (idp *LarkIdProvider) GetToken(code string) (*oauth2.Token, error) {
+ params := &struct {
+ AppID string `json:"app_id"`
+ AppSecret string `json:"app_secret"`
+ }{idp.Config.ClientID, idp.Config.ClientSecret}
+ data, err := idp.postWithBody(params, idp.Config.Endpoint.TokenURL)
+
+ appToken := &LarkAccessToken{}
+ if err = json.Unmarshal(data, appToken); err != nil || appToken.Code != 0 {
+ return nil, err
+ }
+
+ t := &oauth2.Token{
+ AccessToken: appToken.TenantAccessToken,
+ TokenType: "Bearer",
+ Expiry: time.Unix(time.Now().Unix()+int64(appToken.Expire), 0),
+ }
+
+ raw := make(map[string]interface{})
+ raw["code"] = code
+ t = t.WithExtra(raw)
+
+ return t, nil
+}
+
+/*
+{
+ "code": 0,
+ "msg": "success",
+ "data": {
+ "access_token": "u-6U1SbDiM6XIH2DcTCPyeub",
+ "token_type": "Bearer",
+ "expires_in": 7140,
+ "name": "zhangsan",
+ "en_name": "Three Zhang",
+ "avatar_url": "www.feishu.cn/avatar/icon",
+ "avatar_thumb": "www.feishu.cn/avatar/icon_thumb",
+ "avatar_middle": "www.feishu.cn/avatar/icon_middle",
+ "avatar_big": "www.feishu.cn/avatar/icon_big",
+ "open_id": "ou-caecc734c2e3328a62489fe0648c4b98779515d3",
+ "union_id": "on-d89jhsdhjsajkda7828enjdj328ydhhw3u43yjhdj",
+ "email": "zhangsan@feishu.cn",
+ "user_id": "5d9bdxxx",
+ "mobile": "+86130002883xx",
+ "tenant_key": "736588c92lxf175d",
+ "refresh_expires_in": 2591940,
+ "refresh_token": "ur-t9HHgRCjMqGqIU9v05Zhos"
+ }
+}
+*/
+
+type LarkUserInfo struct {
+ Code int `json:"code"`
+ Msg string `json:"msg"`
+ Data struct {
+ AccessToken string `json:"access_token"`
+ TokenType string `json:"token_type"`
+ ExpiresIn int `json:"expires_in"`
+ Name string `json:"name"`
+ EnName string `json:"en_name"`
+ AvatarUrl string `json:"avatar_url"`
+ AvatarThumb string `json:"avatar_thumb"`
+ AvatarMiddle string `json:"avatar_middle"`
+ AvatarBig string `json:"avatar_big"`
+ OpenId string `json:"open_id"`
+ UnionId string `json:"union_id"`
+ Email string `json:"email"`
+ UserId string `json:"user_id"`
+ Mobile string `json:"mobile"`
+ TenantKey string `json:"tenant_key"`
+ RefreshExpiresIn int `json:"refresh_expires_in"`
+ RefreshToken string `json:"refresh_token"`
+ } `json:"data"`
+}
+
+// GetUserInfo use LarkAccessToken gotten before return LinkedInUserInfo
+// get more detail via: https://docs.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin?context=linkedin/consumer/context
+func (idp *LarkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
+ body := &struct {
+ GrantType string `json:"grant_type"`
+ Code string `json:"code"`
+ }{"authorization_code", token.Extra("code").(string)}
+ data, _ := json.Marshal(body)
+ req, err := http.NewRequest("POST", "https://open.feishu.cn/open-apis/authen/v1/access_token", strings.NewReader(string(data)))
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("Content-Type", "application/json;charset=UTF-8")
+ req.Header.Set("Authorization", "Bearer "+token.AccessToken)
+
+ resp, err := idp.Client.Do(req)
+ data, err = ioutil.ReadAll(resp.Body)
+ err = resp.Body.Close()
+ if err != nil {
+ return nil, err
+ }
+
+ var larkUserInfo LarkUserInfo
+ if err = json.Unmarshal(data, &larkUserInfo); err != nil {
+ return nil, err
+ }
+
+ userInfo := UserInfo{
+ Id: larkUserInfo.Data.OpenId,
+ DisplayName: larkUserInfo.Data.EnName,
+ Username: larkUserInfo.Data.Name,
+ Email: larkUserInfo.Data.Email,
+ AvatarUrl: larkUserInfo.Data.AvatarUrl,
+ }
+
+ return &userInfo, nil
+}
+
+func (idp *LarkIdProvider) postWithBody(body interface{}, url string) ([]byte, error) {
+ bs, err := json.Marshal(body)
+ if err != nil {
+ return nil, err
+ }
+ r := strings.NewReader(string(bs))
+ resp, err := idp.Client.Post(url, "application/json;charset=UTF-8", r)
+ if err != nil {
+ return nil, err
+ }
+ data, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, err
+ }
+ defer func(Body io.ReadCloser) {
+ err := Body.Close()
+ if err != nil {
+ return
+ }
+ }(resp.Body)
+
+ return data, nil
+}
diff --git a/idp/provider.go b/idp/provider.go
index a4ae0160..a1d80f75 100644
--- a/idp/provider.go
+++ b/idp/provider.go
@@ -55,6 +55,8 @@ func GetIdProvider(providerType string, clientId string, clientSecret string, re
return NewLinkedInIdProvider(clientId, clientSecret, redirectUrl)
} else if providerType == "WeCom" {
return NewWeComIdProvider(clientId, clientSecret, redirectUrl)
+ } else if providerType == "Lark" {
+ return NewLarkIdProvider(clientId, clientSecret, redirectUrl)
}
return nil
diff --git a/object/user.go b/object/user.go
index c85aff1e..771f2bf9 100644
--- a/object/user.go
+++ b/object/user.go
@@ -57,6 +57,7 @@ type User struct {
Gitee string `xorm:"gitee varchar(100)" json:"gitee"`
LinkedIn string `xorm:"linkedin varchar(100)" json:"linkedin"`
Wecom string `xorm:"wecom varchar(100)" json:"wecom"`
+ Lark string `xorm:"lark varchar(100)" json:"lark"`
Ldap string `xorm:"ldap varchar(100)" json:"ldap"`
Properties map[string]string `json:"properties"`
diff --git a/web/src/ProviderEditPage.js b/web/src/ProviderEditPage.js
index 9fe8df7e..7a7dbc17 100644
--- a/web/src/ProviderEditPage.js
+++ b/web/src/ProviderEditPage.js
@@ -76,6 +76,7 @@ class ProviderEditPage extends React.Component {
{id: 'Gitee', name: 'Gitee'},
{id: 'LinkedIn', name: 'LinkedIn'},
{id: 'WeCom', name: 'WeCom'},
+ {id: 'Lark', name: 'Lark'},
]
);
} else if (provider.category === "Email") {
diff --git a/web/src/auth/LarkLoginButton.js b/web/src/auth/LarkLoginButton.js
new file mode 100644
index 00000000..f23efa6d
--- /dev/null
+++ b/web/src/auth/LarkLoginButton.js
@@ -0,0 +1,32 @@
+// Copyright 2021 The casbin Authors. All Rights Reserved.
+//
+// Licensed 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.
+
+import {createButton} from "react-social-login-buttons";
+import {StaticBaseUrl} from "../Setting";
+
+function Icon({ width = 24, height = 24, color }) {
+ return
;
+}
+
+const config = {
+ text: "Sign in with Lark",
+ icon: Icon,
+ iconFormat: name => `fa fa-${name}`,
+ style: {background: "#ffffff", color: "#000000"},
+ activeStyle: {background: "#ededee"},
+};
+
+const LarkLoginButton = createButton(config);
+
+export default LarkLoginButton;
diff --git a/web/src/auth/LoginPage.js b/web/src/auth/LoginPage.js
index 15123c41..9b09ec0d 100644
--- a/web/src/auth/LoginPage.js
+++ b/web/src/auth/LoginPage.js
@@ -31,6 +31,7 @@ import WeiboLoginButton from "./WeiboLoginButton";
import i18next from "i18next";
import LinkedInLoginButton from "./LinkedInLoginButton";
import WeComLoginButton from "./WeComLoginButton";
+import LarkLoginButton from "./LarkLoginButton";
class LoginPage extends React.Component {
constructor(props) {
@@ -167,6 +168,8 @@ class LoginPage extends React.Component {
return
} else if (type === "WeCom") {
return
+ } else if (type === "Lark") {
+ return
}
return text;
diff --git a/web/src/auth/Provider.js b/web/src/auth/Provider.js
index 13d19369..d9c64a6c 100644
--- a/web/src/auth/Provider.js
+++ b/web/src/auth/Provider.js
@@ -55,6 +55,10 @@ const LinkedInAuthLogo = `${StaticBaseUrl}/img/social_linkedin.png`;
const WeComAuthUri = "https://open.work.weixin.qq.com/wwopen/sso/3rd_qrConnect";
const WeComAuthLogo = `${StaticBaseUrl}/img/social_wecom.png`;
+// const WeComAuthScope = "";
+const LarkAuthUri = "https://open.feishu.cn/open-apis/authen/v1/index";
+const LarkAuthLogo = `${StaticBaseUrl}/img/social_lark.png`;
+
export function getAuthLogo(provider) {
if (provider.type === "Google") {
return GoogleAuthLogo;
@@ -74,8 +78,10 @@ export function getAuthLogo(provider) {
return GiteeAuthLogo;
} else if (provider.type === "LinkedIn") {
return LinkedInAuthLogo;
- } else if (provider.type === "WeCom") {
+ } else if (provider.type === "WeCom") {
return WeComAuthLogo;
+ } else if (provider.type === "Lark") {
+ return LarkAuthLogo;
}
}
@@ -104,7 +110,9 @@ export function getAuthUrl(application, provider, method) {
return `${GiteeAuthUri}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${GiteeAuthScope}&response_type=code&state=${state}`;
} else if (provider.type === "LinkedIn") {
return `${LinkedInAuthUri}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${LinkedInAuthScope}&response_type=code&state=${state}`
- } else if (provider.type === "WeCom") {
+ } else if (provider.type === "WeCom") {
return `${WeComAuthUri}?appid=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&usertype=member`
+ } else if (provider.type === "Lark") {
+ return `${LarkAuthUri}?app_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}`
}
}