From eb15afec3430a87ce526400fb7e23ae5670c816d Mon Sep 17 00:00:00 2001 From: Steve0x2a Date: Thu, 10 Feb 2022 17:14:18 +0800 Subject: [PATCH] fix: use new dingtalk api and support qrcode method (#486) Signed-off-by: Steve0x2a --- idp/dingtalk.go | 320 ++++++++--------------------------- web/src/auth/AuthCallback.js | 4 + web/src/auth/Provider.js | 6 +- 3 files changed, 81 insertions(+), 249 deletions(-) diff --git a/idp/dingtalk.go b/idp/dingtalk.go index 1ad340ae..b0fdfe33 100644 --- a/idp/dingtalk.go +++ b/idp/dingtalk.go @@ -15,32 +15,16 @@ package idp import ( - "bytes" - "crypto/hmac" - "crypto/sha256" - "encoding/base64" "encoding/json" "fmt" "io" "net/http" - "net/url" - "strconv" "strings" "time" "golang.org/x/oauth2" ) -// A total of three steps are required: -// -// 1. Construct the link and get the temporary authorization code -// tmp_auth_code through the code at the end of the url. -// -// 2. Use hmac256 to calculate the signature, and then submit it together with timestamp, -// tmp_auth_code, accessKey to obtain unionid, userid, accessKey. -// -// 3. Get detailed information through userid. - type DingTalkIdProvider struct { Client *http.Client Config *oauth2.Config @@ -64,8 +48,8 @@ func (idp *DingTalkIdProvider) SetHttpClient(client *http.Client) { // getConfig return a point of Config, which describes a typical 3-legged OAuth2 flow func (idp *DingTalkIdProvider) getConfig(clientId string, clientSecret string, redirectUrl string) *oauth2.Config { var endpoint = oauth2.Endpoint{ - AuthURL: "https://oapi.dingtalk.com/sns/getuserinfo_bycode", - TokenURL: "https://oapi.dingtalk.com/gettoken", + AuthURL: "https://api.dingtalk.com/v1.0/contact/users/me", + TokenURL: "https://api.dingtalk.com/v1.0/oauth2/userAccessToken", } var config = &oauth2.Config{ @@ -83,256 +67,121 @@ func (idp *DingTalkIdProvider) getConfig(clientId string, clientSecret string, r } type DingTalkAccessToken struct { - ErrCode int `json:"errcode"` - ErrMsg string `json:"errmsg"` - AccessToken string `json:"access_token"` // Interface call credentials - ExpiresIn int64 `json:"expires_in"` // access_token interface call credential timeout time, unit (seconds) + ErrCode int `json:"code"` + ErrMsg string `json:"message"` + AccessToken string `json:"accessToken"` // Interface call credentials + ExpiresIn int64 `json:"expireIn"` // access_token interface call credential timeout time, unit (seconds) } -type DingTalkIds struct { - UserId string `json:"user_id"` - UnionId string `json:"union_id"` -} - -type InfoResp struct { - Errcode int `json:"errcode"` - UserInfo struct { - Nick string `json:"nick"` - Unionid string `json:"unionid"` - Openid string `json:"openid"` - MainOrgAuthHighLevel bool `json:"main_org_auth_high_level"` - } `json:"user_info"` - Errmsg string `json:"errmsg"` -} - -// GetToken use code get access_token (*operation of getting code ought to be done in front) -// get more detail via: https://developers.dingtalk.com/document/app/dingtalk-retrieve-user-information?spm=ding_open_doc.document.0.0.51b91a31wWV3tY#doc-api-dingtalk-GetUser +// GetToken use code get access_token (*operation of getting authCode ought to be done in front) +// get more detail via: https://open.dingtalk.com/document/orgapp-server/obtain-user-token func (idp *DingTalkIdProvider) GetToken(code string) (*oauth2.Token, error) { - timestamp := strconv.FormatInt(time.Now().UnixNano()/1e6, 10) - signature := EncodeSHA256(timestamp, idp.Config.ClientSecret) - u := fmt.Sprintf( - "%s?accessKey=%s×tamp=%s&signature=%s", idp.Config.Endpoint.AuthURL, - idp.Config.ClientID, timestamp, signature) + pTokenParams := &struct { + ClientId string `json:"clientId"` + ClientSecret string `json:"clientSecret"` + Code string `json:"code"` + GrantType string `json:"grantType"` + }{idp.Config.ClientID, idp.Config.ClientSecret, code, "authorization_code"} - tmpCode := struct { - TmpAuthCode string `json:"tmp_auth_code"` - }{code} - bs, _ := json.Marshal(tmpCode) - r := strings.NewReader(string(bs)) - resp, err := http.Post(u, "application/json;charset=UTF-8", r) + data, err := idp.postWithBody(pTokenParams, idp.Config.Endpoint.TokenURL) if err != nil { return nil, err } - defer func(Body io.ReadCloser) { - err := Body.Close() - if err != nil { - return - } - }(resp.Body) - - body, _ := io.ReadAll(resp.Body) - info := InfoResp{} - _ = json.Unmarshal(body, &info) - errCode := info.Errcode - if errCode != 0 { - return nil, fmt.Errorf("%d: %s", errCode, info.Errmsg) - } - - u2 := fmt.Sprintf("%s?appkey=%s&appsecret=%s", idp.Config.Endpoint.TokenURL, idp.Config.ClientID, idp.Config.ClientSecret) - resp, _ = http.Get(u2) - defer func(Body io.ReadCloser) { - err := Body.Close() - if err != nil { - return - } - }(resp.Body) - body, _ = io.ReadAll(resp.Body) - tokenResp := DingTalkAccessToken{} - _ = json.Unmarshal(body, &tokenResp) - if tokenResp.ErrCode != 0 { - return nil, fmt.Errorf("%d: %s", tokenResp.ErrCode, tokenResp.ErrMsg) - } - - // use unionid to get userid - unionid := info.UserInfo.Unionid - userid, err := idp.GetUseridByUnionid(tokenResp.AccessToken, unionid) + pToken := &DingTalkAccessToken{} + err = json.Unmarshal(data, pToken) if err != nil { return nil, err } - // Since DingTalk does not require scopes, put userid and unionid into - // idp.config.scopes to facilitate GetUserInfo() to obtain these two parameters. - idp.Config.Scopes = []string{unionid, userid} + if pToken.ErrCode != 0 { + return nil, fmt.Errorf("pToken.Errcode = %d, pToken.Errmsg = %s", pToken.ErrCode, pToken.ErrMsg) + } token := &oauth2.Token{ - AccessToken: tokenResp.AccessToken, - Expiry: time.Unix(time.Now().Unix()+tokenResp.ExpiresIn, 0), + AccessToken: pToken.AccessToken, + Expiry: time.Unix(time.Now().Unix()+int64(pToken.ExpiresIn), 0), } - return token, nil } -type UnionIdResponse struct { - Errcode int `json:"errcode"` - Errmsg string `json:"errmsg"` - Result struct { - ContactType string `json:"contact_type"` - Userid string `json:"userid"` - } `json:"result"` - RequestId string `json:"request_id"` -} - -// GetUseridByUnionid ... -func (idp *DingTalkIdProvider) GetUseridByUnionid(accesstoken, unionid string) (userid string, err error) { - u := fmt.Sprintf("https://oapi.dingtalk.com/topapi/user/getbyunionid?access_token=%s&unionid=%s", - accesstoken, unionid) - useridInfo, err := idp.GetUrlResp(u) - if err != nil { - return "", err - } - - uresp := UnionIdResponse{} - _ = json.Unmarshal([]byte(useridInfo), &uresp) - errcode := uresp.Errcode - if errcode != 0 { - return "", fmt.Errorf("%d: %s", errcode, uresp.Errmsg) - } - return uresp.Result.Userid, nil -} - /* { - "errcode":0, - "result":{ - "boss":false, - "unionid":"5M6zgZBKQPCxdiPdANeJ6MgiEiE", - "role_list":[ - { - "group_name":"默认", - "name":"主管理员", - "id":2062489174 - } - ], - "exclusive_account":false, - "mobile":"15236176076", - "active":true, - "admin":true, - "avatar":"https://static-legacy.dingtalk.com/media/lALPDeRETW9WAnnNAyDNAyA_800_800.png", - "hide_mobile":false, - "userid":"manager4713", - "senior":false, - "dept_order_list":[ - { - "dept_id":1, - "order":176294576350761512 - } - ], - "real_authed":true, - "name":"刘继坤", - "dept_id_list":[ - 1 - ], - "state_code":"86", - "email":"", - "leader_in_dept":[ - { - "leader":false, - "dept_id":1 - } - ] - }, - "errmsg":"ok", - "request_id":"3sug9d2exsla" +{ + "nick" : "zhangsan", + "avatarUrl" : "https://xxx", + "mobile" : "150xxxx9144", + "openId" : "123", + "unionId" : "z21HjQliSzpw0Yxxxx", + "email" : "zhangsan@alibaba-inc.com", + "stateCode" : "86" } */ type DingTalkUserResponse struct { - Errcode int `json:"errcode"` - Errmsg string `json:"errmsg"` - Result struct { - Extension string `json:"extension"` - Unionid string `json:"unionid"` - Boss bool `json:"boss"` - UnionEmpExt struct { - CorpId string `json:"corpId"` - Userid string `json:"userid"` - UnionEmpMapList []struct { - CorpId string `json:"corpId"` - Userid string `json:"userid"` - } `json:"unionEmpMapList"` - } `json:"unionEmpExt"` - RoleList []struct { - GroupName string `json:"group_name"` - Id int `json:"id"` - Name string `json:"name"` - } `json:"role_list"` - Admin bool `json:"admin"` - Remark string `json:"remark"` - Title string `json:"title"` - HiredDate int64 `json:"hired_date"` - Userid string `json:"userid"` - WorkPlace string `json:"work_place"` - DeptOrderList []struct { - DeptId int `json:"dept_id"` - Order int64 `json:"order"` - } `json:"dept_order_list"` - RealAuthed bool `json:"real_authed"` - DeptIdList []int `json:"dept_id_list"` - JobNumber string `json:"job_number"` - Email string `json:"email"` - LeaderInDept []struct { - DeptId int `json:"dept_id"` - Leader bool `json:"leader"` - } `json:"leader_in_dept"` - ManagerUserid string `json:"manager_userid"` - Mobile string `json:"mobile"` - Active bool `json:"active"` - Telephone string `json:"telephone"` - Avatar string `json:"avatar"` - HideMobile bool `json:"hide_mobile"` - Senior bool `json:"senior"` - Name string `json:"name"` - StateCode string `json:"state_code"` - } `json:"result"` - RequestId string `json:"request_id"` + Nick string `json:"nick"` + OpenId string `json:"openId"` + AvatarUrl string `json:"avatarUrl"` + Email string `json:"email"` + Errmsg string `json:"message"` + Errcode string `json:"code"` } -// GetUserInfo Use userid and access_token to get UserInfo -// get more detail via: https://developers.dingtalk.com/document/app/query-user-details +// GetUserInfo Use access_token to get UserInfo +// get more detail via: https://open.dingtalk.com/document/orgapp-server/dingtalk-retrieve-user-information func (idp *DingTalkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) { - var dtUserInfo DingTalkUserResponse + dtUserInfo := &DingTalkUserResponse{} accessToken := token.AccessToken - u := fmt.Sprintf("https://oapi.dingtalk.com/topapi/v2/user/get?access_token=%s&userid=%s", - accessToken, idp.Config.Scopes[1]) - - userinfoResp, err := idp.GetUrlResp(u) + reqest, err := http.NewRequest("GET", idp.Config.Endpoint.AuthURL, nil) + if err != nil { + return nil, err + } + reqest.Header.Add("x-acs-dingtalk-access-token", accessToken) + resp, err := idp.Client.Do(reqest) if err != nil { return nil, err } - if err = json.Unmarshal([]byte(userinfoResp), &dtUserInfo); err != nil { + data, err := io.ReadAll(resp.Body) + if err != nil { return nil, err } + err = json.Unmarshal(data, dtUserInfo) + if err != nil { + return nil, err + } + + if dtUserInfo.Errmsg != "" { + return nil, fmt.Errorf("userIdResp.Errcode = %s, userIdResp.Errmsg = %s", dtUserInfo.Errcode, dtUserInfo.Errmsg) + } + userInfo := UserInfo{ - Id: strconv.Itoa(dtUserInfo.Result.RoleList[0].Id), - Username: dtUserInfo.Result.RoleList[0].Name, - DisplayName: dtUserInfo.Result.Name, - Email: dtUserInfo.Result.Email, - AvatarUrl: dtUserInfo.Result.Avatar, + Id: dtUserInfo.OpenId, + Username: dtUserInfo.Nick, + DisplayName: dtUserInfo.Nick, + Email: dtUserInfo.Email, + AvatarUrl: dtUserInfo.AvatarUrl, } return &userInfo, nil } -func (idp *DingTalkIdProvider) GetUrlResp(url string) (string, error) { - resp, err := idp.Client.Get(url) +func (idp *DingTalkIdProvider) postWithBody(body interface{}, url string) ([]byte, error) { + bs, err := json.Marshal(body) if err != nil { - return "", err + 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 := io.ReadAll(resp.Body) + if err != nil { + return nil, err } - defer func(Body io.ReadCloser) { err := Body.Close() if err != nil { @@ -340,26 +189,5 @@ func (idp *DingTalkIdProvider) GetUrlResp(url string) (string, error) { } }(resp.Body) - buf := new(bytes.Buffer) - _, err = buf.ReadFrom(resp.Body) - if err != nil { - return "", err - } - - return buf.String(), nil -} - -// EncodeSHA256 Use the HmacSHA256 algorithm to sign, the signature data is the current timestamp, -// and the key is the appSecret corresponding to the appId. Use this key to calculate the timestamp signature value. -// get more detail via: https://developers.dingtalk.com/document/app/signature-calculation-for-logon-free-scenarios-1?spm=ding_open_doc.document.0.0.63262ea7l6iEm1#topic-2021698 -func EncodeSHA256(message, secret string) string { - h := hmac.New(sha256.New, []byte(secret)) - h.Write([]byte(message)) - sum := h.Sum(nil) - msg1 := base64.StdEncoding.EncodeToString(sum) - - uv := url.Values{} - uv.Add("0", msg1) - msg2 := uv.Encode()[2:] - return msg2 + return data, nil } diff --git a/web/src/auth/AuthCallback.js b/web/src/auth/AuthCallback.js index d738f16c..5ef4b90c 100644 --- a/web/src/auth/AuthCallback.js +++ b/web/src/auth/AuthCallback.js @@ -74,6 +74,10 @@ class AuthCallback extends React.Component { if (code === null) { code = params.get("auth_code"); } + // Dingtalk now returns "authCode=xxx" instead of "code=xxx" + if (code === null) { + code = params.get("authCode") + } const innerParams = this.getInnerParams(); const applicationName = innerParams.get("application"); diff --git a/web/src/auth/Provider.js b/web/src/auth/Provider.js index ccd91a65..762e9e3c 100644 --- a/web/src/auth/Provider.js +++ b/web/src/auth/Provider.js @@ -41,8 +41,8 @@ const authInfo = { endpoint: "https://www.facebook.com/dialog/oauth", }, DingTalk: { - scope: "snsapi_login", - endpoint: "https://oapi.dingtalk.com/connect/oauth2/sns_authorize", + scope: "openid", + endpoint: "https://login.dingtalk.com/oauth2/auth", }, Weibo: { scope: "email", @@ -230,7 +230,7 @@ export function getAuthUrl(application, provider, method) { } else if (provider.type === "Facebook") { return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=code&state=${state}`; } else if (provider.type === "DingTalk") { - return `${endpoint}?appid=${provider.clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=code&state=${state}`; + return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=code&state=${state}&prompt=consent`; } else if (provider.type === "Weibo") { return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=code&state=${state}`; } else if (provider.type === "Gitee") {