From b92d03e2bb43bae724a0bcb465b8d3fb219b1e2e Mon Sep 17 00:00:00 2001 From: Yi Zhan Date: Fri, 15 Apr 2022 11:49:56 +0800 Subject: [PATCH] feat: add wechat mini program support (#658) * feat: add wechat mini program support Signed-off-by: Steve0x2a * fix: accept suggestions. Signed-off-by: Steve0x2a * fix: error message and code level modification Signed-off-by: Steve0x2a * fix: simplify the use process Signed-off-by: Steve0x2a --- controllers/token.go | 6 ++- controllers/types.go | 2 + controllers/user.go | 4 ++ idp/wechat_miniprogram.go | 82 ++++++++++++++++++++++++++++++++++++++ object/provider.go | 10 +++++ object/token.go | 83 ++++++++++++++++++++++++++++++++++++++- object/user.go | 60 ++++++++++++++++++---------- web/src/Setting.js | 5 +++ web/src/auth/Provider.js | 3 ++ 9 files changed, 231 insertions(+), 24 deletions(-) create mode 100644 idp/wechat_miniprogram.go diff --git a/controllers/token.go b/controllers/token.go index d25e6227..51e794bc 100644 --- a/controllers/token.go +++ b/controllers/token.go @@ -175,6 +175,8 @@ func (c *ApiController) GetOAuthToken() { scope := c.Input().Get("scope") username := c.Input().Get("username") password := c.Input().Get("password") + tag := c.Input().Get("tag") + avatar := c.Input().Get("avatar") if clientId == "" && clientSecret == "" { clientId, clientSecret, _ = c.Ctx.Request.BasicAuth() @@ -191,11 +193,13 @@ func (c *ApiController) GetOAuthToken() { scope = tokenRequest.Scope username = tokenRequest.Username password = tokenRequest.Password + tag = tokenRequest.Tag + avatar = tokenRequest.Avatar } } host := c.Ctx.Request.Host - c.Data["json"] = object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, username, password, host) + c.Data["json"] = object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, username, password, host, tag, avatar) c.ServeJSON() } diff --git a/controllers/types.go b/controllers/types.go index 47ab0acf..84f1b1fe 100644 --- a/controllers/types.go +++ b/controllers/types.go @@ -23,4 +23,6 @@ type TokenRequest struct { Scope string `json:"scope"` Username string `json:"username"` Password string `json:"password"` + Tag string `json:"tag"` + Avatar string `json:"avatar"` } diff --git a/controllers/user.go b/controllers/user.go index d9700194..26b9e6c4 100644 --- a/controllers/user.go +++ b/controllers/user.go @@ -111,6 +111,10 @@ func (c *ApiController) UpdateUser() { id := c.Input().Get("id") columnsStr := c.Input().Get("columns") + if id == "" { + id = c.GetSessionUsername() + } + var user object.User err := json.Unmarshal(c.Ctx.Input.RequestBody, &user) if err != nil { diff --git a/idp/wechat_miniprogram.go b/idp/wechat_miniprogram.go new file mode 100644 index 00000000..dd9de6c1 --- /dev/null +++ b/idp/wechat_miniprogram.go @@ -0,0 +1,82 @@ +// Copyright 2022 The Casdoor 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/ioutil" + "net/http" + + "golang.org/x/oauth2" +) + +type WeChatMiniProgramIdProvider struct { + Client *http.Client + Config *oauth2.Config +} + +func NewWeChatMiniProgramIdProvider(clientId string, clientSecret string) *WeChatMiniProgramIdProvider { + idp := &WeChatMiniProgramIdProvider{} + + config := idp.getConfig(clientId, clientSecret) + idp.Config = config + idp.Client = &http.Client{} + return idp +} + +func (idp *WeChatMiniProgramIdProvider) SetHttpClient(client *http.Client) { + idp.Client = client +} + +func (idp *WeChatMiniProgramIdProvider) getConfig(clientId string, clientSecret string) *oauth2.Config { + var config = &oauth2.Config{ + ClientID: clientId, + ClientSecret: clientSecret, + } + + return config +} + +type WeChatMiniProgramSessionResponse struct { + Openid string `json:"openid"` + SessionKey string `json:"session_key"` + Unionid string `json:"unionid"` + Errcode int `json:"errcode"` + Errmsg string `json:"errmsg"` +} + +func (idp *WeChatMiniProgramIdProvider) GetSessionByCode(code string) (*WeChatMiniProgramSessionResponse, error) { + sessionUri := fmt.Sprintf("https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code", idp.Config.ClientID, idp.Config.ClientSecret, code) + sessionResponse, err := idp.Client.Get(sessionUri) + if err != nil { + return nil, err + } + defer sessionResponse.Body.Close() + data, err := ioutil.ReadAll(sessionResponse.Body) + if err != nil { + return nil, err + } + var session WeChatMiniProgramSessionResponse + err = json.Unmarshal(data, &session) + if err != nil { + return nil, err + } + if session.Errcode != 0 { + return nil, fmt.Errorf("err: %s", session.Errmsg) + } + return &session, nil + +} diff --git a/object/provider.go b/object/provider.go index 8d05083d..2a403a55 100644 --- a/object/provider.go +++ b/object/provider.go @@ -151,6 +151,16 @@ func GetDefaultHumanCheckProvider() *Provider { return &provider } +func GetWechatMiniProgramProvider(application *Application) *Provider { + providers := application.Providers + for _, provider := range providers { + if provider.Provider.Type == "WeChatMiniProgram" { + return provider.Provider + } + } + return nil +} + func UpdateProvider(id string, provider *Provider) bool { owner, name := util.GetOwnerAndNameFromId(id) if getProvider(owner, name) == nil { diff --git a/object/token.go b/object/token.go index 0ad6a514..5ab6021c 100644 --- a/object/token.go +++ b/object/token.go @@ -22,6 +22,7 @@ import ( "strings" "time" + "github.com/casdoor/casdoor/idp" "github.com/casdoor/casdoor/util" "xorm.io/core" ) @@ -306,7 +307,8 @@ func GetOAuthCode(userId string, clientId string, responseType string, redirectU } } -func GetOAuthToken(grantType string, clientId string, clientSecret string, code string, verifier string, scope string, username string, password string, host string) *TokenWrapper { + +func GetOAuthToken(grantType string, clientId string, clientSecret string, code string, verifier string, scope string, username string, password string, host string, tag string, avatar string) *TokenWrapper { var errString string application := GetApplicationByClientId(clientId) if application == nil { @@ -321,7 +323,8 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code } //Check if grantType is allowed in the current application - if !IsGrantTypeValid(grantType, application.GrantTypes) { + + if !IsGrantTypeValid(grantType, application.GrantTypes) && tag == "" { errString = fmt.Sprintf("error: grant_type: %s is not supported in this application", grantType) return &TokenWrapper{ AccessToken: errString, @@ -343,6 +346,11 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code token, err = GetClientCredentialsToken(application, clientSecret, scope, host) } + if tag == "wechat_miniprogram" { + // Wechat Mini Program + token, err = GetWechatMiniProgramToken(application, code, host, username, avatar) + } + if err != nil { errString = err.Error() return &TokenWrapper{ @@ -629,3 +637,74 @@ func GetTokenByUser(application *Application, user *User, scope string, host str AddToken(token) return token, nil } + +// Wechat Mini Program flow +func GetWechatMiniProgramToken(application *Application, code string, host string, username string, avatar string) (*Token, error) { + mpProvider := GetWechatMiniProgramProvider(application) + if mpProvider == nil { + return nil, errors.New("error: the application does not support wechat mini program") + } + provider := GetProvider(util.GetId(mpProvider.Name)) + mpIdp := idp.NewWeChatMiniProgramIdProvider(provider.ClientId, provider.ClientSecret) + session, err := mpIdp.GetSessionByCode(code) + if err != nil { + return nil, err + } + openId, unionId := session.Openid, session.Unionid + if openId == "" && unionId == "" { + return nil, errors.New("err: WeChat's openid and unionid are empty") + } + user := getUserByWechatId(openId, unionId) + if user == nil { + if !application.EnableSignUp { + return nil, errors.New("err: the application does not allow to sign up new account") + } + //Add new user + var name string + if username != "" { + name = username + } else { + name = fmt.Sprintf("wechat-%s", openId) + } + + user = &User{ + Owner: application.Organization, + Id: util.GenerateId(), + Name: name, + Avatar: avatar, + SignupApplication: application.Name, + WeChat: openId, + WeChatUnionId: unionId, + Type: "normal-user", + CreatedTime: util.GetCurrentTime(), + IsAdmin: false, + IsGlobalAdmin: false, + IsForbidden: false, + IsDeleted: false, + } + AddUser(user) + } + + accessToken, refreshToken, err := generateJwtToken(application, user, "", "", host) + if err != nil { + return nil, err + } + + token := &Token{ + Owner: application.Owner, + Name: util.GenerateId(), + CreatedTime: util.GetCurrentTime(), + Application: application.Name, + Organization: user.Owner, + User: user.Name, + Code: session.SessionKey, //a trick, because miniprogram does not use the code, so use the code field to save the session_key + AccessToken: accessToken, + RefreshToken: refreshToken, + ExpiresIn: application.ExpireInHours * 60, + Scope: "", + TokenType: "Bearer", + CodeIsUsed: true, + } + AddToken(token) + return token, nil +} diff --git a/object/user.go b/object/user.go index 9ce3088e..e75aa688 100644 --- a/object/user.go +++ b/object/user.go @@ -72,27 +72,28 @@ type User struct { LastSigninTime string `xorm:"varchar(100)" json:"lastSigninTime"` LastSigninIp string `xorm:"varchar(100)" json:"lastSigninIp"` - Github string `xorm:"varchar(100)" json:"github"` - Google string `xorm:"varchar(100)" json:"google"` - QQ string `xorm:"qq varchar(100)" json:"qq"` - WeChat string `xorm:"wechat varchar(100)" json:"wechat"` - Facebook string `xorm:"facebook varchar(100)" json:"facebook"` - DingTalk string `xorm:"dingtalk varchar(100)" json:"dingtalk"` - Weibo string `xorm:"weibo varchar(100)" json:"weibo"` - 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"` - Gitlab string `xorm:"gitlab varchar(100)" json:"gitlab"` - Adfs string `xorm:"adfs varchar(100)" json:"adfs"` - Baidu string `xorm:"baidu varchar(100)" json:"baidu"` - Alipay string `xorm:"alipay varchar(100)" json:"alipay"` - Casdoor string `xorm:"casdoor varchar(100)" json:"casdoor"` - Infoflow string `xorm:"infoflow varchar(100)" json:"infoflow"` - Apple string `xorm:"apple varchar(100)" json:"apple"` - AzureAD string `xorm:"azuread varchar(100)" json:"azuread"` - Slack string `xorm:"slack varchar(100)" json:"slack"` - Steam string `xorm:"steam varchar(100)" json:"steam"` + Github string `xorm:"varchar(100)" json:"github"` + Google string `xorm:"varchar(100)" json:"google"` + QQ string `xorm:"qq varchar(100)" json:"qq"` + WeChat string `xorm:"wechat varchar(100)" json:"wechat"` + WeChatUnionId string `xorm:"varchar(100)" json:"unionId"` + Facebook string `xorm:"facebook varchar(100)" json:"facebook"` + DingTalk string `xorm:"dingtalk varchar(100)" json:"dingtalk"` + Weibo string `xorm:"weibo varchar(100)" json:"weibo"` + 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"` + Gitlab string `xorm:"gitlab varchar(100)" json:"gitlab"` + Adfs string `xorm:"adfs varchar(100)" json:"adfs"` + Baidu string `xorm:"baidu varchar(100)" json:"baidu"` + Alipay string `xorm:"alipay varchar(100)" json:"alipay"` + Casdoor string `xorm:"casdoor varchar(100)" json:"casdoor"` + Infoflow string `xorm:"infoflow varchar(100)" json:"infoflow"` + Apple string `xorm:"apple varchar(100)" json:"apple"` + AzureAD string `xorm:"azuread varchar(100)" json:"azuread"` + Slack string `xorm:"slack varchar(100)" json:"slack"` + Steam string `xorm:"steam varchar(100)" json:"steam"` Ldap string `xorm:"ldap varchar(100)" json:"ldap"` Properties map[string]string `json:"properties"` @@ -227,6 +228,23 @@ func getUserById(owner string, id string) *User { } } +func getUserByWechatId(wechatOpenId string, wechatUnionId string) *User { + if wechatUnionId == "" { + wechatUnionId = wechatOpenId + } + user := &User{} + existed, err := adapter.Engine.Where("wechat = ? OR wechat = ? OR unionid = ?", wechatOpenId, wechatUnionId, wechatUnionId).Get(user) + if err != nil { + panic(err) + } + + if existed { + return user + } else { + return nil + } +} + func GetUserByEmail(owner string, email string) *User { if owner == "" || email == "" { return nil diff --git a/web/src/Setting.js b/web/src/Setting.js index c214a6b5..977105c2 100644 --- a/web/src/Setting.js +++ b/web/src/Setting.js @@ -76,6 +76,10 @@ export function isProviderVisible(providerItem) { return false; } + if (providerItem.provider.type === "WeChatMiniProgram"){ + return false + } + return true; } @@ -392,6 +396,7 @@ export function getProviderTypeOptions(category) { {id: 'GitHub', name: 'GitHub'}, {id: 'QQ', name: 'QQ'}, {id: 'WeChat', name: 'WeChat'}, + {id: 'WeChatMiniProgram', name: 'WeChat Mini Program'}, {id: 'Facebook', name: 'Facebook'}, {id: 'DingTalk', name: 'DingTalk'}, {id: 'Weibo', name: 'Weibo'}, diff --git a/web/src/auth/Provider.js b/web/src/auth/Provider.js index 12999e42..401d600b 100644 --- a/web/src/auth/Provider.js +++ b/web/src/auth/Provider.js @@ -36,6 +36,9 @@ const authInfo = { mpScope: "snsapi_userinfo", mpEndpoint: "https://open.weixin.qq.com/connect/oauth2/authorize" }, + WeChatMiniProgram: { + endpoint: "https://mp.weixin.qq.com/", + }, Facebook: { scope: "email,public_profile", endpoint: "https://www.facebook.com/dialog/oauth",