git.iko.soy/typeable/oauth2-proxy
on master help sign in
Github provider
37b38ddJehiah Czebotar
README.md+6
@@ -101,6 +101,8 @@ Usage of google_auth_proxy:
101 -version=false: print version string
101 -version=false: print version string
102```
102```
103
103
104+See below for provider specific options
105+
104### Environment variables
106### Environment variables
105
107
106The environment variables `GOOGLE_AUTH_PROXY_CLIENT_ID`, `GOOGLE_AUTH_PROXY_CLIENT_SECRET`, `GOOGLE_AUTH_PROXY_COOKIE_SECRET`, `GOOGLE_AUTH_PROXY_COOKIE_DOMAIN` and `GOOGLE_AUTH_PROXY_COOKIE_EXPIRE` can be used in place of the corresponding command-line arguments.
108The environment variables `GOOGLE_AUTH_PROXY_CLIENT_ID`, `GOOGLE_AUTH_PROXY_CLIENT_SECRET`, `GOOGLE_AUTH_PROXY_COOKIE_SECRET`, `GOOGLE_AUTH_PROXY_COOKIE_DOMAIN` and `GOOGLE_AUTH_PROXY_COOKIE_EXPIRE` can be used in place of the corresponding command-line arguments.
@@ -173,6 +175,10 @@ directive. Right now this includes:
173* `myusa` - The [MyUSA](https://alpha.my.usa.gov) authentication service
175* `myusa` - The [MyUSA](https://alpha.my.usa.gov) authentication service
174 ([GitHub](https://github.com/18F/myusa))
176 ([GitHub](https://github.com/18F/myusa))
175* `linkedin` - The [LinkedIn](https://developer.linkedin.com/docs/signin-with-linkedin) Sign In service.
177* `linkedin` - The [LinkedIn](https://developer.linkedin.com/docs/signin-with-linkedin) Sign In service.
178+* `github` - Via [Github][https://github.com/settings/developers] OAuth App. Also supports restricting via org and team.
179+
180+ -github-org="": restrict logins to members of this organisation
181+ -github-team="": restrict logins to members of this team
176
182
177## Adding a new Provider
183## Adding a new Provider
178
184
api/api.go+1−2
@@ -10,8 +10,7 @@ import (
10)
10)
11
11
12func Request(req *http.Request) (*simplejson.Json, error) {
12func Request(req *http.Request) (*simplejson.Json, error) {
13− httpclient := &http.Client{}
14− resp, err := httpclient.Do(req)
13+ resp, err := http.DefaultClient.Do(req)
15 if err != nil {
14 if err != nil {
16 return nil, err
15 return nil, err
17 }
16 }
main.go+3
@@ -17,6 +17,7 @@ import (
17)
17)
18
18
19func main() {
19func main() {
20+ log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
20 flagSet := flag.NewFlagSet("google_auth_proxy", flag.ExitOnError)
21 flagSet := flag.NewFlagSet("google_auth_proxy", flag.ExitOnError)
21
22
22 googleAppsDomains := StringArray{}
23 googleAppsDomains := StringArray{}
@@ -35,6 +36,8 @@ func main() {
35 flagSet.Var(&skipAuthRegex, "skip-auth-regex", "bypass authentication for requests path's that match (may be given multiple times)")
36 flagSet.Var(&skipAuthRegex, "skip-auth-regex", "bypass authentication for requests path's that match (may be given multiple times)")
36
37
37 flagSet.Var(&googleAppsDomains, "google-apps-domain", "authenticate against the given Google apps domain (may be given multiple times)")
38 flagSet.Var(&googleAppsDomains, "google-apps-domain", "authenticate against the given Google apps domain (may be given multiple times)")
39+ flagSet.String("github-org", "", "restrict logins to members of this organisation")
40+ flagSet.String("github-team", "", "restrict logins to members of this team")
38 flagSet.String("client-id", "", "the Google OAuth Client ID: ie: \"123456.apps.googleusercontent.com\"")
41 flagSet.String("client-id", "", "the Google OAuth Client ID: ie: \"123456.apps.googleusercontent.com\"")
39 flagSet.String("client-secret", "", "the OAuth Client Secret")
42 flagSet.String("client-secret", "", "the OAuth Client Secret")
40 flagSet.String("authenticated-emails-file", "", "authenticate against emails via file (one per line)")
43 flagSet.String("authenticated-emails-file", "", "authenticate against emails via file (one per line)")
oauthproxy.go+17−37
@@ -1,7 +1,6 @@
1package main
1package main
2
2
3import (
3import (
4− "bytes"
5 "crypto/aes"
4 "crypto/aes"
6 "crypto/cipher"
5 "crypto/cipher"
7 "encoding/base64"
6 "encoding/base64"
@@ -17,7 +16,6 @@ import (
17 "strings"
16 "strings"
18 "time"
17 "time"
19
18
20− "github.com/bitly/google_auth_proxy/api"
21 "github.com/bitly/google_auth_proxy/providers"
19 "github.com/bitly/google_auth_proxy/providers"
22)
20)
23
21
@@ -39,7 +37,6 @@ type OauthProxy struct {
39
37
40 redirectUrl *url.URL // the url to receive requests at
38 redirectUrl *url.URL // the url to receive requests at
41 provider providers.Provider
39 provider providers.Provider
42− oauthRedemptionUrl *url.URL // endpoint to redeem the code
43 oauthLoginUrl *url.URL // to redirect the user to
40 oauthLoginUrl *url.URL // to redirect the user to
44 oauthValidateUrl *url.URL // to validate the access token
41 oauthValidateUrl *url.URL // to validate the access token
45 oauthScope string
42 oauthScope string
@@ -143,21 +140,20 @@ func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy {
143 CookieRefresh: opts.CookieRefresh,
140 CookieRefresh: opts.CookieRefresh,
144 Validator: validator,
141 Validator: validator,
145
142
146− clientID: opts.ClientID,
143+ clientID: opts.ClientID,
147− clientSecret: opts.ClientSecret,
144+ clientSecret: opts.ClientSecret,
148− oauthScope: opts.provider.Data().Scope,
145+ oauthScope: opts.provider.Data().Scope,
149− provider: opts.provider,
146+ provider: opts.provider,
150− oauthRedemptionUrl: opts.provider.Data().RedeemUrl,
151− oauthLoginUrl: opts.provider.Data().LoginUrl,
147+ oauthLoginUrl: opts.provider.Data().LoginUrl,
152− oauthValidateUrl: opts.provider.Data().ValidateUrl,
148+ oauthValidateUrl: opts.provider.Data().ValidateUrl,
153− serveMux: serveMux,
149+ serveMux: serveMux,
154− redirectUrl: redirectUrl,
150+ redirectUrl: redirectUrl,
155− skipAuthRegex: opts.SkipAuthRegex,
151+ skipAuthRegex: opts.SkipAuthRegex,
156− compiledRegex: opts.CompiledRegex,
152+ compiledRegex: opts.CompiledRegex,
157− PassBasicAuth: opts.PassBasicAuth,
153+ PassBasicAuth: opts.PassBasicAuth,
158− PassAccessToken: opts.PassAccessToken,
154+ PassAccessToken: opts.PassAccessToken,
159− AesCipher: aes_cipher,
155+ AesCipher: aes_cipher,
160− templates: loadTemplates(opts.CustomTemplatesDir),
156+ templates: loadTemplates(opts.CustomTemplatesDir),
161 }
157 }
162}
158}
163
159
@@ -200,29 +196,13 @@ func (p *OauthProxy) redeemCode(host, code string) (string, string, error) {
200 if code == "" {
196 if code == "" {
201 return "", "", errors.New("missing code")
197 return "", "", errors.New("missing code")
202 }
198 }
203− params := url.Values{}
204− params.Add("redirect_uri", p.GetRedirectUrl(host))
199+ redirectUri := p.GetRedirectUrl(host)
205− params.Add("client_id", p.clientID)
200+ body, access_token, err := p.provider.Redeem(redirectUri, code)
206− params.Add("client_secret", p.clientSecret)
207− params.Add("code", code)
208− params.Add("grant_type", "authorization_code")
209− req, err := http.NewRequest("POST", p.oauthRedemptionUrl.String(), bytes.NewBufferString(params.Encode()))
210− if err != nil {
211− log.Printf("failed building request %s", err.Error())
212− return "", "", err
213− }
214− req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
215− json, err := api.Request(req)
216− if err != nil {
217− log.Printf("failed making request %s", err)
218− return "", "", err
219− }
220− access_token, err := json.Get("access_token").String()
221 if err != nil {
201 if err != nil {
222 return "", "", err
202 return "", "", err
223 }
203 }
224
204
225− email, err := p.provider.GetEmailAddress(json, access_token)
205+ email, err := p.provider.GetEmailAddress(body, access_token)
226 if err != nil {
206 if err != nil {
227 return "", "", err
207 return "", "", err
228 }
208 }
oauthproxy_test.go+11−9
@@ -1,10 +1,10 @@
1package main
1package main
2
2
3import (
3import (
4− "github.com/bitly/go-simplejson"
5 "github.com/bitly/google_auth_proxy/providers"
4 "github.com/bitly/google_auth_proxy/providers"
6 "github.com/bmizerany/assert"
5 "github.com/bmizerany/assert"
7 "io/ioutil"
6 "io/ioutil"
7+ "log"
8 "net"
8 "net"
9 "net/http"
9 "net/http"
10 "net/http/httptest"
10 "net/http/httptest"
@@ -15,6 +15,11 @@ import (
15 "time"
15 "time"
16)
16)
17
17
18+func init() {
19+ log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
20+
21+}
22+
18func TestNewReverseProxy(t *testing.T) {
23func TestNewReverseProxy(t *testing.T) {
19 backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
24 backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
20 w.WriteHeader(200)
25 w.WriteHeader(200)
@@ -89,8 +94,7 @@ type TestProvider struct {
89 ValidToken bool
94 ValidToken bool
90}
95}
91
96
92−func (tp *TestProvider) GetEmailAddress(unused_auth_response *simplejson.Json,
97+func (tp *TestProvider) GetEmailAddress(body []byte, access_token string) (string, error) {
93− unused_access_token string) (string, error) {
94 return tp.EmailAddress, nil
98 return tp.EmailAddress, nil
95}
99}
96
100
@@ -113,16 +117,15 @@ func NewPassAccessTokenTest(opts PassAccessTokenTestOptions) *PassAccessTokenTes
113
117
114 t.provider_server = httptest.NewServer(
118 t.provider_server = httptest.NewServer(
115 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
119 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
120+ log.Printf("%#v", r)
116 url := r.URL
121 url := r.URL
117 payload := ""
122 payload := ""
118 switch url.Path {
123 switch url.Path {
119 case "/oauth/token":
124 case "/oauth/token":
120 payload = `{"access_token": "my_auth_token"}`
125 payload = `{"access_token": "my_auth_token"}`
121 default:
126 default:
122− token_header := r.Header["X-Forwarded-Access-Token"]
127+ payload = r.Header.Get("X-Forwarded-Access-Token")
123− if len(token_header) != 0 {
124− payload = token_header[0]
128+ if payload == "" {
125− } else {
126 payload = "No access token found."
129 payload = "No access token found."
127 }
130 }
128 }
131 }
@@ -189,8 +192,7 @@ func (pat_test *PassAccessTokenTest) getCallbackEndpoint() (http_code int,
189 return rw.Code, rw.HeaderMap["Set-Cookie"][0]
192 return rw.Code, rw.HeaderMap["Set-Cookie"][0]
190}
193}
191
194
192−func (pat_test *PassAccessTokenTest) getRootEndpoint(
195+func (pat_test *PassAccessTokenTest) getRootEndpoint(cookie string) (http_code int, access_token string) {
193− cookie string) (http_code int, access_token string) {
194 cookie_key := pat_test.proxy.CookieKey
196 cookie_key := pat_test.proxy.CookieKey
195 var value string
197 var value string
196 key_prefix := cookie_key + "="
198 key_prefix := cookie_key + "="
options.go+8−1
@@ -19,6 +19,8 @@ type Options struct {
19
19
20 AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"`
20 AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"`
21 GoogleAppsDomains []string `flag:"google-apps-domain" cfg:"google_apps_domains"`
21 GoogleAppsDomains []string `flag:"google-apps-domain" cfg:"google_apps_domains"`
22+ GitHubOrg string `flag:"github-org" cfg:"github_org"`
23+ GitHubTeam string `flag:"github-team" cfg:"github_team"`
22 HtpasswdFile string `flag:"htpasswd-file" cfg:"htpasswd_file"`
24 HtpasswdFile string `flag:"htpasswd-file" cfg:"htpasswd_file"`
23 DisplayHtpasswdForm bool `flag:"display-htpasswd-form" cfg:"display_htpasswd_form"`
25 DisplayHtpasswdForm bool `flag:"display-htpasswd-form" cfg:"display_htpasswd_form"`
24 CustomTemplatesDir string `flag:"custom-templates-dir" cfg:"custom_templates_dir"`
26 CustomTemplatesDir string `flag:"custom-templates-dir" cfg:"custom_templates_dir"`
@@ -153,11 +155,16 @@ func (o *Options) Validate() error {
153}
155}
154
156
155func parseProviderInfo(o *Options, msgs []string) []string {
157func parseProviderInfo(o *Options, msgs []string) []string {
156− p := &providers.ProviderData{Scope: o.Scope}
158+ p := &providers.ProviderData{Scope: o.Scope, ClientID: o.ClientID, ClientSecret: o.ClientSecret}
157 p.LoginUrl, msgs = parseUrl(o.LoginUrl, "login", msgs)
159 p.LoginUrl, msgs = parseUrl(o.LoginUrl, "login", msgs)
158 p.RedeemUrl, msgs = parseUrl(o.RedeemUrl, "redeem", msgs)
160 p.RedeemUrl, msgs = parseUrl(o.RedeemUrl, "redeem", msgs)
159 p.ProfileUrl, msgs = parseUrl(o.ProfileUrl, "profile", msgs)
161 p.ProfileUrl, msgs = parseUrl(o.ProfileUrl, "profile", msgs)
160 p.ValidateUrl, msgs = parseUrl(o.ValidateUrl, "validate", msgs)
162 p.ValidateUrl, msgs = parseUrl(o.ValidateUrl, "validate", msgs)
163+
161 o.provider = providers.New(o.Provider, p)
164 o.provider = providers.New(o.Provider, p)
165+ switch p := o.provider.(type) {
166+ case *providers.GitHubProvider:
167+ p.SetOrgTeam(o.GitHubOrg, o.GitHubTeam)
168+ }
162 return msgs
169 return msgs
163}
170}
providers/github.gonew file+136
@@ -0,0 +1,136 @@
1+package providers
2+
3+import (
4+ "encoding/json"
5+ "io/ioutil"
6+ "net/http"
7+ "net/url"
8+)
9+
10+type GitHubProvider struct {
11+ *ProviderData
12+ Org string
13+ Team string
14+}
15+
16+func NewGitHubProvider(p *ProviderData) *GitHubProvider {
17+ p.ProviderName = "GitHub"
18+ if p.LoginUrl.String() == "" {
19+ p.LoginUrl = &url.URL{
20+ Scheme: "https",
21+ Host: "github.com",
22+ Path: "/login/oauth/authorize",
23+ }
24+ }
25+ if p.RedeemUrl.String() == "" {
26+ p.RedeemUrl = &url.URL{
27+ Scheme: "https",
28+ Host: "github.com",
29+ Path: "/login/oauth/access_token",
30+ }
31+ }
32+ if p.ValidateUrl.String() == "" {
33+ p.ValidateUrl = &url.URL{
34+ Scheme: "https",
35+ Host: "api.github.com",
36+ Path: "/user/emails",
37+ }
38+ }
39+ if p.Scope == "" {
40+ p.Scope = "user:email"
41+ }
42+ return &GitHubProvider{ProviderData: p}
43+}
44+func (p *GitHubProvider) SetOrgTeam(org, team string) {
45+ p.Org = org
46+ p.Team = team
47+ if org != "" || team != "" {
48+ p.Scope += " read:org"
49+ }
50+}
51+
52+func (p *GitHubProvider) hasOrgAndTeam(accessToken string) (bool, error) {
53+
54+ var teams []struct {
55+ Name string `json:"name"`
56+ Slug string `json:"slug"`
57+ Org struct {
58+ Login string `json:"login"`
59+ } `json:"organization"`
60+ }
61+
62+ params := url.Values{
63+ "access_token": {accessToken},
64+ }
65+
66+ req, _ := http.NewRequest("GET", "https://api.github.com/user/teams?"+params.Encode(), nil)
67+ req.Header.Set("Accept", "application/vnd.github.moondragon+json")
68+ resp, err := http.DefaultClient.Do(req)
69+ if err != nil {
70+ return false, err
71+ }
72+
73+ body, err := ioutil.ReadAll(resp.Body)
74+ resp.Body.Close()
75+ if err != nil {
76+ return false, err
77+ }
78+
79+ if err := json.Unmarshal(body, &teams); err != nil {
80+ return false, err
81+ }
82+
83+ for _, team := range teams {
84+ if p.Org == team.Org.Login {
85+ if p.Team == "" || p.Team == team.Slug {
86+ return true, nil
87+ }
88+ }
89+ }
90+ return false, nil
91+}
92+
93+func (p *GitHubProvider) GetEmailAddress(body []byte, access_token string) (string, error) {
94+
95+ var emails []struct {
96+ Email string `json:"email"`
97+ Primary bool `json:"primary"`
98+ }
99+
100+ params := url.Values{
101+ "access_token": {access_token},
102+ }
103+
104+ // if we require an Org or Team, check that first
105+ if p.Org != "" || p.Team != "" {
106+ if ok, err := p.hasOrgAndTeam(access_token); err != nil || !ok {
107+ return "", err
108+ }
109+ }
110+
111+ resp, err := http.DefaultClient.Get("https://api.github.com/user/emails?" + params.Encode())
112+ if err != nil {
113+ return "", err
114+ }
115+ body, err = ioutil.ReadAll(resp.Body)
116+ resp.Body.Close()
117+ if err != nil {
118+ return "", err
119+ }
120+
121+ if err := json.Unmarshal(body, &emails); err != nil {
122+ return "", err
123+ }
124+
125+ for _, email := range emails {
126+ if email.Primary {
127+ return email.Email, nil
128+ }
129+ }
130+
131+ return "", nil
132+}
133+
134+func (p *GitHubProvider) ValidateToken(access_token string) bool {
135+ return validateToken(p, access_token, nil)
136+}
providers/google.go+18−12
@@ -2,10 +2,10 @@ package providers
2
2
3import (
3import (
4 "encoding/base64"
4 "encoding/base64"
5+ "encoding/json"
6+ "errors"
5 "net/url"
7 "net/url"
6 "strings"
8 "strings"
7−
8− "github.com/bitly/go-simplejson"
9)
9)
10
10
11type GoogleProvider struct {
11type GoogleProvider struct {
@@ -35,28 +35,34 @@ func NewGoogleProvider(p *ProviderData) *GoogleProvider {
35 return &GoogleProvider{ProviderData: p}
35 return &GoogleProvider{ProviderData: p}
36}
36}
37
37
38−func (s *GoogleProvider) GetEmailAddress(auth_response *simplejson.Json,
38+func (s *GoogleProvider) GetEmailAddress(body []byte, access_token string) (string, error) {
39− unused_access_token string) (string, error) {
39+ var response struct {
40− idToken, err := auth_response.Get("id_token").String()
40+ IdToken string `json:"id_token"`
41− if err != nil {
41+ }
42+
43+ if err := json.Unmarshal(body, &response); err != nil {
42 return "", err
44 return "", err
43 }
45 }
46+
44 // id_token is a base64 encode ID token payload
47 // id_token is a base64 encode ID token payload
45 // https://developers.google.com/accounts/docs/OAuth2Login#obtainuserinfo
48 // https://developers.google.com/accounts/docs/OAuth2Login#obtainuserinfo
46− jwt := strings.Split(idToken, ".")
49+ jwt := strings.Split(response.IdToken, ".")
47 b, err := jwtDecodeSegment(jwt[1])
50 b, err := jwtDecodeSegment(jwt[1])
48 if err != nil {
51 if err != nil {
49 return "", err
52 return "", err
50 }
53 }
51− data, err := simplejson.NewJson(b)
54+
52− if err != nil {
55+ var email struct {
53− return "", err
56+ Email string `json:"email"`
54 }
57 }
55− email, err := data.Get("email").String()
58+ err = json.Unmarshal(b, &email)
56 if err != nil {
59 if err != nil {
57 return "", err
60 return "", err
58 }
61 }
62+ if email.Email == "" {
63+ return "", errors.New("missing email")
64+ }
59− return email, nil
65+ return email.Email, nil
60}
66}
61
67
62func jwtDecodeSegment(seg string) ([]byte, error) {
68func jwtDecodeSegment(seg string) ([]byte, error) {
providers/google_test.go+38−16
@@ -2,7 +2,7 @@ package providers
2
2
3import (
3import (
4 "encoding/base64"
4 "encoding/base64"
5− "github.com/bitly/go-simplejson"
5+ "encoding/json"
6 "github.com/bmizerany/assert"
6 "github.com/bmizerany/assert"
7 "net/url"
7 "net/url"
8 "testing"
8 "testing"
@@ -68,39 +68,61 @@ func TestGoogleProviderOverrides(t *testing.T) {
68
68
69func TestGoogleProviderGetEmailAddress(t *testing.T) {
69func TestGoogleProviderGetEmailAddress(t *testing.T) {
70 p := newGoogleProvider()
70 p := newGoogleProvider()
71− j := simplejson.New()
71+ body, err := json.Marshal(
72+ struct {
73+ IdToken string `json:"id_token"`
74+ }{
72− j.Set("id_token", "ignored prefix."+base64.URLEncoding.EncodeToString(
75+ IdToken: "ignored prefix." + base64.URLEncoding.EncodeToString([]byte(`{"email": "michael.bland@gsa.gov"}`)),
73− []byte("{\"email\": \"michael.bland@gsa.gov\"}")))
76+ },
77+ )
78+ assert.Equal(t, nil, err)
74− email, err := p.GetEmailAddress(j, "ignored access_token")
79+ email, err := p.GetEmailAddress(body, "ignored access_token")
75 assert.Equal(t, "michael.bland@gsa.gov", email)
80 assert.Equal(t, "michael.bland@gsa.gov", email)
76 assert.Equal(t, nil, err)
81 assert.Equal(t, nil, err)
77}
82}
78
83
79func TestGoogleProviderGetEmailAddressInvalidEncoding(t *testing.T) {
84func TestGoogleProviderGetEmailAddressInvalidEncoding(t *testing.T) {
80 p := newGoogleProvider()
85 p := newGoogleProvider()
81− j := simplejson.New()
86+ body, err := json.Marshal(
87+ struct {
88+ IdToken string `json:"id_token"`
89+ }{
82− j.Set("id_token", "ignored prefix.{\"email\": \"michael.bland@gsa.gov\"}")
90+ IdToken: "ignored prefix." + `{"email": "michael.bland@gsa.gov"}`,
91+ },
92+ )
93+ assert.Equal(t, nil, err)
83− email, err := p.GetEmailAddress(j, "ignored access_token")
94+ email, err := p.GetEmailAddress(body, "ignored access_token")
84 assert.Equal(t, "", email)
95 assert.Equal(t, "", email)
85 assert.NotEqual(t, nil, err)
96 assert.NotEqual(t, nil, err)
86}
97}
87
98
88func TestGoogleProviderGetEmailAddressInvalidJson(t *testing.T) {
99func TestGoogleProviderGetEmailAddressInvalidJson(t *testing.T) {
89 p := newGoogleProvider()
100 p := newGoogleProvider()
90− j := simplejson.New()
101+
102+ body, err := json.Marshal(
103+ struct {
104+ IdToken string `json:"id_token"`
105+ }{
91− j.Set("id_token", "ignored prefix."+base64.URLEncoding.EncodeToString(
106+ IdToken: "ignored prefix." + base64.URLEncoding.EncodeToString([]byte(`{"email": michael.bland@gsa.gov}`)),
92− []byte("{email: michael.bland@gsa.gov}")))
107+ },
108+ )
109+ assert.Equal(t, nil, err)
93− email, err := p.GetEmailAddress(j, "ignored access_token")
110+ email, err := p.GetEmailAddress(body, "ignored access_token")
94 assert.Equal(t, "", email)
111 assert.Equal(t, "", email)
95 assert.NotEqual(t, nil, err)
112 assert.NotEqual(t, nil, err)
96}
113}
97
114
98func TestGoogleProviderGetEmailAddressEmailMissing(t *testing.T) {
115func TestGoogleProviderGetEmailAddressEmailMissing(t *testing.T) {
99 p := newGoogleProvider()
116 p := newGoogleProvider()
100− j := simplejson.New()
117+ body, err := json.Marshal(
118+ struct {
119+ IdToken string `json:"id_token"`
120+ }{
101− j.Set("id_token", "ignored prefix."+base64.URLEncoding.EncodeToString(
121+ IdToken: "ignored prefix." + base64.URLEncoding.EncodeToString([]byte(`{"not_email": "missing"}`)),
102− []byte("{\"not_email\": \"missing!\"}")))
122+ },
123+ )
124+ assert.Equal(t, nil, err)
103− email, err := p.GetEmailAddress(j, "ignored access_token")
125+ email, err := p.GetEmailAddress(body, "ignored access_token")
104 assert.Equal(t, "", email)
126 assert.Equal(t, "", email)
105 assert.NotEqual(t, nil, err)
127 assert.NotEqual(t, nil, err)
106}
128}
providers/internal_util_test.go+1−4
@@ -1,7 +1,6 @@
1package providers
1package providers
2
2
3import (
3import (
4− "github.com/bitly/go-simplejson"
5 "github.com/bmizerany/assert"
4 "github.com/bmizerany/assert"
6 "net/http"
5 "net/http"
7 "net/http/httptest"
6 "net/http/httptest"
@@ -13,9 +12,7 @@ type ValidateTokenTestProvider struct {
13 *ProviderData
12 *ProviderData
14}
13}
15
14
16−func (tp *ValidateTokenTestProvider) GetEmailAddress(
15+func (tp *ValidateTokenTestProvider) GetEmailAddress(body []byte, access_token string) (string, error) {
17− unused_auth_response *simplejson.Json,
18− unused_access_token string) (string, error) {
19 return "", nil
16 return "", nil
20}
17}
21
18
providers/linkedin.go+1−3
@@ -8,7 +8,6 @@ import (
8 "net/http"
8 "net/http"
9 "net/url"
9 "net/url"
10
10
11− "github.com/bitly/go-simplejson"
12 "github.com/bitly/google_auth_proxy/api"
11 "github.com/bitly/google_auth_proxy/api"
13)
12)
14
13
@@ -50,8 +49,7 @@ func getLinkedInHeader(access_token string) http.Header {
50 return header
49 return header
51}
50}
52
51
53−func (p *LinkedInProvider) GetEmailAddress(unused_auth_response *simplejson.Json,
54− access_token string) (string, error) {
52+func (p *LinkedInProvider) GetEmailAddress(body []byte, access_token string) (string, error) {
55 if access_token == "" {
53 if access_token == "" {
56 return "", errors.New("missing access token")
54 return "", errors.New("missing access token")
57 }
55 }
providers/linkedin_test.go+3−9
@@ -1,7 +1,6 @@
1package providers
1package providers
2
2
3import (
3import (
4− "github.com/bitly/go-simplejson"
5 "github.com/bmizerany/assert"
4 "github.com/bmizerany/assert"
6 "net/http"
5 "net/http"
7 "net/http/httptest"
6 "net/http/httptest"
@@ -97,9 +96,8 @@ func TestLinkedInProviderGetEmailAddress(t *testing.T) {
97
96
98 b_url, _ := url.Parse(b.URL)
97 b_url, _ := url.Parse(b.URL)
99 p := testLinkedInProvider(b_url.Host)
98 p := testLinkedInProvider(b_url.Host)
100− unused_auth_response := simplejson.New()
101
99
102− email, err := p.GetEmailAddress(unused_auth_response,
100+ email, err := p.GetEmailAddress([]byte{},
103 "imaginary_access_token")
101 "imaginary_access_token")
104 assert.Equal(t, nil, err)
102 assert.Equal(t, nil, err)
105 assert.Equal(t, "user@linkedin.com", email)
103 assert.Equal(t, "user@linkedin.com", email)
@@ -111,13 +109,11 @@ func TestLinkedInProviderGetEmailAddressFailedRequest(t *testing.T) {
111
109
112 b_url, _ := url.Parse(b.URL)
110 b_url, _ := url.Parse(b.URL)
113 p := testLinkedInProvider(b_url.Host)
111 p := testLinkedInProvider(b_url.Host)
114− unused_auth_response := simplejson.New()
115
112
116 // We'll trigger a request failure by using an unexpected access
113 // We'll trigger a request failure by using an unexpected access
117 // token. Alternatively, we could allow the parsing of the payload as
114 // token. Alternatively, we could allow the parsing of the payload as
118 // JSON to fail.
115 // JSON to fail.
119− email, err := p.GetEmailAddress(unused_auth_response,
120− "unexpected_access_token")
116+ email, err := p.GetEmailAddress([]byte{}, "unexpected_access_token")
121 assert.NotEqual(t, nil, err)
117 assert.NotEqual(t, nil, err)
122 assert.Equal(t, "", email)
118 assert.Equal(t, "", email)
123}
119}
@@ -128,10 +124,8 @@ func TestLinkedInProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {
128
124
129 b_url, _ := url.Parse(b.URL)
125 b_url, _ := url.Parse(b.URL)
130 p := testLinkedInProvider(b_url.Host)
126 p := testLinkedInProvider(b_url.Host)
131− unused_auth_response := simplejson.New()
132
127
133− email, err := p.GetEmailAddress(unused_auth_response,
134− "imaginary_access_token")
128+ email, err := p.GetEmailAddress([]byte{}, "imaginary_access_token")
135 assert.NotEqual(t, nil, err)
129 assert.NotEqual(t, nil, err)
136 assert.Equal(t, "", email)
130 assert.Equal(t, "", email)
137}
131}
providers/myusa.go+1−3
@@ -5,7 +5,6 @@ import (
5 "net/http"
5 "net/http"
6 "net/url"
6 "net/url"
7
7
8− "github.com/bitly/go-simplejson"
9 "github.com/bitly/google_auth_proxy/api"
8 "github.com/bitly/google_auth_proxy/api"
10)
9)
11
10
@@ -43,8 +42,7 @@ func NewMyUsaProvider(p *ProviderData) *MyUsaProvider {
43 return &MyUsaProvider{ProviderData: p}
42 return &MyUsaProvider{ProviderData: p}
44}
43}
45
44
46−func (p *MyUsaProvider) GetEmailAddress(auth_response *simplejson.Json,
47− access_token string) (string, error) {
45+func (p *MyUsaProvider) GetEmailAddress(body []byte, access_token string) (string, error) {
48 req, err := http.NewRequest("GET",
46 req, err := http.NewRequest("GET",
49 p.ProfileUrl.String()+"?access_token="+access_token, nil)
47 p.ProfileUrl.String()+"?access_token="+access_token, nil)
50 if err != nil {
48 if err != nil {
providers/myusa_test.go+3−10
@@ -1,7 +1,6 @@
1package providers
1package providers
2
2
3import (
3import (
4− "github.com/bitly/go-simplejson"
5 "github.com/bmizerany/assert"
4 "github.com/bmizerany/assert"
6 "net/http"
5 "net/http"
7 "net/http/httptest"
6 "net/http/httptest"
@@ -102,10 +101,8 @@ func TestMyUsaProviderGetEmailAddress(t *testing.T) {
102
101
103 b_url, _ := url.Parse(b.URL)
102 b_url, _ := url.Parse(b.URL)
104 p := testMyUsaProvider(b_url.Host)
103 p := testMyUsaProvider(b_url.Host)
105− unused_auth_response := simplejson.New()
106
104
107− email, err := p.GetEmailAddress(unused_auth_response,
108− "imaginary_access_token")
105+ email, err := p.GetEmailAddress([]byte{}, "imaginary_access_token")
109 assert.Equal(t, nil, err)
106 assert.Equal(t, nil, err)
110 assert.Equal(t, "michael.bland@gsa.gov", email)
107 assert.Equal(t, "michael.bland@gsa.gov", email)
111}
108}
@@ -118,13 +115,11 @@ func TestMyUsaProviderGetEmailAddressFailedRequest(t *testing.T) {
118
115
119 b_url, _ := url.Parse(b.URL)
116 b_url, _ := url.Parse(b.URL)
120 p := testMyUsaProvider(b_url.Host)
117 p := testMyUsaProvider(b_url.Host)
121− unused_auth_response := simplejson.New()
122
118
123 // We'll trigger a request failure by using an unexpected access
119 // We'll trigger a request failure by using an unexpected access
124 // token. Alternatively, we could allow the parsing of the payload as
120 // token. Alternatively, we could allow the parsing of the payload as
125 // JSON to fail.
121 // JSON to fail.
126− email, err := p.GetEmailAddress(unused_auth_response,
127− "unexpected_access_token")
122+ email, err := p.GetEmailAddress([]byte{}, "unexpected_access_token")
128 assert.NotEqual(t, nil, err)
123 assert.NotEqual(t, nil, err)
129 assert.Equal(t, "", email)
124 assert.Equal(t, "", email)
130}
125}
@@ -135,10 +130,8 @@ func TestMyUsaProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {
135
130
136 b_url, _ := url.Parse(b.URL)
131 b_url, _ := url.Parse(b.URL)
137 p := testMyUsaProvider(b_url.Host)
132 p := testMyUsaProvider(b_url.Host)
138− unused_auth_response := simplejson.New()
139
133
140− email, err := p.GetEmailAddress(unused_auth_response,
141− "imaginary_access_token")
134+ email, err := p.GetEmailAddress([]byte{}, "imaginary_access_token")
142 assert.NotEqual(t, nil, err)
135 assert.NotEqual(t, nil, err)
143 assert.Equal(t, "", email)
136 assert.Equal(t, "", email)
144}
137}
providers/provider_data.go+2
@@ -6,6 +6,8 @@ import (
6
6
7type ProviderData struct {
7type ProviderData struct {
8 ProviderName string
8 ProviderName string
9+ ClientID string
10+ ClientSecret string
9 LoginUrl *url.URL
11 LoginUrl *url.URL
10 RedeemUrl *url.URL
12 RedeemUrl *url.URL
11 ProfileUrl *url.URL
13 ProfileUrl *url.URL
providers/provider_default.gonew file+51
@@ -0,0 +1,51 @@
1+package providers
2+
3+import (
4+ "bytes"
5+ "encoding/json"
6+ "errors"
7+ "io/ioutil"
8+ "net/http"
9+ "net/url"
10+)
11+
12+func (p *ProviderData) Redeem(redirectUrl, code string) (body []byte, token string, err error) {
13+ if code == "" {
14+ err = errors.New("missing code")
15+ return
16+ }
17+
18+ params := url.Values{}
19+ params.Add("redirect_uri", redirectUrl)
20+ params.Add("client_id", p.ClientID)
21+ params.Add("client_secret", p.ClientSecret)
22+ params.Add("code", code)
23+ params.Add("grant_type", "authorization_code")
24+ req, err := http.NewRequest("POST", p.RedeemUrl.String(), bytes.NewBufferString(params.Encode()))
25+ if err != nil {
26+ return nil, "", err
27+ }
28+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
29+
30+ resp, err := http.DefaultClient.Do(req)
31+ if err != nil {
32+ return nil, "", err
33+ }
34+ body, err = ioutil.ReadAll(resp.Body)
35+ resp.Body.Close()
36+ if err != nil {
37+ return nil, "", err
38+ }
39+
40+ // blindly try json and x-www-form-urlencoded
41+ var jsonResponse struct {
42+ AccessToken string `json:"access_token"`
43+ }
44+ err = json.Unmarshal(body, &jsonResponse)
45+ if err == nil {
46+ return body, jsonResponse.AccessToken, nil
47+ }
48+
49+ v, err := url.ParseQuery(string(body))
50+ return body, v.Get("access_token"), err
51+}
providers/providers.go+4−6
@@ -1,13 +1,9 @@
1package providers
1package providers
2
2
3−import (
4− "github.com/bitly/go-simplejson"
5−)
6−
7type Provider interface {
3type Provider interface {
8 Data() *ProviderData
4 Data() *ProviderData
9− GetEmailAddress(auth_response *simplejson.Json,
5+ GetEmailAddress(body []byte, access_token string) (string, error)
10− access_token string) (string, error)
6+ Redeem(string, string) ([]byte, string, error)
11 ValidateToken(access_token string) bool
7 ValidateToken(access_token string) bool
12}
8}
13
9
@@ -17,6 +13,8 @@ func New(provider string, p *ProviderData) Provider {
17 return NewMyUsaProvider(p)
13 return NewMyUsaProvider(p)
18 case "linkedin":
14 case "linkedin":
19 return NewLinkedInProvider(p)
15 return NewLinkedInProvider(p)
16+ case "github":
17+ return NewGitHubProvider(p)
20 default:
18 default:
21 return NewGoogleProvider(p)
19 return NewGoogleProvider(p)
22 }
20 }