Skip to content
This repository was archived by the owner on Jan 24, 2019. It is now read-only.

Commit b0c1c85

Browse files
authored
Merge pull request #466 from clobrano/github-use-login-as-user
GitHub use login as user
2 parents 6ddbb2c + 731fa9f commit b0c1c85

7 files changed

+313
-43
lines changed

oauthproxy.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,13 @@ func (p *OAuthProxy) redeemCode(host, code string) (s *providers.SessionState, e
244244
if s.Email == "" {
245245
s.Email, err = p.provider.GetEmailAddress(s)
246246
}
247+
248+
if s.User == "" {
249+
s.User, err = p.provider.GetUserName(s)
250+
if err != nil && err.Error() == "not implemented" {
251+
err = nil
252+
}
253+
}
247254
return
248255
}
249256

providers/github.go

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,10 +218,10 @@ func (p *GitHubProvider) GetEmailAddress(s *SessionState) (string, error) {
218218
if resp.StatusCode != 200 {
219219
return "", fmt.Errorf("got %d from %q %s",
220220
resp.StatusCode, endpoint.String(), body)
221-
} else {
222-
log.Printf("got %d from %q %s", resp.StatusCode, endpoint.String(), body)
223221
}
224222

223+
log.Printf("got %d from %q %s", resp.StatusCode, endpoint.String(), body)
224+
225225
if err := json.Unmarshal(body, &emails); err != nil {
226226
return "", fmt.Errorf("%s unmarshaling %s", err, body)
227227
}
@@ -234,3 +234,46 @@ func (p *GitHubProvider) GetEmailAddress(s *SessionState) (string, error) {
234234

235235
return "", nil
236236
}
237+
238+
func (p *GitHubProvider) GetUserName(s *SessionState) (string, error) {
239+
var user struct {
240+
Login string `json:"login"`
241+
Email string `json:"email"`
242+
}
243+
244+
endpoint := &url.URL{
245+
Scheme: p.ValidateURL.Scheme,
246+
Host: p.ValidateURL.Host,
247+
Path: path.Join(p.ValidateURL.Path, "/user"),
248+
}
249+
250+
req, err := http.NewRequest("GET", endpoint.String(), nil)
251+
if err != nil {
252+
return "", fmt.Errorf("could not create new GET request: %v", err)
253+
}
254+
255+
req.Header.Set("Authorization", fmt.Sprintf("token %s", s.AccessToken))
256+
resp, err := http.DefaultClient.Do(req)
257+
if err != nil {
258+
return "", err
259+
}
260+
261+
body, err := ioutil.ReadAll(resp.Body)
262+
defer resp.Body.Close()
263+
if err != nil {
264+
return "", err
265+
}
266+
267+
if resp.StatusCode != 200 {
268+
return "", fmt.Errorf("got %d from %q %s",
269+
resp.StatusCode, endpoint.String(), body)
270+
}
271+
272+
log.Printf("got %d from %q %s", resp.StatusCode, endpoint.String(), body)
273+
274+
if err := json.Unmarshal(body, &user); err != nil {
275+
return "", fmt.Errorf("%s unmarshaling %s", err, body)
276+
}
277+
278+
return user.Login, nil
279+
}

providers/github_test.go

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package providers
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"net/url"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func testGitHubProvider(hostname string) *GitHubProvider {
13+
p := NewGitHubProvider(
14+
&ProviderData{
15+
ProviderName: "",
16+
LoginURL: &url.URL{},
17+
RedeemURL: &url.URL{},
18+
ProfileURL: &url.URL{},
19+
ValidateURL: &url.URL{},
20+
Scope: ""})
21+
if hostname != "" {
22+
updateURL(p.Data().LoginURL, hostname)
23+
updateURL(p.Data().RedeemURL, hostname)
24+
updateURL(p.Data().ProfileURL, hostname)
25+
updateURL(p.Data().ValidateURL, hostname)
26+
}
27+
return p
28+
}
29+
30+
func testGitHubBackend(payload string) *httptest.Server {
31+
pathToQueryMap := map[string]string{
32+
"/user": "",
33+
"/user/emails": "",
34+
}
35+
36+
return httptest.NewServer(http.HandlerFunc(
37+
func(w http.ResponseWriter, r *http.Request) {
38+
url := r.URL
39+
query, ok := pathToQueryMap[url.Path]
40+
if !ok {
41+
w.WriteHeader(404)
42+
} else if url.RawQuery != query {
43+
w.WriteHeader(404)
44+
} else {
45+
w.WriteHeader(200)
46+
w.Write([]byte(payload))
47+
}
48+
}))
49+
}
50+
51+
func TestGitHubProviderDefaults(t *testing.T) {
52+
p := testGitHubProvider("")
53+
assert.NotEqual(t, nil, p)
54+
assert.Equal(t, "GitHub", p.Data().ProviderName)
55+
assert.Equal(t, "https://github.com/login/oauth/authorize",
56+
p.Data().LoginURL.String())
57+
assert.Equal(t, "https://github.com/login/oauth/access_token",
58+
p.Data().RedeemURL.String())
59+
assert.Equal(t, "https://api.github.com/",
60+
p.Data().ValidateURL.String())
61+
assert.Equal(t, "user:email", p.Data().Scope)
62+
}
63+
64+
func TestGitHubProviderOverrides(t *testing.T) {
65+
p := NewGitHubProvider(
66+
&ProviderData{
67+
LoginURL: &url.URL{
68+
Scheme: "https",
69+
Host: "example.com",
70+
Path: "/login/oauth/authorize"},
71+
RedeemURL: &url.URL{
72+
Scheme: "https",
73+
Host: "example.com",
74+
Path: "/login/oauth/access_token"},
75+
ValidateURL: &url.URL{
76+
Scheme: "https",
77+
Host: "api.example.com",
78+
Path: "/"},
79+
Scope: "profile"})
80+
assert.NotEqual(t, nil, p)
81+
assert.Equal(t, "GitHub", p.Data().ProviderName)
82+
assert.Equal(t, "https://example.com/login/oauth/authorize",
83+
p.Data().LoginURL.String())
84+
assert.Equal(t, "https://example.com/login/oauth/access_token",
85+
p.Data().RedeemURL.String())
86+
assert.Equal(t, "https://api.example.com/",
87+
p.Data().ValidateURL.String())
88+
assert.Equal(t, "profile", p.Data().Scope)
89+
}
90+
91+
func TestGitHubProviderGetEmailAddress(t *testing.T) {
92+
b := testGitHubBackend(`[ {"email": "[email protected]", "primary": true} ]`)
93+
defer b.Close()
94+
95+
bURL, _ := url.Parse(b.URL)
96+
p := testGitHubProvider(bURL.Host)
97+
98+
session := &SessionState{AccessToken: "imaginary_access_token"}
99+
email, err := p.GetEmailAddress(session)
100+
assert.Equal(t, nil, err)
101+
assert.Equal(t, "[email protected]", email)
102+
}
103+
104+
// Note that trying to trigger the "failed building request" case is not
105+
// practical, since the only way it can fail is if the URL fails to parse.
106+
func TestGitHubProviderGetEmailAddressFailedRequest(t *testing.T) {
107+
b := testGitHubBackend("unused payload")
108+
defer b.Close()
109+
110+
bURL, _ := url.Parse(b.URL)
111+
p := testGitHubProvider(bURL.Host)
112+
113+
// We'll trigger a request failure by using an unexpected access
114+
// token. Alternatively, we could allow the parsing of the payload as
115+
// JSON to fail.
116+
session := &SessionState{AccessToken: "unexpected_access_token"}
117+
email, err := p.GetEmailAddress(session)
118+
assert.NotEqual(t, nil, err)
119+
assert.Equal(t, "", email)
120+
}
121+
122+
func TestGitHubProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {
123+
b := testGitHubBackend("{\"foo\": \"bar\"}")
124+
defer b.Close()
125+
126+
bURL, _ := url.Parse(b.URL)
127+
p := testGitHubProvider(bURL.Host)
128+
129+
session := &SessionState{AccessToken: "imaginary_access_token"}
130+
email, err := p.GetEmailAddress(session)
131+
assert.NotEqual(t, nil, err)
132+
assert.Equal(t, "", email)
133+
}
134+
135+
func TestGitHubProviderGetUserName(t *testing.T) {
136+
b := testGitHubBackend(`{"email": "[email protected]", "login": "mbland"}`)
137+
defer b.Close()
138+
139+
bURL, _ := url.Parse(b.URL)
140+
p := testGitHubProvider(bURL.Host)
141+
142+
session := &SessionState{AccessToken: "imaginary_access_token"}
143+
email, err := p.GetUserName(session)
144+
assert.Equal(t, nil, err)
145+
assert.Equal(t, "mbland", email)
146+
}

providers/provider_default.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,11 @@ func (p *ProviderData) GetEmailAddress(s *SessionState) (string, error) {
106106
return "", errors.New("not implemented")
107107
}
108108

109+
// GetUserName returns the Account username
110+
func (p *ProviderData) GetUserName(s *SessionState) (string, error) {
111+
return "", errors.New("not implemented")
112+
}
113+
109114
// ValidateGroup validates that the provided email exists in the configured provider
110115
// email group(s).
111116
func (p *ProviderData) ValidateGroup(email string) bool {

providers/providers.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
type Provider interface {
88
Data() *ProviderData
99
GetEmailAddress(*SessionState) (string, error)
10+
GetUserName(*SessionState) (string, error)
1011
Redeem(string, string) (*SessionState, error)
1112
ValidateGroup(string) bool
1213
ValidateSessionState(*SessionState) bool

providers/session_state.go

Lines changed: 40 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func (s *SessionState) IsExpired() bool {
2525
}
2626

2727
func (s *SessionState) String() string {
28-
o := fmt.Sprintf("Session{%s", s.userOrEmail())
28+
o := fmt.Sprintf("Session{%s", s.accountInfo())
2929
if s.AccessToken != "" {
3030
o += " token:true"
3131
}
@@ -40,17 +40,13 @@ func (s *SessionState) String() string {
4040

4141
func (s *SessionState) EncodeSessionState(c *cookie.Cipher) (string, error) {
4242
if c == nil || s.AccessToken == "" {
43-
return s.userOrEmail(), nil
43+
return s.accountInfo(), nil
4444
}
4545
return s.EncryptedString(c)
4646
}
4747

48-
func (s *SessionState) userOrEmail() string {
49-
u := s.User
50-
if s.Email != "" {
51-
u = s.Email
52-
}
53-
return u
48+
func (s *SessionState) accountInfo() string {
49+
return fmt.Sprintf("email:%s user:%s", s.Email, s.User)
5450
}
5551

5652
func (s *SessionState) EncryptedString(c *cookie.Cipher) (string, error) {
@@ -60,56 +56,64 @@ func (s *SessionState) EncryptedString(c *cookie.Cipher) (string, error) {
6056
}
6157
a := s.AccessToken
6258
if a != "" {
63-
a, err = c.Encrypt(a)
64-
if err != nil {
59+
if a, err = c.Encrypt(a); err != nil {
6560
return "", err
6661
}
6762
}
6863
r := s.RefreshToken
6964
if r != "" {
70-
r, err = c.Encrypt(r)
71-
if err != nil {
65+
if r, err = c.Encrypt(r); err != nil {
7266
return "", err
7367
}
7468
}
75-
return fmt.Sprintf("%s|%s|%d|%s", s.userOrEmail(), a, s.ExpiresOn.Unix(), r), nil
69+
return fmt.Sprintf("%s|%s|%d|%s", s.accountInfo(), a, s.ExpiresOn.Unix(), r), nil
70+
}
71+
72+
func decodeSessionStatePlain(v string) (s *SessionState, err error) {
73+
chunks := strings.Split(v, " ")
74+
if len(chunks) != 2 {
75+
return nil, fmt.Errorf("could not decode session state: expected 2 chunks got %d", len(chunks))
76+
}
77+
78+
email := strings.TrimPrefix(chunks[0], "email:")
79+
user := strings.TrimPrefix(chunks[1], "user:")
80+
if user == "" {
81+
user = strings.Split(email, "@")[0]
82+
}
83+
84+
return &SessionState{User: user, Email: email}, nil
7685
}
7786

7887
func DecodeSessionState(v string, c *cookie.Cipher) (s *SessionState, err error) {
79-
chunks := strings.Split(v, "|")
80-
if len(chunks) == 1 {
81-
if strings.Contains(chunks[0], "@") {
82-
u := strings.Split(v, "@")[0]
83-
return &SessionState{Email: v, User: u}, nil
84-
}
85-
return &SessionState{User: v}, nil
88+
if c == nil {
89+
return decodeSessionStatePlain(v)
8690
}
8791

92+
chunks := strings.Split(v, "|")
8893
if len(chunks) != 4 {
8994
err = fmt.Errorf("invalid number of fields (got %d expected 4)", len(chunks))
9095
return
9196
}
9297

93-
s = &SessionState{}
94-
if c != nil && chunks[1] != "" {
95-
s.AccessToken, err = c.Decrypt(chunks[1])
96-
if err != nil {
98+
sessionState, err := decodeSessionStatePlain(chunks[0])
99+
if err != nil {
100+
return nil, err
101+
}
102+
103+
if chunks[1] != "" {
104+
if sessionState.AccessToken, err = c.Decrypt(chunks[1]); err != nil {
97105
return nil, err
98106
}
99107
}
100-
if c != nil && chunks[3] != "" {
101-
s.RefreshToken, err = c.Decrypt(chunks[3])
102-
if err != nil {
108+
109+
ts, _ := strconv.Atoi(chunks[2])
110+
sessionState.ExpiresOn = time.Unix(int64(ts), 0)
111+
112+
if chunks[3] != "" {
113+
if sessionState.RefreshToken, err = c.Decrypt(chunks[3]); err != nil {
103114
return nil, err
104115
}
105116
}
106-
if u := chunks[0]; strings.Contains(u, "@") {
107-
s.Email = u
108-
s.User = strings.Split(u, "@")[0]
109-
} else {
110-
s.User = u
111-
}
112-
ts, _ := strconv.Atoi(chunks[2])
113-
s.ExpiresOn = time.Unix(int64(ts), 0)
114-
return
117+
118+
return sessionState, nil
115119
}

0 commit comments

Comments
 (0)