qywechat.go 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. // Copyright 2019 Yunion
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package qywechat
  15. import (
  16. "context"
  17. "fmt"
  18. "strings"
  19. "yunion.io/x/jsonutils"
  20. "yunion.io/x/pkg/errors"
  21. "yunion.io/x/pkg/util/httputils"
  22. "yunion.io/x/onecloud/pkg/httperrors"
  23. "yunion.io/x/onecloud/pkg/keystone/driver/oauth2"
  24. )
  25. type SQywxOAuth2Driver struct {
  26. oauth2.SOAuth2BaseDriver
  27. }
  28. func NewQywxOAuth2Driver(appId string, secret string) oauth2.IOAuth2Driver {
  29. drv := &SQywxOAuth2Driver{
  30. SOAuth2BaseDriver: oauth2.SOAuth2BaseDriver{
  31. AppId: appId,
  32. Secret: secret,
  33. },
  34. }
  35. return drv
  36. }
  37. const (
  38. AuthUrl = "https://open.work.weixin.qq.com/wwopen/sso/qrConnect"
  39. )
  40. func splitAppId(appId string) (corpId, agentId string, err error) {
  41. slash := strings.LastIndexByte(appId, '/')
  42. if slash < 0 {
  43. err = errors.Wrap(httperrors.ErrInputParameter, "invalid qywx appid, in the format of corp_id/agent_id")
  44. return
  45. } else {
  46. corpId = appId[:slash]
  47. agentId = appId[slash+1:]
  48. return
  49. }
  50. }
  51. func (drv *SQywxOAuth2Driver) GetSsoRedirectUri(ctx context.Context, callbackUrl, state string) (string, error) {
  52. corpId, agentId, err := splitAppId(drv.AppId)
  53. if err != nil {
  54. return "", err
  55. }
  56. req := map[string]string{
  57. "appid": corpId,
  58. "agentid": agentId,
  59. "redirect_uri": callbackUrl,
  60. "state": state,
  61. }
  62. urlStr := fmt.Sprintf("%s?%s", AuthUrl, jsonutils.Marshal(req).QueryString())
  63. return urlStr, nil
  64. }
  65. const (
  66. AccessTokenUrl = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"
  67. UserIdUrl = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo"
  68. UserInfoUrl = "https://qyapi.weixin.qq.com/cgi-bin/user/get"
  69. )
  70. type sAccessTokenInput struct {
  71. Corpid string `json:"corpid"`
  72. Corpsecret string `json:"corpsecret"`
  73. }
  74. type SBaseData struct {
  75. Errcode int `json:"errcode"`
  76. Errmsg string `json:"errmsg"`
  77. }
  78. type sAccessTokenData struct {
  79. SBaseData
  80. AccessToken string `json:"access_token"`
  81. ExpiresIn int64 `json:"expires_in"`
  82. }
  83. func (drv *SQywxOAuth2Driver) fetchAccessToken(ctx context.Context) (*sAccessTokenData, error) {
  84. corpId, _, err := splitAppId(drv.AppId)
  85. if err != nil {
  86. return nil, err
  87. }
  88. // ?corpid=ID&corpsecret=SECRET
  89. httpclient := httputils.GetDefaultClient()
  90. qs := sAccessTokenInput{
  91. Corpid: corpId,
  92. Corpsecret: drv.Secret,
  93. }
  94. urlstr := fmt.Sprintf("%s?%s", AccessTokenUrl, jsonutils.Marshal(qs).QueryString())
  95. _, resp, err := httputils.JSONRequest(httpclient, ctx, httputils.GET, urlstr, nil, nil, true)
  96. if err != nil {
  97. return nil, errors.Wrap(err, "request access token")
  98. }
  99. data := sAccessTokenData{}
  100. err = resp.Unmarshal(&data)
  101. if err != nil {
  102. return nil, errors.Wrap(err, "unmarshal")
  103. }
  104. return &data, nil
  105. }
  106. type sUserIdInput struct {
  107. AccessToken string `json:"access_token"`
  108. Code string `json:"code"`
  109. }
  110. type sUserIdData struct {
  111. SBaseData
  112. UserId string `json:"UserId"`
  113. }
  114. func fetchUserId(ctx context.Context, accessToken, code string) (*sUserIdData, error) {
  115. httpclient := httputils.GetDefaultClient()
  116. qs := sUserIdInput{
  117. AccessToken: accessToken,
  118. Code: code,
  119. }
  120. urlStr := fmt.Sprintf("%s?%s", UserIdUrl, jsonutils.Marshal(qs).QueryString())
  121. _, resp, err := httputils.JSONRequest(httpclient, ctx, httputils.GET, urlStr, nil, nil, true)
  122. if err != nil {
  123. return nil, errors.Wrap(err, "request access token")
  124. }
  125. data := sUserIdData{}
  126. err = resp.Unmarshal(&data)
  127. if err != nil {
  128. return nil, errors.Wrap(err, "Unmarshal")
  129. }
  130. return &data, nil
  131. }
  132. type sUserInfoInput struct {
  133. AccessToken string `json:"access_token"`
  134. Userid string `json:"userid"`
  135. }
  136. type sUserInfoData struct {
  137. SBaseData
  138. Userid string `json:"userid"`
  139. Name string `json:"name"`
  140. Department []int64 `json:"department"`
  141. Order []int64 `json:"order"`
  142. Position string `json:"position"`
  143. Mobile string `json:"mobile"`
  144. Gender string `json:"gender"`
  145. Email string `json:"email"`
  146. IsLeaderInDept []int64 `json:"is_leader_in_dept"`
  147. Avatar string `json:"avatar"`
  148. ThumbAvatar string `json:"thumb_avatar"`
  149. Telephone string `json:"telephone"`
  150. Alias string `json:"alias"`
  151. Address string `json:"address"`
  152. OpenUserid string `json:"open_userid"`
  153. MainDepartment int64 `json:"main_department"`
  154. Extattr Extattr `json:"extattr"`
  155. Status int64 `json:"status"`
  156. QrCode string `json:"qr_code"`
  157. ExternalPosition string `json:"external_position"`
  158. ExternalProfile ExternalProfile `json:"external_profile"`
  159. }
  160. type Extattr struct {
  161. Attrs []Attr `json:"attrs"`
  162. }
  163. type Attr struct {
  164. Type int64 `json:"type"`
  165. Name string `json:"name"`
  166. Text *Text `json:"text,omitempty"`
  167. Web *Web `json:"web,omitempty"`
  168. Miniprogram *Miniprogram `json:"miniprogram,omitempty"`
  169. }
  170. type Miniprogram struct {
  171. Appid string `json:"appid"`
  172. Pagepath string `json:"pagepath"`
  173. Title string `json:"title"`
  174. }
  175. type Text struct {
  176. Value string `json:"value"`
  177. }
  178. type Web struct {
  179. URL string `json:"url"`
  180. Title string `json:"title"`
  181. }
  182. type ExternalProfile struct {
  183. ExternalCorpName string `json:"external_corp_name"`
  184. ExternalAttr []Attr `json:"external_attr"`
  185. }
  186. func fetchUserInfo(ctx context.Context, accessToken, userId string) (*sUserInfoData, error) {
  187. // https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=ACCESS_TOKEN&userid=USERID
  188. httpclient := httputils.GetDefaultClient()
  189. qs := sUserInfoInput{
  190. AccessToken: accessToken,
  191. Userid: userId,
  192. }
  193. urlStr := fmt.Sprintf("%s?%s", UserInfoUrl, jsonutils.Marshal(qs).QueryString())
  194. _, resp, err := httputils.JSONRequest(httpclient, ctx, httputils.GET, urlStr, nil, nil, true)
  195. if err != nil {
  196. return nil, errors.Wrap(err, "request user info")
  197. }
  198. data := sUserInfoData{}
  199. err = resp.Unmarshal(&data)
  200. if err != nil {
  201. return nil, errors.Wrap(err, "Unmarshal")
  202. }
  203. return &data, nil
  204. }
  205. func (drv *SQywxOAuth2Driver) Authenticate(ctx context.Context, code string) (map[string][]string, error) {
  206. accessData, err := drv.fetchAccessToken(ctx)
  207. if err != nil {
  208. return nil, errors.Wrap(err, "fetchAccessToken")
  209. }
  210. userId, err := fetchUserId(ctx, accessData.AccessToken, code)
  211. if err != nil {
  212. return nil, errors.Wrap(err, "fetchUserId")
  213. }
  214. userInfo, err := fetchUserInfo(ctx, accessData.AccessToken, userId.UserId)
  215. if err != nil {
  216. return nil, errors.Wrap(err, "fetchUserInfo")
  217. }
  218. ret := make(map[string][]string)
  219. ret["name"] = []string{userId.UserId}
  220. ret["user_id"] = []string{userId.UserId}
  221. ret["displayname"] = []string{userInfo.Name}
  222. ret["email"] = []string{userInfo.Email}
  223. ret["mobile"] = []string{userInfo.Mobile}
  224. return ret, nil
  225. }