Skip to content

Commit aa6a4dc

Browse files
authored
feat: support login by following wechat official account (casdoor#1284)
* show QRcode when click WeChat Icon * update how to show qrcode * handle wechat scan qrcode * fix api problems * fix url problems * fix problems * modify get frequency * remove useless print * fix:fix PR problems * fix: fix PR problems * fix:fix PR problem * fix IMG load delay problems * fix:fix provider problems * fix test problems * use gofumpt to fmt code * fix:delete useless variables * feat:add button for follow official account * fix:fix review problems * use gofumpt to fmt code * fix:fix scantype problems * fix Response problem * use gofumpt to format code
1 parent 462a82a commit aa6a4dc

File tree

19 files changed

+313
-11
lines changed

19 files changed

+313
-11
lines changed

authz/authz.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ p, *, *, POST, /api/logout, *, *
8585
p, *, *, GET, /api/logout, *, *
8686
p, *, *, GET, /api/get-account, *, *
8787
p, *, *, GET, /api/userinfo, *, *
88+
p, *, *, POST, /api/webhook, *, *
89+
p, *, *, GET, /api/get-webhook-event, *, *
8890
p, *, *, *, /api/login/oauth, *, *
8991
p, *, *, GET, /api/get-application, *, *
9092
p, *, *, GET, /api/get-organization-applications, *, *

controllers/auth.go

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,16 @@ package controllers
1717
import (
1818
"encoding/base64"
1919
"encoding/json"
20+
"encoding/xml"
2021
"fmt"
22+
"io/ioutil"
2123
"net/url"
2224
"strconv"
2325
"strings"
26+
"sync"
2427
"time"
2528

2629
"github.com/casdoor/casdoor/captcha"
27-
2830
"github.com/casdoor/casdoor/conf"
2931
"github.com/casdoor/casdoor/idp"
3032
"github.com/casdoor/casdoor/object"
@@ -33,6 +35,11 @@ import (
3335
"github.com/google/uuid"
3436
)
3537

38+
var (
39+
wechatScanType string
40+
lock sync.RWMutex
41+
)
42+
3643
func codeToResponse(code *object.Code) *Response {
3744
if code.Code == "" {
3845
return &Response{Status: "error", Msg: code.Message, Data: code.Code}
@@ -531,3 +538,46 @@ func (c *ApiController) HandleSamlLogin() {
531538
slice[4], relayState, samlResponse)
532539
c.Redirect(targetUrl, 303)
533540
}
541+
542+
// HandleOfficialAccountEvent ...
543+
// @Tag HandleOfficialAccountEvent API
544+
// @Title HandleOfficialAccountEvent
545+
// @router /api/webhook [POST]
546+
func (c *ApiController) HandleOfficialAccountEvent() {
547+
respBytes, err := ioutil.ReadAll(c.Ctx.Request.Body)
548+
if err != nil {
549+
c.ResponseError(err.Error())
550+
}
551+
var data struct {
552+
MsgType string `xml:"MsgType"`
553+
Event string `xml:"Event"`
554+
EventKey string `xml:"EventKey"`
555+
}
556+
err = xml.Unmarshal(respBytes, &data)
557+
if err != nil {
558+
c.ResponseError(err.Error())
559+
}
560+
lock.Lock()
561+
defer lock.Unlock()
562+
if data.EventKey != "" {
563+
wechatScanType = data.Event
564+
c.Ctx.WriteString("")
565+
}
566+
}
567+
568+
// GetWebhookEventType ...
569+
// @Tag GetWebhookEventType API
570+
// @Title GetWebhookEventType
571+
// @router /api/get-webhook-event [GET]
572+
func (c *ApiController) GetWebhookEventType() {
573+
lock.Lock()
574+
defer lock.Unlock()
575+
resp := &Response{
576+
Status: "ok",
577+
Msg: "",
578+
Data: wechatScanType,
579+
}
580+
c.Data["json"] = resp
581+
wechatScanType = ""
582+
c.ServeJSON()
583+
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ require (
3636
github.com/russellhaering/goxmldsig v1.1.1
3737
github.com/satori/go.uuid v1.2.0
3838
github.com/shirou/gopsutil v3.21.11+incompatible
39+
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
3940
github.com/smartystreets/goconvey v1.6.4 // indirect
4041
github.com/stretchr/testify v1.8.0
4142
github.com/tealeg/xlsx v1.0.5

go.sum

Lines changed: 109 additions & 0 deletions
Large diffs are not rendered by default.

idp/wechat.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,17 @@ package idp
1616

1717
import (
1818
"bytes"
19+
"encoding/base64"
1920
"encoding/json"
2021
"fmt"
2122
"io"
23+
"io/ioutil"
2224
"net/http"
2325
"net/url"
2426
"strings"
2527
"time"
2628

29+
"github.com/skip2/go-qrcode"
2730
"golang.org/x/oauth2"
2831
)
2932

@@ -191,3 +194,54 @@ func (idp *WeChatIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error)
191194
}
192195
return &userInfo, nil
193196
}
197+
198+
func GetWechatOfficialAccountAccessToken(clientId string, clientSecret string) (string, error) {
199+
accessTokenUrl := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", clientId, clientSecret)
200+
request, err := http.NewRequest("GET", accessTokenUrl, nil)
201+
client := new(http.Client)
202+
resp, err := client.Do(request)
203+
respBytes, err := ioutil.ReadAll(resp.Body)
204+
if err != nil {
205+
return "", err
206+
}
207+
var data struct {
208+
ExpireIn int `json:"expires_in"`
209+
AccessToken string `json:"access_token"`
210+
}
211+
err = json.Unmarshal(respBytes, &data)
212+
if err != nil {
213+
return "", err
214+
}
215+
return data.AccessToken, nil
216+
}
217+
218+
func GetWechatOfficialAccountQRCode(clientId string, clientSecret string) (string, error) {
219+
accessToken, err := GetWechatOfficialAccountAccessToken(clientId, clientSecret)
220+
client := new(http.Client)
221+
params := "{\"action_name\": \"QR_LIMIT_STR_SCENE\", \"action_info\": {\"scene\": {\"scene_str\": \"test\"}}}"
222+
bodyData := bytes.NewReader([]byte(params))
223+
qrCodeUrl := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=%s", accessToken)
224+
requeset, err := http.NewRequest("POST", qrCodeUrl, bodyData)
225+
resp, err := client.Do(requeset)
226+
if err != nil {
227+
return "", err
228+
}
229+
respBytes, err := ioutil.ReadAll(resp.Body)
230+
if err != nil {
231+
return "", err
232+
}
233+
var data struct {
234+
Ticket string `json:"ticket"`
235+
ExpireSeconds int `json:"expire_seconds"`
236+
URL string `json:"url"`
237+
}
238+
err = json.Unmarshal(respBytes, &data)
239+
if err != nil {
240+
return "", err
241+
}
242+
243+
var png []byte
244+
png, err = qrcode.Encode(data.URL, qrcode.Medium, 256)
245+
base64Image := base64.StdEncoding.EncodeToString(png)
246+
return base64Image, nil
247+
}

object/application.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"regexp"
2121
"strings"
2222

23+
"github.com/casdoor/casdoor/idp"
2324
"github.com/casdoor/casdoor/util"
2425
"xorm.io/core"
2526
)
@@ -119,9 +120,11 @@ func getProviderMap(owner string) map[string]*Provider {
119120
providers := GetProviders(owner)
120121
m := map[string]*Provider{}
121122
for _, provider := range providers {
122-
//if provider.Category != "OAuth" {
123-
// continue
124-
//}
123+
// Get QRCode only once
124+
if provider.Type == "WeChat" && provider.DisableSsl == true && provider.Content == "" {
125+
provider.Content, _ = idp.GetWechatOfficialAccountQRCode(provider.ClientId2, provider.ClientSecret2)
126+
UpdateProvider(provider.Owner+"/"+provider.Name, provider)
127+
}
125128

126129
m[provider.Name] = GetMaskedProvider(provider)
127130
}

object/provider.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ type Provider struct {
4646

4747
Host string `xorm:"varchar(100)" json:"host"`
4848
Port int `json:"port"`
49-
DisableSsl bool `json:"disableSsl"`
49+
DisableSsl bool `json:"disableSsl"` // If the provider type is WeChat, DisableSsl means EnableQRCode
5050
Title string `xorm:"varchar(100)" json:"title"`
51-
Content string `xorm:"varchar(1000)" json:"content"`
51+
Content string `xorm:"varchar(1000)" json:"content"` // If provider type is WeChat, Content means QRCode string by Base64 encoding
5252
Receiver string `xorm:"varchar(100)" json:"receiver"`
5353

5454
RegionId string `xorm:"varchar(100)" json:"regionId"`

routers/router.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ func initAPI() {
5454
beego.Router("/api/get-saml-login", &controllers.ApiController{}, "GET:GetSamlLogin")
5555
beego.Router("/api/acs", &controllers.ApiController{}, "POST:HandleSamlLogin")
5656
beego.Router("/api/saml/metadata", &controllers.ApiController{}, "GET:GetSamlMeta")
57+
beego.Router("/api/webhook", &controllers.ApiController{}, "POST:HandleOfficialAccountEvent")
58+
beego.Router("/api/get-webhook-event", &controllers.ApiController{}, "GET:GetWebhookEventType")
5759

5860
beego.Router("/api/get-organizations", &controllers.ApiController{}, "GET:GetOrganizations")
5961
beego.Router("/api/get-organization", &controllers.ApiController{}, "GET:GetOrganization")

web/src/ProviderEditPage.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,20 @@ class ProviderEditPage extends React.Component {
424424
</React.Fragment>
425425
)
426426
}
427+
{
428+
this.state.provider.type !== "WeChat" ? null : (
429+
<Row style={{marginTop: "20px"}} >
430+
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
431+
{Setting.getLabel(i18next.t("provider:Enable QR code"), i18next.t("provider:Enable QR code - Tooltip"))} :
432+
</Col>
433+
<Col span={1} >
434+
<Switch checked={this.state.provider.disableSsl} onChange={checked => {
435+
this.updateProviderField("disableSsl", checked);
436+
}} />
437+
</Col>
438+
</Row>
439+
)
440+
}
427441
{
428442
this.state.provider.type !== "Adfs" && this.state.provider.type !== "Casdoor" && this.state.provider.type !== "Okta" ? null : (
429443
<Row style={{marginTop: "20px"}} >

web/src/auth/AuthBackend.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,13 @@ export function loginWithSaml(values, param) {
130130
},
131131
}).then(res => res.json());
132132
}
133+
134+
export function getWechatMessageEvent() {
135+
return fetch(`${Setting.ServerUrl}/api/get-webhook-event`, {
136+
method: "GET",
137+
credentials: "include",
138+
headers: {
139+
"Accept-Language": Setting.getAcceptLanguage(),
140+
},
141+
}).then(res => res.json());
142+
}

0 commit comments

Comments
 (0)