@@ -2,81 +2,186 @@ package github
22
33import (
44 "encoding/json"
5+ "io"
56 "net/http"
67
8+ "golang.org/x/oauth2"
9+
710 "github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/data"
811 "github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider"
9- "github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider/common"
1012 "github.com/mayswind/ezbookkeeping/pkg/core"
1113 "github.com/mayswind/ezbookkeeping/pkg/errs"
1214 "github.com/mayswind/ezbookkeeping/pkg/log"
1315 "github.com/mayswind/ezbookkeeping/pkg/settings"
1416)
1517
18+ const githubOAuth2AuthUrl = "https://github.com/login/oauth/authorize" // Reference: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps
19+ const githubOAuth2TokenUrl = "https://github.com/login/oauth/access_token" // Reference: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps
20+ const githubUserProfileApiUrl = "https://api.github.com/user" // Reference: https://docs.github.com/en/rest/users/users
21+ const githubUserEmailApiUrl = "https://api.github.com/user/emails" // Reference: https://docs.github.com/en/rest/users/emails
22+
23+ var githubOAuth2Scopes = []string {"user:email" }
24+
1625type githubUserProfileResponse struct {
1726 Login string `json:"login"`
1827 Name string `json:"name"`
1928 Email string `json:"email"`
2029}
2130
22- // GithubOAuth2DataSource represents Github OAuth 2.0 data source
23- type GithubOAuth2DataSource struct {
24- common.CommonOAuth2DataSource
31+ type githubUserEmailsResponse struct {
32+ Email string `json:"email"`
33+ Primary bool `json:"primary"`
34+ Verified bool `json:"verified"`
35+ }
36+
37+ // GithubOAuth2Provider represents Github OAuth 2.0 provider
38+ type GithubOAuth2Provider struct {
39+ provider.OAuth2Provider
40+ oauth2Config * oauth2.Config
2541}
2642
27- // GetAuthUrl returns the authentication url of the Github data source
28- func (s * GithubOAuth2DataSource ) GetAuthUrl () string {
29- // Reference: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps
30- return "https://github.com/login/oauth/authorize"
43+ // GetOAuth2AuthUrl returns the authentication url of the GitHub OAuth 2.0 provider
44+ func (p * GithubOAuth2Provider ) GetOAuth2AuthUrl (c core.Context , state string , challenge string ) (string , error ) {
45+ return p .oauth2Config .AuthCodeURL (state ), nil
3146}
3247
33- // GetTokenUrl returns the token url of the Github data source
34- func (s * GithubOAuth2DataSource ) GetTokenUrl () string {
35- // Reference: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps
36- return "https://github.com/login/oauth/access_token"
48+ // GetOAuth2Token returns the OAuth 2.0 token of the GitHub OAuth 2.0 provider
49+ func (p * GithubOAuth2Provider ) GetOAuth2Token (c core.Context , code string , verifier string ) (* oauth2.Token , error ) {
50+ return p .oauth2Config .Exchange (c , code )
3751}
3852
39- // GetUserInfoRequest returns the user info request of the Github data source
40- func (s * GithubOAuth2DataSource ) GetUserInfoRequest () (* http.Request , error ) {
41- // Reference: https://docs.github.com/en/rest/users/users
42- req , err := http .NewRequest ("GET" , "https://api.github.com/user" , nil )
53+ // GetUserInfo returns the user info by the Github OAuth 2.0 provider
54+ func (p * GithubOAuth2Provider ) GetUserInfo (c core.Context , oauth2Token * oauth2.Token ) (* data.OAuth2UserInfo , error ) {
55+ // first get user name and nick name from user profile
56+ req , err := p .buildAPIRequest (githubUserProfileApiUrl )
57+
58+ if err != nil {
59+ log .Errorf (c , "[github_oauth2_datasource_test.GetUserInfo] failed to get user info request, because %s" , err .Error ())
60+ return nil , errs .ErrFailedToRequestRemoteApi
61+ }
62+
63+ oauth2Client := oauth2 .NewClient (c , oauth2 .StaticTokenSource (oauth2Token ))
64+ resp , err := oauth2Client .Do (req )
65+
66+ if err != nil {
67+ log .Errorf (c , "[github_oauth2_datasource_test.GetUserInfo] failed to get user info response, because %s" , err .Error ())
68+ return nil , errs .ErrFailedToRequestRemoteApi
69+ }
70+
71+ defer resp .Body .Close ()
72+ body , err := io .ReadAll (resp .Body )
73+
74+ log .Debugf (c , "[github_oauth2_datasource_test.GetUserInfo] user profile response is %s" , body )
75+
76+ if resp .StatusCode != 200 {
77+ log .Errorf (c , "[github_oauth2_datasource_test.GetUserInfo] failed to get user info response, because response code is %d" , resp .StatusCode )
78+ return nil , errs .ErrFailedToRequestRemoteApi
79+ }
80+
81+ userProfileResp , err := p .parseUserProfile (c , body )
4382
4483 if err != nil {
4584 return nil , err
4685 }
4786
48- req .Header .Set ("Accept" , "application/vnd.github+json" )
49- return req , nil
50- }
87+ // then get user primary email
88+ req , err = p .buildAPIRequest (githubUserEmailApiUrl )
89+
90+ if err != nil {
91+ log .Errorf (c , "[github_oauth2_datasource_test.GetUserInfo] failed to get user emails request, because %s" , err .Error ())
92+ return nil , errs .ErrFailedToRequestRemoteApi
93+ }
94+
95+ resp , err = oauth2Client .Do (req )
96+
97+ if err != nil {
98+ log .Errorf (c , "[github_oauth2_datasource_test.GetUserInfo] failed to get user emails response, because %s" , err .Error ())
99+ return nil , errs .ErrFailedToRequestRemoteApi
100+ }
101+
102+ defer resp .Body .Close ()
103+ body , err = io .ReadAll (resp .Body )
104+
105+ log .Debugf (c , "[github_oauth2_datasource_test.GetUserInfo] user emails response is %s" , body )
106+
107+ if resp .StatusCode != 200 {
108+ log .Errorf (c , "[github_oauth2_datasource_test.GetUserInfo] failed to get user emails response, because response code is %d" , resp .StatusCode )
109+ return nil , errs .ErrFailedToRequestRemoteApi
110+ }
111+
112+ email , err := p .parsePrimaryEmail (c , body )
113+
114+ if err != nil {
115+ return nil , err
116+ }
51117
52- // GetScopes returns the scopes required by the Github provider
53- func (p * GithubOAuth2DataSource ) GetScopes () []string {
54- return []string {"read:user" }
118+ return & data.OAuth2UserInfo {
119+ UserName : userProfileResp .Login ,
120+ Email : email ,
121+ NickName : userProfileResp .Name ,
122+ }, nil
55123}
56124
57- // ParseUserInfo returns the user info by parsing the response body
58- func (p * GithubOAuth2DataSource ) ParseUserInfo (c core.Context , body []byte ) (* data.OAuth2UserInfo , error ) {
59- userInfoResp := & githubUserProfileResponse {}
60- err := json .Unmarshal (body , & userInfoResp )
125+ func (p * GithubOAuth2Provider ) parseUserProfile (c core.Context , body []byte ) (* githubUserProfileResponse , error ) {
126+ userProfileResp := & githubUserProfileResponse {}
127+ err := json .Unmarshal (body , & userProfileResp )
61128
62129 if err != nil {
63- log .Warnf (c , "[github_oauth2_datasource.ParseUserInfo ] failed to parse user profile response body, because %s" , err .Error ())
130+ log .Warnf (c , "[github_oauth2_datasource.parseUserProfile ] failed to parse user profile response body, because %s" , err .Error ())
64131 return nil , errs .ErrCannotRetrieveUserInfo
65132 }
66133
67- if userInfoResp .Login == "" {
68- log .Warnf (c , "[github_oauth2_datasource.ParseUserInfo ] invalid user profile response body" )
134+ if userProfileResp .Login == "" {
135+ log .Warnf (c , "[github_oauth2_datasource.parseUserProfile ] invalid user profile response body" )
69136 return nil , errs .ErrCannotRetrieveUserInfo
70137 }
71138
72- return & data.OAuth2UserInfo {
73- UserName : userInfoResp .Login ,
74- Email : userInfoResp .Email ,
75- NickName : userInfoResp .Name ,
76- }, nil
139+ return userProfileResp , nil
140+ }
141+
142+ func (p * GithubOAuth2Provider ) parsePrimaryEmail (c core.Context , body []byte ) (string , error ) {
143+ emailsResp := make ([]githubUserEmailsResponse , 0 )
144+ err := json .Unmarshal (body , & emailsResp )
145+
146+ if err != nil {
147+ log .Warnf (c , "[github_oauth2_datasource.parsePrimaryEmail] failed to parse user emails response body, because %s" , err .Error ())
148+ return "" , errs .ErrCannotRetrieveUserInfo
149+ }
150+
151+ for _ , emailEntry := range emailsResp {
152+ if emailEntry .Primary && emailEntry .Verified {
153+ return emailEntry .Email , nil
154+ }
155+ }
156+
157+ return "" , nil
158+ }
159+
160+ func (p * GithubOAuth2Provider ) buildAPIRequest (url string ) (* http.Request , error ) {
161+ req , err := http .NewRequest ("GET" , url , nil )
162+
163+ if err != nil {
164+ return nil , err
165+ }
166+
167+ req .Header .Set ("Accept" , "application/vnd.github+json" )
168+ return req , nil
77169}
78170
79171// NewGithubOAuth2Provider creates a new Github OAuth 2.0 provider instance
80172func NewGithubOAuth2Provider (config * settings.Config , redirectUrl string ) (provider.OAuth2Provider , error ) {
81- return common .NewCommonOAuth2Provider (config , redirectUrl , & GithubOAuth2DataSource {}), nil
173+ oauth2Config := & oauth2.Config {
174+ ClientID : config .OAuth2ClientID ,
175+ ClientSecret : config .OAuth2ClientSecret ,
176+ Endpoint : oauth2.Endpoint {
177+ AuthURL : githubOAuth2AuthUrl ,
178+ TokenURL : githubOAuth2TokenUrl ,
179+ },
180+ RedirectURL : redirectUrl ,
181+ Scopes : githubOAuth2Scopes ,
182+ }
183+
184+ return & GithubOAuth2Provider {
185+ oauth2Config : oauth2Config ,
186+ }, nil
82187}
0 commit comments