Skip to content

Commit 3022385

Browse files
committed
feat: update Feishu to current spec and adapt to DingTalk style
1 parent 8490134 commit 3022385

4 files changed

Lines changed: 160 additions & 468 deletions

File tree

providers/feishu/feishu.go

Lines changed: 99 additions & 202 deletions
Original file line numberDiff line numberDiff line change
@@ -6,69 +6,61 @@ import (
66
"fmt"
77
"io"
88
"net/http"
9-
"net/url"
10-
"strings"
11-
"sync"
12-
"time"
139

1410
"github.com/markbates/goth"
1511
"golang.org/x/oauth2"
1612
)
1713

18-
const (
19-
appAccessTokenURL string = "https://open.feishu.cn/open-apis/auth/v3/app_access_token/internal/" // get app_access_token
20-
21-
authURL string = "https://open.feishu.cn/open-apis/authen/v1/authorize" // obtain authorization code
22-
tokenURL string = "https://open.feishu.cn/open-apis/authen/v1/oidc/access_token" // get user_access_token
23-
refreshTokenURL string = "https://open.feishu.cn/open-apis/authen/v1/oidc/refresh_access_token" // refresh user_access_token
24-
endpointProfile string = "https://open.feishu.cn/open-apis/authen/v1/user_info" // get user info
14+
// See: https://open.feishu.cn/document/sso/web-application-sso/login-overview
15+
var (
16+
AuthURL = "https://accounts.feishu.cn/open-apis/authen/v1/authorize"
17+
TokenURL = "https://open.feishu.cn/open-apis/authen/v2/oauth/token"
18+
ProfileURL = "https://open.feishu.cn/open-apis/authen/v1/user_info"
2519
)
2620

27-
// Feishu is the implementation of `goth.Provider` for accessing Feishu
28-
type Feishu interface {
29-
GetAppAccessToken() error // get app access token
30-
}
31-
32-
// Provider is the implementation of `goth.Provider` for accessing Feishu
21+
// Provider is the implementation of `goth.Provider` for accessing Feishu.
3322
type Provider struct {
3423
ClientKey string
3524
Secret string
3625
CallbackURL string
3726
HTTPClient *http.Client
3827
config *oauth2.Config
3928
providerName string
40-
41-
appAccessToken *appAccessToken
4229
}
4330

44-
// New creates a new Feishu provider and sets up important connection details.
31+
// New creates a new Feishu provider, and sets up important connection details.
32+
// You should always call `feishu.New` to get a new Provider. Never try to create
33+
// one manually.
4534
func New(clientKey, secret, callbackURL string, scopes ...string) *Provider {
4635
p := &Provider{
47-
ClientKey: clientKey,
48-
Secret: secret,
49-
CallbackURL: callbackURL,
50-
providerName: "feishu",
51-
appAccessToken: &appAccessToken{},
36+
ClientKey: clientKey,
37+
Secret: secret,
38+
CallbackURL: callbackURL,
39+
providerName: "feishu",
5240
}
53-
p.config = newConfig(p, authURL, tokenURL, scopes)
41+
p.config = newConfig(p, scopes)
5442
return p
5543
}
5644

57-
func newConfig(provider *Provider, authURL, tokenURL string, scopes []string) *oauth2.Config {
45+
func newConfig(provider *Provider, scopes []string) *oauth2.Config {
5846
c := &oauth2.Config{
5947
ClientID: provider.ClientKey,
6048
ClientSecret: provider.Secret,
6149
RedirectURL: provider.CallbackURL,
6250
Endpoint: oauth2.Endpoint{
63-
AuthURL: authURL,
64-
TokenURL: tokenURL,
51+
AuthURL: AuthURL,
52+
TokenURL: TokenURL,
6553
},
6654
Scopes: []string{},
6755
}
6856

6957
if len(scopes) > 0 {
7058
c.Scopes = append(c.Scopes, scopes...)
59+
} else {
60+
// If no scope is provided, add the default "auth:user.id:read"
61+
c.Scopes = []string{"auth:user.id:read"}
7162
}
63+
7264
return c
7365
}
7466

@@ -80,175 +72,54 @@ func (p *Provider) Name() string {
8072
return p.providerName
8173
}
8274

75+
// SetName is to update the name of the provider (needed in case of multiple providers of 1 type)
8376
func (p *Provider) SetName(name string) {
8477
p.providerName = name
8578
}
8679

87-
type appAccessToken struct {
88-
Token string
89-
ExpiresAt time.Time
90-
rMutex sync.RWMutex
91-
}
92-
93-
type appAccessTokenReq struct {
94-
AppID string `json:"app_id"` // 自建应用的 app_id
95-
AppSecret string `json:"app_secret"` // 自建应用的 app_secret
96-
}
97-
98-
type appAccessTokenResp struct {
99-
Code int `json:"code"` // 错误码
100-
Msg string `json:"msg"` // 错误信息
101-
AppAccessToken string `json:"app_access_token"` // 用于调用应用级接口的 app_access_token
102-
Expire int64 `json:"expire"` // app_access_token 的过期时间
103-
}
104-
105-
// GetAppAccessToken get feishu app access token
106-
func (p *Provider) GetAppAccessToken() error {
107-
// get from cache app access token
108-
p.appAccessToken.rMutex.RLock()
109-
if time.Now().Before(p.appAccessToken.ExpiresAt) {
110-
p.appAccessToken.rMutex.RUnlock()
111-
return nil
112-
}
113-
p.appAccessToken.rMutex.RUnlock()
114-
115-
reqBody, err := json.Marshal(&appAccessTokenReq{
116-
AppID: p.ClientKey,
117-
AppSecret: p.Secret,
118-
})
119-
if err != nil {
120-
return fmt.Errorf("failed to marshal request body: %w", err)
121-
}
122-
123-
req, err := http.NewRequest(http.MethodPost, appAccessTokenURL, bytes.NewBuffer(reqBody))
124-
if err != nil {
125-
return fmt.Errorf("failed to create app access token request: %w", err)
126-
}
127-
req.Header.Set("Content-Type", "application/json")
128-
129-
resp, err := p.Client().Do(req)
130-
if err != nil {
131-
return fmt.Errorf("failed to send app access token request: %w", err)
132-
}
133-
defer resp.Body.Close()
134-
135-
if resp.StatusCode != http.StatusOK {
136-
return fmt.Errorf("unexpected status code while fetching app access token: %d", resp.StatusCode)
137-
}
138-
139-
tokenResp := new(appAccessTokenResp)
140-
if err = json.NewDecoder(resp.Body).Decode(tokenResp); err != nil {
141-
return fmt.Errorf("failed to decode app access token response: %w", err)
142-
}
143-
144-
if tokenResp.Code != 0 {
145-
return fmt.Errorf("failed to get app access token: code:%v msg: %s", tokenResp.Code, tokenResp.Msg)
146-
}
147-
148-
// update local cache
149-
expirationDuration := time.Duration(tokenResp.Expire) * time.Second
150-
p.appAccessToken.rMutex.Lock()
151-
p.appAccessToken.Token = tokenResp.AppAccessToken
152-
p.appAccessToken.ExpiresAt = time.Now().Add(expirationDuration)
153-
p.appAccessToken.rMutex.Unlock()
154-
155-
return nil
156-
}
157-
80+
// BeginAuth asks Feishu for an authentication end-point.
15881
func (p *Provider) BeginAuth(state string) (goth.Session, error) {
159-
// build feishu auth url
160-
u, err := url.Parse(p.config.AuthCodeURL(state))
161-
if err != nil {
162-
panic(err)
82+
url := p.config.AuthCodeURL(state)
83+
session := &Session{
84+
AuthURL: url,
16385
}
164-
query := u.Query()
165-
query.Del("response_type")
166-
query.Del("client_id")
167-
query.Add("app_id", p.ClientKey)
168-
u.RawQuery = query.Encode()
169-
170-
return &Session{
171-
AuthURL: u.String(),
172-
}, nil
173-
}
174-
175-
func (p *Provider) UnmarshalSession(data string) (goth.Session, error) {
176-
s := &Session{}
177-
err := json.NewDecoder(strings.NewReader(data)).Decode(s)
178-
return s, err
179-
}
180-
181-
func (p *Provider) Debug(b bool) {
86+
return session, nil
18287
}
18388

184-
type getUserAccessTokenResp struct {
185-
AccessToken string `json:"access_token"`
186-
RefreshToken string `json:"refresh_token"`
187-
TokenType string `json:"token_type"`
188-
ExpiresIn int `json:"expires_in"`
189-
RefreshExpiresIn int `json:"refresh_expires_in"`
190-
Scope string `json:"scope"`
191-
}
89+
// Debug is a no-op for the amazon package.
90+
func (p *Provider) Debug(debug bool) {}
19291

92+
// RefreshToken get new access token based on the refresh token
19393
func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) {
194-
if err := p.GetAppAccessToken(); err != nil {
195-
return nil, fmt.Errorf("failed to get app access token: %w", err)
196-
}
197-
reqBody := strings.NewReader(`{"grant_type":"refresh_token","refresh_token":"` + refreshToken + `"}`)
198-
199-
req, err := http.NewRequest(http.MethodPost, refreshTokenURL, reqBody)
200-
if err != nil {
201-
return nil, fmt.Errorf("failed to create refresh token request: %w", err)
202-
}
203-
req.Header.Set("Content-Type", "application/json")
204-
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", p.appAccessToken.Token))
205-
206-
resp, err := p.Client().Do(req)
94+
token := &oauth2.Token{RefreshToken: refreshToken}
95+
ts := p.config.TokenSource(goth.ContextForClient(p.Client()), token)
96+
newToken, err := ts.Token()
20797
if err != nil {
208-
return nil, fmt.Errorf("failed to send refresh token request: %w", err)
209-
}
210-
defer resp.Body.Close()
211-
212-
if resp.StatusCode != http.StatusOK {
213-
return nil, fmt.Errorf("unexpected status code while refreshing token: %d", resp.StatusCode)
98+
return nil, err
21499
}
215-
216-
var oauthResp commResponse[getUserAccessTokenResp]
217-
err = json.NewDecoder(resp.Body).Decode(&oauthResp)
218-
if err != nil {
219-
return nil, fmt.Errorf("failed to decode refreshed token: %w", err)
220-
}
221-
if oauthResp.Code != 0 {
222-
return nil, fmt.Errorf("failed to refresh token: code:%v msg: %s", oauthResp.Code, oauthResp.Msg)
223-
}
224-
225-
token := oauth2.Token{
226-
AccessToken: oauthResp.Data.AccessToken,
227-
RefreshToken: oauthResp.Data.RefreshToken,
228-
Expiry: time.Now().Add(time.Duration(oauthResp.Data.ExpiresIn) * time.Second),
229-
}
230-
231-
return &token, nil
100+
return newToken, err
232101
}
233102

103+
// RefreshTokenAvailable refresh token is provided by Feishu
234104
func (p *Provider) RefreshTokenAvailable() bool {
235105
return true
236106
}
237107

238-
type commResponse[T any] struct {
239-
Code int `json:"code"`
240-
Msg string `json:"msg"`
241-
Data T `json:"data"`
242-
}
243-
244108
type feishuUser struct {
245-
OpenID string `json:"open_id"`
246-
UnionID string `json:"union_id"`
247-
UserID string `json:"user_id"`
248-
Name string `json:"name"`
249-
Email string `json:"enterprise_email"`
250-
AvatarURL string `json:"avatar_url"`
251-
Mobile string `json:"mobile,omitempty"`
109+
Name string `json:"name"`
110+
EnName string `json:"en_name"`
111+
AvatarURL string `json:"avatar_url"`
112+
AvatarThumb string `json:"avatar_thumb"`
113+
AvatarMiddle string `json:"avatar_middle"`
114+
AvatarBig string `json:"avatar_big"`
115+
OpenID string `json:"open_id"`
116+
UnionID string `json:"union_id"`
117+
Email string `json:"email,omitempty"`
118+
EnterpriseEmail string `json:"enterprise_email,omitempty"`
119+
UserID string `json:"user_id,omitempty"`
120+
Mobile string `json:"mobile,omitempty"`
121+
TenantKey string `json:"tenant_key"`
122+
EmployeeNo string `json:"employee_no,omitempty"`
252123
}
253124

254125
// FetchUser will go to Feishu and access basic information about the user.
@@ -260,48 +131,74 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
260131
RefreshToken: sess.RefreshToken,
261132
ExpiresAt: sess.ExpiresAt,
262133
}
134+
263135
if user.AccessToken == "" {
136+
// data is not yet retrieved since accessToken is still empty
264137
return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName)
265138
}
266139

267-
req, err := http.NewRequest("GET", endpointProfile, nil)
140+
// Get user information
141+
reqProfile, err := http.NewRequest("GET", ProfileURL, nil)
268142
if err != nil {
269-
return user, fmt.Errorf("%s failed to create request: %w", p.providerName, err)
143+
return user, err
270144
}
271-
req.Header.Set("Authorization", "Bearer "+user.AccessToken)
272145

273-
resp, err := p.Client().Do(req)
146+
reqProfile.Header.Add("Authorization", fmt.Sprintf("Bearer %s", user.AccessToken))
147+
reqProfile.Header.Add("Content-Type", "application/json")
148+
149+
response, err := p.Client().Do(reqProfile)
274150
if err != nil {
275-
return user, fmt.Errorf("%s failed to get user information: %w", p.providerName, err)
151+
return user, err
276152
}
277-
defer resp.Body.Close()
153+
defer response.Body.Close()
278154

279-
if resp.StatusCode != http.StatusOK {
280-
return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, resp.StatusCode)
155+
if response.StatusCode != http.StatusOK {
156+
return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
281157
}
282158

283-
responseBytes, err := io.ReadAll(resp.Body)
159+
bits, err := io.ReadAll(response.Body)
284160
if err != nil {
285-
return user, fmt.Errorf("failed to read response body: %w", err)
161+
return user, err
286162
}
287163

288-
var oauthResp commResponse[feishuUser]
289-
if err = json.Unmarshal(responseBytes, &oauthResp); err != nil {
290-
return user, fmt.Errorf("failed to decode user info: %w", err)
164+
resBody := struct {
165+
Code int `json:"code"`
166+
Msg string `json:"msg"`
167+
Data map[string]interface{} `json:"data"`
168+
}{}
169+
err = json.Unmarshal(bits, &resBody)
170+
if err != nil {
171+
return user, err
291172
}
292-
if oauthResp.Code != 0 {
293-
return user, fmt.Errorf("failed to get user info: code:%v msg: %s", oauthResp.Code, oauthResp.Msg)
173+
if resBody.Code != 0 {
174+
return user, fmt.Errorf(resBody.Msg)
294175
}
295176

296-
u := oauthResp.Data
297-
user.UserID = u.UserID
177+
dataBits, err := json.Marshal(resBody.Data)
178+
if err != nil {
179+
return user, err
180+
}
181+
182+
err = userFromReader(bytes.NewReader(dataBits), &user)
183+
return user, err
184+
}
185+
186+
func userFromReader(r io.Reader, user *goth.User) error {
187+
// Extract user fields directly
188+
u := feishuUser{}
189+
err := json.NewDecoder(r).Decode(&u)
190+
if err != nil {
191+
return err
192+
}
193+
bits, _ := json.Marshal(u)
194+
json.NewDecoder(bytes.NewReader(bits)).Decode(&user.RawData)
195+
196+
// Populate user struct
197+
user.Email = u.EnterpriseEmail
298198
user.Name = u.Name
299-
user.Email = u.Email
300-
user.AvatarURL = u.AvatarURL
301199
user.NickName = u.Name
200+
user.UserID = u.OpenID
201+
user.AvatarURL = u.AvatarURL
302202

303-
if err = json.Unmarshal(responseBytes, &user.RawData); err != nil {
304-
return user, err
305-
}
306-
return user, nil
203+
return nil
307204
}

0 commit comments

Comments
 (0)