git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge pull request #77 from 18F/extract-providers-package
0136ab0Jehiah Czebotar
@@ -50,6 +50,12 @@ func main() {
50
50
51 flagSet.Bool("request-logging", true, "Log requests to stdout")
51 flagSet.Bool("request-logging", true, "Log requests to stdout")
52
52
53+ flagSet.String("provider", "", "Oauth provider (defaults to Google)")
54+ flagSet.String("login-url", "", "Authentication endpoint")
55+ flagSet.String("redeem-url", "", "Token redemption endpoint")
56+ flagSet.String("profile-url", "", "Profile access endpoint")
57+ flagSet.String("scope", "", "Oauth scope specification")
58+
53 flagSet.Parse(os.Args[1:])
59 flagSet.Parse(os.Args[1:])
54
60
55 if *showVersion {
61 if *showVersion {
oauthproxy.go+7−31
@@ -15,8 +15,8 @@ import (
15 "strings"
15 "strings"
16 "time"
16 "time"
17
17
18− "github.com/bitly/go-simplejson"
19 "github.com/bitly/google_auth_proxy/api"
18 "github.com/bitly/google_auth_proxy/api"
19+ "github.com/bitly/google_auth_proxy/providers"
20)
20)
21
21
22const pingPath = "/ping"
22const pingPath = "/ping"
@@ -34,6 +34,7 @@ type OauthProxy struct {
34 Validator func(string) bool
34 Validator func(string) bool
35
35
36 redirectUrl *url.URL // the url to receive requests at
36 redirectUrl *url.URL // the url to receive requests at
37+ provider providers.Provider
37 oauthRedemptionUrl *url.URL // endpoint to redeem the code
38 oauthRedemptionUrl *url.URL // endpoint to redeem the code
38 oauthLoginUrl *url.URL // to redirect the user to
39 oauthLoginUrl *url.URL // to redirect the user to
39 oauthScope string
40 oauthScope string
@@ -83,8 +84,6 @@ func setProxyDirector(proxy *httputil.ReverseProxy) {
83}
84}
84
85
85func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy {
86func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy {
86− login, _ := url.Parse("https://accounts.google.com/o/oauth2/auth")
87− redeem, _ := url.Parse("https://accounts.google.com/o/oauth2/token")
88 serveMux := http.NewServeMux()
87 serveMux := http.NewServeMux()
89 for _, u := range opts.proxyUrls {
88 for _, u := range opts.proxyUrls {
90 path := u.Path
89 path := u.Path
@@ -128,9 +127,10 @@ func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy {
128
127
129 clientID: opts.ClientID,
128 clientID: opts.ClientID,
130 clientSecret: opts.ClientSecret,
129 clientSecret: opts.ClientSecret,
131− oauthScope: "profile email",
130+ oauthScope: opts.provider.Data().Scope,
131+ provider: opts.provider,
132− oauthRedemptionUrl: redeem,
132+ oauthRedemptionUrl: opts.provider.Data().RedeemUrl,
133− oauthLoginUrl: login,
133+ oauthLoginUrl: opts.provider.Data().LoginUrl,
134 serveMux: serveMux,
134 serveMux: serveMux,
135 redirectUrl: redirectUrl,
135 redirectUrl: redirectUrl,
136 skipAuthRegex: opts.SkipAuthRegex,
136 skipAuthRegex: opts.SkipAuthRegex,
@@ -201,23 +201,7 @@ func (p *OauthProxy) redeemCode(host, code string) (string, string, error) {
201 return "", "", err
201 return "", "", err
202 }
202 }
203
203
204− idToken, err := json.Get("id_token").String()
204+ email, err := p.provider.GetEmailAddress(json, access_token)
205− if err != nil {
206− return "", "", err
207− }
208−
209− // id_token is a base64 encode ID token payload
210− // https://developers.google.com/accounts/docs/OAuth2Login#obtainuserinfo
211− jwt := strings.Split(idToken, ".")
212− b, err := jwtDecodeSegment(jwt[1])
213− if err != nil {
214− return "", "", err
215− }
216− data, err := simplejson.NewJson(b)
217− if err != nil {
218− return "", "", err
219− }
220− email, err := data.Get("email").String()
221 if err != nil {
205 if err != nil {
222 return "", "", err
206 return "", "", err
223 }
207 }
@@ -225,14 +209,6 @@ func (p *OauthProxy) redeemCode(host, code string) (string, string, error) {
225 return access_token, email, nil
209 return access_token, email, nil
226}
210}
227
211
228−func jwtDecodeSegment(seg string) ([]byte, error) {
229− if l := len(seg) % 4; l > 0 {
230− seg += strings.Repeat("=", 4-l)
231− }
232−
233− return base64.URLEncoding.DecodeString(seg)
234−}
235−
236func (p *OauthProxy) ClearCookie(rw http.ResponseWriter, req *http.Request) {
212func (p *OauthProxy) ClearCookie(rw http.ResponseWriter, req *http.Request) {
237 domain := req.Host
213 domain := req.Host
238 if h, _, err := net.SplitHostPort(domain); err == nil {
214 if h, _, err := net.SplitHostPort(domain); err == nil {
options.go+31−6
@@ -6,6 +6,8 @@ import (
6 "regexp"
6 "regexp"
7 "strings"
7 "strings"
8 "time"
8 "time"
9+
10+ "github.com/bitly/google_auth_proxy/providers"
9)
11)
10
12
11// Configuration Options that can be set by Command Line Flag, or Config File
13// Configuration Options that can be set by Command Line Flag, or Config File
@@ -33,12 +35,21 @@ type Options struct {
33 PassBasicAuth bool `flag:"pass-basic-auth" cfg:"pass_basic_auth"`
35 PassBasicAuth bool `flag:"pass-basic-auth" cfg:"pass_basic_auth"`
34 PassHostHeader bool `flag:"pass-host-header" cfg:"pass_host_header"`
36 PassHostHeader bool `flag:"pass-host-header" cfg:"pass_host_header"`
35
37
38+ // These options allow for other providers besides Google, with
39+ // potential overrides.
40+ Provider string `flag:"provider" cfg:"provider"`
41+ LoginUrl string `flag:"login-url" cfg:"login_url"`
42+ RedeemUrl string `flag:"redeem-url" cfg:"redeem_url"`
43+ ProfileUrl string `flag:"profile-url" cfg:"profile_url"`
44+ Scope string `flag:"scope" cfg:"scope"`
45+
36 RequestLogging bool `flag:"request-logging" cfg:"request_logging"`
46 RequestLogging bool `flag:"request-logging" cfg:"request_logging"`
37
47
38 // internal values that are set after config validation
48 // internal values that are set after config validation
39 redirectUrl *url.URL
49 redirectUrl *url.URL
40 proxyUrls []*url.URL
50 proxyUrls []*url.URL
41 CompiledRegex []*regexp.Regexp
51 CompiledRegex []*regexp.Regexp
52+ provider providers.Provider
42}
53}
43
54
44func NewOptions() *Options {
55func NewOptions() *Options {
@@ -55,6 +66,15 @@ func NewOptions() *Options {
55 }
66 }
56}
67}
57
68
69+func parseUrl(to_parse string, urltype string, msgs []string) (*url.URL, []string) {
70+ parsed, err := url.Parse(to_parse)
71+ if err != nil {
72+ return nil, append(msgs, fmt.Sprintf(
73+ "error parsing %s-url=%q %s", urltype, to_parse, err))
74+ }
75+ return parsed, msgs
76+}
77+
58func (o *Options) Validate() error {
78func (o *Options) Validate() error {
59 msgs := make([]string, 0)
79 msgs := make([]string, 0)
60 if len(o.Upstreams) < 1 {
80 if len(o.Upstreams) < 1 {
@@ -70,12 +90,7 @@ func (o *Options) Validate() error {
70 msgs = append(msgs, "missing setting: client-secret")
90 msgs = append(msgs, "missing setting: client-secret")
71 }
91 }
72
92
73− redirectUrl, err := url.Parse(o.RedirectUrl)
93+ o.redirectUrl, msgs = parseUrl(o.RedirectUrl, "redirect", msgs)
74− if err != nil {
75− msgs = append(msgs, fmt.Sprintf(
76− "error parsing redirect-url=%q %s", o.RedirectUrl, err))
77− }
78− o.redirectUrl = redirectUrl
79
94
80 for _, u := range o.Upstreams {
95 for _, u := range o.Upstreams {
81 upstreamUrl, err := url.Parse(u)
96 upstreamUrl, err := url.Parse(u)
@@ -98,6 +113,7 @@ func (o *Options) Validate() error {
98 }
113 }
99 o.CompiledRegex = append(o.CompiledRegex, CompiledRegex)
114 o.CompiledRegex = append(o.CompiledRegex, CompiledRegex)
100 }
115 }
116+ msgs = parseProviderInfo(o, msgs)
101
117
102 if len(msgs) != 0 {
118 if len(msgs) != 0 {
103 return fmt.Errorf("Invalid configuration:\n %s",
119 return fmt.Errorf("Invalid configuration:\n %s",
@@ -105,3 +121,12 @@ func (o *Options) Validate() error {
105 }
121 }
106 return nil
122 return nil
107}
123}
124+
125+func parseProviderInfo(o *Options, msgs []string) []string {
126+ p := &providers.ProviderData{Scope: o.Scope}
127+ p.LoginUrl, msgs = parseUrl(o.LoginUrl, "login", msgs)
128+ p.RedeemUrl, msgs = parseUrl(o.RedeemUrl, "redeem", msgs)
129+ p.ProfileUrl, msgs = parseUrl(o.ProfileUrl, "profile", msgs)
130+ o.provider = providers.New(o.Provider, p)
131+ return msgs
132+}
options_test.go+12
@@ -90,3 +90,15 @@ func TestCompiledRegexError(t *testing.T) {
90 "unexpected ): `barquux)`"})
90 "unexpected ): `barquux)`"})
91 assert.Equal(t, expected, err.Error())
91 assert.Equal(t, expected, err.Error())
92}
92}
93+
94+func TestDefaultProviderApiSettings(t *testing.T) {
95+ o := testOptions()
96+ assert.Equal(t, nil, o.Validate())
97+ p := o.provider.Data()
98+ assert.Equal(t, "https://accounts.google.com/o/oauth2/auth",
99+ p.LoginUrl.String())
100+ assert.Equal(t, "https://accounts.google.com/o/oauth2/token",
101+ p.RedeemUrl.String())
102+ assert.Equal(t, "", p.ProfileUrl.String())
103+ assert.Equal(t, "profile email", p.Scope)
104+}
providers/google.gonew file+62
@@ -0,0 +1,62 @@
1+package providers
2+
3+import (
4+ "encoding/base64"
5+ "net/url"
6+ "strings"
7+
8+ "github.com/bitly/go-simplejson"
9+)
10+
11+type GoogleProvider struct {
12+ *ProviderData
13+}
14+
15+func NewGoogleProvider(p *ProviderData) *GoogleProvider {
16+ if p.LoginUrl.String() == "" {
17+ p.LoginUrl = &url.URL{Scheme: "https",
18+ Host: "accounts.google.com",
19+ Path: "/o/oauth2/auth"}
20+ }
21+ if p.RedeemUrl.String() == "" {
22+ p.RedeemUrl = &url.URL{Scheme: "https",
23+ Host: "accounts.google.com",
24+ Path: "/o/oauth2/token"}
25+ }
26+ if p.Scope == "" {
27+ p.Scope = "profile email"
28+ }
29+ return &GoogleProvider{ProviderData: p}
30+}
31+
32+func (s *GoogleProvider) GetEmailAddress(auth_response *simplejson.Json,
33+ unused_access_token string) (string, error) {
34+ idToken, err := auth_response.Get("id_token").String()
35+ if err != nil {
36+ return "", err
37+ }
38+ // id_token is a base64 encode ID token payload
39+ // https://developers.google.com/accounts/docs/OAuth2Login#obtainuserinfo
40+ jwt := strings.Split(idToken, ".")
41+ b, err := jwtDecodeSegment(jwt[1])
42+ if err != nil {
43+ return "", err
44+ }
45+ data, err := simplejson.NewJson(b)
46+ if err != nil {
47+ return "", err
48+ }
49+ email, err := data.Get("email").String()
50+ if err != nil {
51+ return "", err
52+ }
53+ return email, nil
54+}
55+
56+func jwtDecodeSegment(seg string) ([]byte, error) {
57+ if l := len(seg) % 4; l > 0 {
58+ seg += strings.Repeat("=", 4-l)
59+ }
60+
61+ return base64.URLEncoding.DecodeString(seg)
62+}
providers/google_test.gonew file+94
@@ -0,0 +1,94 @@
1+package providers
2+
3+import (
4+ "encoding/base64"
5+ "github.com/bitly/go-simplejson"
6+ "github.com/bmizerany/assert"
7+ "net/url"
8+ "testing"
9+)
10+
11+func newGoogleProvider() *GoogleProvider {
12+ return NewGoogleProvider(
13+ &ProviderData{
14+ LoginUrl: &url.URL{},
15+ RedeemUrl: &url.URL{},
16+ ProfileUrl: &url.URL{},
17+ Scope: ""})
18+}
19+
20+func TestGoogleProviderDefaults(t *testing.T) {
21+ p := newGoogleProvider()
22+ assert.NotEqual(t, nil, p)
23+ assert.Equal(t, "https://accounts.google.com/o/oauth2/auth",
24+ p.Data().LoginUrl.String())
25+ assert.Equal(t, "https://accounts.google.com/o/oauth2/token",
26+ p.Data().RedeemUrl.String())
27+ assert.Equal(t, "", p.Data().ProfileUrl.String())
28+ assert.Equal(t, "profile email", p.Data().Scope)
29+}
30+
31+func TestGoogleProviderOverrides(t *testing.T) {
32+ p := NewGoogleProvider(
33+ &ProviderData{
34+ LoginUrl: &url.URL{
35+ Scheme: "https",
36+ Host: "example.com",
37+ Path: "/oauth/auth"},
38+ RedeemUrl: &url.URL{
39+ Scheme: "https",
40+ Host: "example.com",
41+ Path: "/oauth/token"},
42+ ProfileUrl: &url.URL{
43+ Scheme: "https",
44+ Host: "example.com",
45+ Path: "/oauth/profile"},
46+ Scope: "profile"})
47+ assert.NotEqual(t, nil, p)
48+ assert.Equal(t, "https://example.com/oauth/auth",
49+ p.Data().LoginUrl.String())
50+ assert.Equal(t, "https://example.com/oauth/token",
51+ p.Data().RedeemUrl.String())
52+ assert.Equal(t, "https://example.com/oauth/profile",
53+ p.Data().ProfileUrl.String())
54+ assert.Equal(t, "profile", p.Data().Scope)
55+}
56+
57+func TestGoogleProviderGetEmailAddress(t *testing.T) {
58+ p := newGoogleProvider()
59+ j := simplejson.New()
60+ j.Set("id_token", "ignored prefix."+base64.URLEncoding.EncodeToString(
61+ []byte("{\"email\": \"michael.bland@gsa.gov\"}")))
62+ email, err := p.GetEmailAddress(j, "ignored access_token")
63+ assert.Equal(t, "michael.bland@gsa.gov", email)
64+ assert.Equal(t, nil, err)
65+}
66+
67+func TestGoogleProviderGetEmailAddressInvalidEncoding(t *testing.T) {
68+ p := newGoogleProvider()
69+ j := simplejson.New()
70+ j.Set("id_token", "ignored prefix.{\"email\": \"michael.bland@gsa.gov\"}")
71+ email, err := p.GetEmailAddress(j, "ignored access_token")
72+ assert.Equal(t, "", email)
73+ assert.NotEqual(t, nil, err)
74+}
75+
76+func TestGoogleProviderGetEmailAddressInvalidJson(t *testing.T) {
77+ p := newGoogleProvider()
78+ j := simplejson.New()
79+ j.Set("id_token", "ignored prefix."+base64.URLEncoding.EncodeToString(
80+ []byte("{email: michael.bland@gsa.gov}")))
81+ email, err := p.GetEmailAddress(j, "ignored access_token")
82+ assert.Equal(t, "", email)
83+ assert.NotEqual(t, nil, err)
84+}
85+
86+func TestGoogleProviderGetEmailAddressEmailMissing(t *testing.T) {
87+ p := newGoogleProvider()
88+ j := simplejson.New()
89+ j.Set("id_token", "ignored prefix."+base64.URLEncoding.EncodeToString(
90+ []byte("{\"not_email\": \"missing!\"}")))
91+ email, err := p.GetEmailAddress(j, "ignored access_token")
92+ assert.Equal(t, "", email)
93+ assert.NotEqual(t, nil, err)
94+}
providers/provider_data.gonew file+14
@@ -0,0 +1,14 @@
1+package providers
2+
3+import (
4+ "net/url"
5+)
6+
7+type ProviderData struct {
8+ LoginUrl *url.URL
9+ RedeemUrl *url.URL
10+ ProfileUrl *url.URL
11+ Scope string
12+}
13+
14+func (p *ProviderData) Data() *ProviderData { return p }
providers/providers.gonew file+18
@@ -0,0 +1,18 @@
1+package providers
2+
3+import (
4+ "github.com/bitly/go-simplejson"
5+)
6+
7+type Provider interface {
8+ Data() *ProviderData
9+ GetEmailAddress(auth_response *simplejson.Json,
10+ access_token string) (string, error)
11+}
12+
13+func New(provider string, p *ProviderData) Provider {
14+ switch provider {
15+ default:
16+ return NewGoogleProvider(p)
17+ }
18+}