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 Sign in with Lark; +} + +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}` } }