WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit beea6fe

Browse files
committed
retrieve user email address via the GitHub user email API when logging in with GitHub
1 parent 85b05f9 commit beea6fe

File tree

7 files changed

+182
-71
lines changed

7 files changed

+182
-71
lines changed

pkg/auth/oauth2/provider/common/common_oauth2_provider.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ type CommonOAuth2DataSource interface {
3636
GetScopes() []string
3737

3838
// ParseUserInfo returns the user info by parsing the response body
39-
ParseUserInfo(c core.Context, body []byte) (*data.OAuth2UserInfo, error)
39+
ParseUserInfo(c core.Context, body []byte, oauth2Client *http.Client) (*data.OAuth2UserInfo, error)
4040
}
4141

4242
// GetOAuth2AuthUrl returns the authentication url of the common OAuth 2.0 provider
@@ -76,7 +76,7 @@ func (p *CommonOAuth2Provider) GetUserInfo(c core.Context, oauth2Token *oauth2.T
7676
return nil, errs.ErrFailedToRequestRemoteApi
7777
}
7878

79-
return p.dataSource.ParseUserInfo(c, body)
79+
return p.dataSource.ParseUserInfo(c, body, oauth2Client)
8080
}
8181

8282
// GetDataSource returns the data source of the common OAuth 2.0 provider

pkg/auth/oauth2/provider/gitea/gitea_oauth2_datasource.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func (s *GiteaOAuth2DataSource) GetScopes() []string {
5656
}
5757

5858
// ParseUserInfo returns the user info by parsing the response body
59-
func (s *GiteaOAuth2DataSource) ParseUserInfo(c core.Context, body []byte) (*data.OAuth2UserInfo, error) {
59+
func (s *GiteaOAuth2DataSource) ParseUserInfo(c core.Context, body []byte, oauth2Client *http.Client) (*data.OAuth2UserInfo, error) {
6060
userInfoResp := &giteaUserInfoResponse{}
6161
err := json.Unmarshal(body, &userInfoResp)
6262

pkg/auth/oauth2/provider/gitea/gitea_oauth2_datasource_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package gitea
22

33
import (
4+
"net/http"
45
"testing"
56

67
"github.com/stretchr/testify/assert"
@@ -47,7 +48,7 @@ func TestGiteaOAuth2Datasource_ParseUserInfo_Success(t *testing.T) {
4748
"full_name": "User",
4849
"email": "[email protected]"
4950
}`
50-
info, err := datasource.ParseUserInfo(core.NewNullContext(), []byte(responseContent))
51+
info, err := datasource.ParseUserInfo(core.NewNullContext(), []byte(responseContent), &http.Client{})
5152

5253
assert.Nil(t, err)
5354
assert.Equal(t, "user1", info.UserName)
@@ -57,15 +58,15 @@ func TestGiteaOAuth2Datasource_ParseUserInfo_Success(t *testing.T) {
5758

5859
func TestGiteaOAuth2Datasource_ParseUserInfo_InvalidJson(t *testing.T) {
5960
datasource := &GiteaOAuth2DataSource{}
60-
_, err := datasource.ParseUserInfo(core.NewNullContext(), []byte("invalid"))
61+
_, err := datasource.ParseUserInfo(core.NewNullContext(), []byte("invalid"), &http.Client{})
6162

6263
assert.Equal(t, errs.ErrCannotRetrieveUserInfo, err)
6364
}
6465

6566
func TestGiteaOAuth2Datasource_ParseUserInfo_EmptyLogin(t *testing.T) {
6667
datasource := &GiteaOAuth2DataSource{}
6768
responseContent := `{"login": ""}`
68-
_, err := datasource.ParseUserInfo(core.NewNullContext(), []byte(responseContent))
69+
_, err := datasource.ParseUserInfo(core.NewNullContext(), []byte(responseContent), &http.Client{})
6970

7071
assert.Equal(t, errs.ErrCannotRetrieveUserInfo, err)
7172
}

pkg/auth/oauth2/provider/github/github_oauth2_datasource.go

Lines changed: 140 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,81 +2,186 @@ package github
22

33
import (
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+
1625
type 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
80172
func 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
}

pkg/auth/oauth2/provider/github/github_oauth2_datasource_test.go

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,8 @@ import (
99
"github.com/mayswind/ezbookkeeping/pkg/errs"
1010
)
1111

12-
func TestGithubOAuth2Datasource_GetUserInfoRequest(t *testing.T) {
13-
datasource := &GithubOAuth2DataSource{}
14-
req, err := datasource.GetUserInfoRequest()
15-
16-
assert.Nil(t, err)
17-
assert.Equal(t, "GET", req.Method)
18-
assert.Equal(t, "https://api.github.com/user", req.URL.String())
19-
assert.Equal(t, "application/vnd.github+json", req.Header.Get("Accept"))
20-
}
21-
22-
func TestGithubOAuth2Datasource_ParseUserInfo_Success(t *testing.T) {
23-
datasource := &GithubOAuth2DataSource{}
12+
func TestGithubOAuth2Datasource_ParseUserProfile_Success(t *testing.T) {
13+
datasource := &GithubOAuth2Provider{}
2414
responseContent := `{
2515
"login": "octocat",
2616
"id": 1,
@@ -67,25 +57,39 @@ func TestGithubOAuth2Datasource_ParseUserInfo_Success(t *testing.T) {
6757
"collaborators": 0
6858
}
6959
}`
70-
info, err := datasource.ParseUserInfo(core.NewNullContext(), []byte(responseContent))
60+
info, err := datasource.parseUserProfile(core.NewNullContext(), []byte(responseContent))
7161

7262
assert.Nil(t, err)
73-
assert.Equal(t, "octocat", info.UserName)
74-
assert.Equal(t, "[email protected]", info.Email)
75-
assert.Equal(t, "monalisa octocat", info.NickName)
63+
assert.Equal(t, "octocat", info.Login)
64+
assert.Equal(t, "monalisa octocat", info.Name)
7665
}
7766

78-
func TestGithubOAuth2Datasource_ParseUserInfo_InvalidJson(t *testing.T) {
79-
datasource := &GithubOAuth2DataSource{}
80-
_, err := datasource.ParseUserInfo(core.NewNullContext(), []byte("invalid"))
67+
func TestGithubOAuth2Datasource_ParseUserProfile_EmptyLogin(t *testing.T) {
68+
datasource := &GithubOAuth2Provider{}
69+
responseContent := `{"login": ""}`
70+
_, err := datasource.parseUserProfile(core.NewNullContext(), []byte(responseContent))
8171

8272
assert.Equal(t, errs.ErrCannotRetrieveUserInfo, err)
8373
}
8474

85-
func TestGithubOAuth2Datasource_ParseUserInfo_EmptyLogin(t *testing.T) {
86-
datasource := &GithubOAuth2DataSource{}
87-
responseContent := `{"login": ""}`
88-
_, err := datasource.ParseUserInfo(core.NewNullContext(), []byte(responseContent))
75+
func TestGithubOAuth2Datasource_ParsePrimaryEmail(t *testing.T) {
76+
datasource := &GithubOAuth2Provider{}
77+
responseContent := `[
78+
{
79+
"email": "[email protected]",
80+
"primary": false,
81+
"verified": true,
82+
"visibility": null
83+
},
84+
{
85+
"email": "[email protected]",
86+
"primary": true,
87+
"verified": true,
88+
"visibility": "public"
89+
}
90+
]`
91+
email, err := datasource.parsePrimaryEmail(core.NewNullContext(), []byte(responseContent))
8992

90-
assert.Equal(t, errs.ErrCannotRetrieveUserInfo, err)
93+
assert.Nil(t, err)
94+
assert.Equal(t, "[email protected]", email)
9195
}

pkg/auth/oauth2/provider/nextcloud/nextcloud_oauth2_datasource.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func (s *NextcloudOAuth2DataSource) GetScopes() []string {
6565
}
6666

6767
// ParseUserInfo returns the user info by parsing the response body
68-
func (s *NextcloudOAuth2DataSource) ParseUserInfo(c core.Context, body []byte) (*data.OAuth2UserInfo, error) {
68+
func (s *NextcloudOAuth2DataSource) ParseUserInfo(c core.Context, body []byte, oauth2Client *http.Client) (*data.OAuth2UserInfo, error) {
6969
userInfoResp := &nextcloudUserInfoResponse{}
7070
err := json.Unmarshal(body, &userInfoResp)
7171

0 commit comments

Comments
 (0)