feishu.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  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 feishu
  15. import (
  16. "context"
  17. "fmt"
  18. "net/http"
  19. "yunion.io/x/jsonutils"
  20. "yunion.io/x/pkg/errors"
  21. "yunion.io/x/pkg/util/httputils"
  22. "yunion.io/x/onecloud/pkg/keystone/driver/oauth2"
  23. )
  24. type SFeishuOAuth2Driver struct {
  25. oauth2.SOAuth2BaseDriver
  26. }
  27. func NewFeishuOAuth2Driver(appId string, secret string) oauth2.IOAuth2Driver {
  28. drv := &SFeishuOAuth2Driver{
  29. SOAuth2BaseDriver: oauth2.SOAuth2BaseDriver{
  30. AppId: appId,
  31. Secret: secret,
  32. },
  33. }
  34. return drv
  35. }
  36. const (
  37. AuthUrl = "https://open.feishu.cn/open-apis/authen/v1/index"
  38. )
  39. func (drv *SFeishuOAuth2Driver) GetSsoRedirectUri(ctx context.Context, callbackUrl, state string) (string, error) {
  40. req := map[string]string{
  41. "app_id": drv.AppId,
  42. "state": state,
  43. "redirect_uri": callbackUrl,
  44. }
  45. urlStr := fmt.Sprintf("%s?%s", AuthUrl, jsonutils.Marshal(req).QueryString())
  46. return urlStr, nil
  47. }
  48. const (
  49. AppAccessTokenUrl = "https://open.feishu.cn/open-apis/auth/v3/app_access_token/internal/"
  50. AccessTokenUrl = "https://open.feishu.cn/open-apis/authen/v1/access_token"
  51. UserInfoUrl = "https://open.feishu.cn/open-apis/authen/v1/user_info"
  52. )
  53. type sAccessTokenInput struct {
  54. AppAccessToken string `json:"app_access_token"`
  55. GrantType string `json:"grant_type"`
  56. Code string `json:"code"`
  57. }
  58. type sAccessTokenData struct {
  59. AccessToken string `json:"access_token"`
  60. AvatarURL string `json:"avatar_url"`
  61. AvatarThumb string `json:"avatar_thumb"`
  62. AvatarMiddle string `json:"avatar_middle"`
  63. AvatarBig string `json:"avatar_big"`
  64. ExpiresIn int64 `json:"expires_in"`
  65. Name string `json:"name"`
  66. EnName string `json:"en_name"`
  67. OpenID string `json:"open_id"`
  68. TenantKey string `json:"tenant_key"`
  69. RefreshExpiresIn int64 `json:"refresh_expires_in"`
  70. RefreshToken string `json:"refresh_token"`
  71. TokenType string `json:"token_type"`
  72. }
  73. func fetchAccessToken(ctx context.Context, appAccessToken string, code string) (*sAccessTokenData, error) {
  74. httpclient := httputils.GetDefaultClient()
  75. body := sAccessTokenInput{
  76. AppAccessToken: appAccessToken,
  77. GrantType: "authorization_code",
  78. Code: code,
  79. }
  80. _, resp, err := httputils.JSONRequest(httpclient, ctx, httputils.POST, AccessTokenUrl, nil, jsonutils.Marshal(body), true)
  81. if err != nil {
  82. return nil, errors.Wrap(err, "request access token")
  83. }
  84. data := sAccessTokenData{}
  85. err = resp.Unmarshal(&data, "data")
  86. if err != nil {
  87. return nil, errors.Wrap(err, "unmarshal")
  88. }
  89. return &data, nil
  90. }
  91. type sUserInfoData struct {
  92. Name string `json:"name"`
  93. AvatarURL string `json:"avatar_url"`
  94. AvatarThumb string `json:"avatar_thumb"`
  95. AvatarMiddle string `json:"avatar_middle"`
  96. AvatarBig string `json:"avatar_big"`
  97. Email string `json:"email"`
  98. UserID string `json:"user_id"`
  99. Mobile string `json:"mobile"`
  100. Status int64 `json:"status"`
  101. }
  102. func fetchUserInfo(ctx context.Context, accessToken string) (*sUserInfoData, error) {
  103. httpclient := httputils.GetDefaultClient()
  104. header := http.Header{}
  105. header.Set("Authorization", "Bearer "+accessToken)
  106. _, resp, err := httputils.JSONRequest(httpclient, ctx, httputils.GET, UserInfoUrl, header, nil, true)
  107. if err != nil {
  108. return nil, errors.Wrap(err, "request access token")
  109. }
  110. data := sUserInfoData{}
  111. err = resp.Unmarshal(&data, "data")
  112. if err != nil {
  113. return nil, errors.Wrap(err, "Unmarshal")
  114. }
  115. return &data, nil
  116. }
  117. type sAppAccessTokenInput struct {
  118. AppID string `json:"app_id"`
  119. AppSecret string `json:"app_secret"`
  120. }
  121. type sAppAccessTokenData struct {
  122. Code int64 `json:"code"`
  123. Msg string `json:"msg"`
  124. AppAccessToken string `json:"app_access_token"`
  125. Expire int64 `json:"expire"`
  126. TenantAccessToken string `json:"tenant_access_token"`
  127. }
  128. // https://open.feishu.cn/document/ukTMukTMukTM/uADN14CM0UjLwQTN
  129. func fetchAppAccessToken(ctx context.Context, appId, appSecret string) (*sAppAccessTokenData, error) {
  130. httpclient := httputils.GetDefaultClient()
  131. body := sAppAccessTokenInput{
  132. AppID: appId,
  133. AppSecret: appSecret,
  134. }
  135. _, resp, err := httputils.JSONRequest(httpclient, ctx, httputils.POST, AppAccessTokenUrl, nil, jsonutils.Marshal(body), true)
  136. if err != nil {
  137. return nil, errors.Wrap(err, "request access token")
  138. }
  139. data := sAppAccessTokenData{}
  140. err = resp.Unmarshal(&data)
  141. if err != nil {
  142. return nil, errors.Wrap(err, "unmarshal")
  143. }
  144. return &data, nil
  145. }
  146. func (drv *SFeishuOAuth2Driver) Authenticate(ctx context.Context, code string) (map[string][]string, error) {
  147. appData, err := fetchAppAccessToken(ctx, drv.AppId, drv.Secret)
  148. if err != nil {
  149. return nil, errors.Wrap(err, "fetchAppAccessToken")
  150. }
  151. accessData, err := fetchAccessToken(ctx, appData.AppAccessToken, code)
  152. if err != nil {
  153. return nil, errors.Wrap(err, "fetchAccessToken")
  154. }
  155. userInfo, err := fetchUserInfo(ctx, accessData.AccessToken)
  156. if err != nil {
  157. return nil, errors.Wrap(err, "fetchUserInfo")
  158. }
  159. ret := make(map[string][]string)
  160. ret["name"] = []string{userInfo.Name}
  161. ret["user_id"] = []string{userInfo.UserID}
  162. ret["name_en"] = []string{accessData.EnName}
  163. ret["email"] = []string{userInfo.Email}
  164. ret["mobile"] = []string{userInfo.Mobile}
  165. return ret, nil
  166. }