@@ -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.
3322type 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.
4534func 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)
8376func (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.
15881func (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
19393func (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
234104func (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-
244108type 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