feat: add wecom provider (#200)

Signed-off-by: sh1luo <690898835@qq.com>
This commit is contained in:
sh1luo 2021-07-28 20:36:27 +08:00 committed by GitHub
parent c9ae582802
commit 512a451800
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 210 additions and 1 deletions

2
go.mod
View File

@ -28,7 +28,7 @@ require (
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect
gopkg.in/ini.v1 v1.62.0 gopkg.in/ini.v1 v1.62.0 // indirect
xorm.io/core v0.7.2 xorm.io/core v0.7.2
xorm.io/xorm v1.0.3 xorm.io/xorm v1.0.3
) )

View File

@ -53,6 +53,8 @@ func GetIdProvider(providerType string, clientId string, clientSecret string, re
return NewGiteeIdProvider(clientId, clientSecret, redirectUrl) return NewGiteeIdProvider(clientId, clientSecret, redirectUrl)
} else if providerType == "LinkedIn" { } else if providerType == "LinkedIn" {
return NewLinkedInIdProvider(clientId, clientSecret, redirectUrl) return NewLinkedInIdProvider(clientId, clientSecret, redirectUrl)
} else if providerType == "WeCom" {
return NewWeComIdProvider(clientId, clientSecret, redirectUrl)
} }
return nil return nil

197
idp/wecom.go Normal file
View File

@ -0,0 +1,197 @@
// 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"
"fmt"
"io"
"io/ioutil"
"net/http"
"strings"
"time"
"golang.org/x/oauth2"
)
type WeComIdProvider struct {
Client *http.Client
Config *oauth2.Config
}
func NewWeComIdProvider(clientId string, clientSecret string, redirectUrl string) *WeComIdProvider {
idp := &WeComIdProvider{}
config := idp.getConfig(clientId, clientSecret, redirectUrl)
idp.Config = config
return idp
}
func (idp *WeComIdProvider) SetHttpClient(client *http.Client) {
idp.Client = client
}
// getConfig return a point of Config, which describes a typical 3-legged OAuth2 flow
func (idp *WeComIdProvider) getConfig(clientId string, clientSecret string, redirectUrl string) *oauth2.Config {
var endpoint = oauth2.Endpoint{
TokenURL: "https://graph.qq.com/oauth2.0/token",
}
var config = &oauth2.Config{
Scopes: []string{"snsapi_login"},
Endpoint: endpoint,
ClientID: clientId,
ClientSecret: clientSecret,
RedirectURL: redirectUrl,
}
return config
}
type WeComProviderToken struct {
Errcode int `json:"errcode"`
Errmsg string `json:"errmsg"`
ProviderAccessToken string `json:"provider_access_token"`
ExpiresIn int `json:"expires_in"`
}
// GetToken use code get access_token (*operation of getting code ought to be done in front)
// get more detail via: https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html
func (idp *WeComIdProvider) GetToken(code string) (*oauth2.Token, error) {
pTokenParams := &struct {
CorpId string `json:"corpid"`
ProviderSecret string `json:"provider_secret"`
}{idp.Config.ClientID, idp.Config.ClientSecret}
data, err := idp.postWithBody(pTokenParams, "https://qyapi.weixin.qq.com/cgi-bin/service/get_provider_token")
pToken := &WeComProviderToken{}
if err = json.Unmarshal(data, pToken); err != nil || pToken.Errcode != 0 || pToken.Errmsg != "ok" {
return nil, err
}
token := &oauth2.Token{
AccessToken: pToken.ProviderAccessToken,
Expiry: time.Unix(time.Now().Unix()+int64(pToken.ExpiresIn), 0),
}
raw := make(map[string]string)
raw["code"] = code
token = token.WithExtra(raw)
return token, nil
}
/*
{
"errcode":0,
"errmsg":"ok",
"usertype": 1,
"user_info":{
"userid":"xxxx",
"open_userid":"xxx",
"name":"xxxx",
"avatar":"xxxx"
},
"corp_info":{
"corpid":"wxCorpId",
},
"agent":[
{"agentid":0,"auth_type":1},
{"agentid":1,"auth_type":1},
{"agentid":2,"auth_type":1}
],
"auth_info":{
"department":[
{
"id":2,
"writable":true
}
]
}
}
*/
type WeComUserInfo struct {
Errcode int `json:"errcode"`
Errmsg string `json:"errmsg"`
Usertype int `json:"usertype"`
UserInfo struct {
Userid string `json:"userid"`
OpenUserid string `json:"open_userid"`
Name string `json:"name"`
Avatar string `json:"avatar"`
} `json:"user_info"`
CorpInfo struct {
Corpid string `json:"corpid"`
} `json:"corp_info"`
Agent []struct {
Agentid int `json:"agentid"`
AuthType int `json:"auth_type"`
} `json:"agent"`
AuthInfo struct {
Department []struct {
Id int `json:"id"`
Writable bool `json:"writable"`
} `json:"department"`
} `json:"auth_info"`
}
// GetUserInfo use WeComProviderToken gotten before return WeComUserInfo
// get more detail via: https://work.weixin.qq.com/api/doc/90001/90143/91125
func (idp *WeComIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
accessToken := token.AccessToken
code := token.Extra("code").(string)
requestBody := &struct {
AuthCode string `json:"auth_code"`
}{code}
data, err := idp.postWithBody(requestBody, fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/service/get_login_info?access_token=%s", accessToken))
wecomUserInfo := WeComUserInfo{}
if err = json.Unmarshal(data, &wecomUserInfo); err != nil || wecomUserInfo.Errcode != 0 {
return nil, err
}
userInfo := UserInfo{
Id: wecomUserInfo.UserInfo.OpenUserid,
Username: wecomUserInfo.UserInfo.Name,
DisplayName: wecomUserInfo.UserInfo.Name,
AvatarUrl: wecomUserInfo.UserInfo.Avatar,
}
return &userInfo, nil
}
func (idp *WeComIdProvider) 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
}

View File

@ -55,6 +55,7 @@ type User struct {
Weibo string `xorm:"weibo varchar(100)" json:"weibo"` Weibo string `xorm:"weibo varchar(100)" json:"weibo"`
Gitee string `xorm:"gitee varchar(100)" json:"gitee"` Gitee string `xorm:"gitee varchar(100)" json:"gitee"`
LinkedIn string `xorm:"linkedin varchar(100)" json:"linkedin"` LinkedIn string `xorm:"linkedin varchar(100)" json:"linkedin"`
WeCom string `xorm:"wecom varchar(100)" json:"we_com"`
Ldap string `xorm:"ldap varchar(100)" json:"ldap"` Ldap string `xorm:"ldap varchar(100)" json:"ldap"`
Properties map[string]string `json:"properties"` Properties map[string]string `json:"properties"`

View File

@ -75,6 +75,7 @@ class ProviderEditPage extends React.Component {
{id: 'Weibo', name: 'Weibo'}, {id: 'Weibo', name: 'Weibo'},
{id: 'Gitee', name: 'Gitee'}, {id: 'Gitee', name: 'Gitee'},
{id: 'LinkedIn', name: 'LinkedIn'}, {id: 'LinkedIn', name: 'LinkedIn'},
{id: 'WeCom', name: 'WeCom'},
] ]
); );
} else if (provider.category === "Email") { } else if (provider.category === "Email") {

View File

@ -51,6 +51,10 @@ const LinkedInAuthScope = "r_liteprofile%20r_emailaddress";
const LinkedInAuthUri = "https://www.linkedin.com/oauth/v2/authorization"; const LinkedInAuthUri = "https://www.linkedin.com/oauth/v2/authorization";
const LinkedInAuthLogo = `${StaticBaseUrl}/img/social_linkedin.png`; const LinkedInAuthLogo = `${StaticBaseUrl}/img/social_linkedin.png`;
// const WeComAuthScope = "";
const WeComAuthUri = "https://open.work.weixin.qq.com/wwopen/sso/3rd_qrConnect";
const WeComAuthLogo = `${StaticBaseUrl}/img/social_wecom.png`;
export function getAuthLogo(provider) { export function getAuthLogo(provider) {
if (provider.type === "Google") { if (provider.type === "Google") {
return GoogleAuthLogo; return GoogleAuthLogo;
@ -70,6 +74,8 @@ export function getAuthLogo(provider) {
return GiteeAuthLogo; return GiteeAuthLogo;
} else if (provider.type === "LinkedIn") { } else if (provider.type === "LinkedIn") {
return LinkedInAuthLogo; return LinkedInAuthLogo;
} else if (provider.type === "WeCom") {
return WeComAuthLogo;
} }
} }
@ -98,5 +104,7 @@ export function getAuthUrl(application, provider, method) {
return `${GiteeAuthUri}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${GiteeAuthScope}&response_type=code&state=${state}`; return `${GiteeAuthUri}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${GiteeAuthScope}&response_type=code&state=${state}`;
} else if (provider.type === "LinkedIn") { } else if (provider.type === "LinkedIn") {
return `${LinkedInAuthUri}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${LinkedInAuthScope}&response_type=code&state=${state}` return `${LinkedInAuthUri}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${LinkedInAuthScope}&response_type=code&state=${state}`
} else if (provider.type === "WeCom") {
return `${WeComAuthUri}?appid=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&usertype=member`
} }
} }