From e2931da85317217925dbc1826271c1aed9e2e86d Mon Sep 17 00:00:00 2001 From: Mike Bland Date: Mon, 30 Mar 2015 15:30:27 -0400 Subject: [PATCH 1/2] Create providers package with Google default --- providers/google.go | 62 +++++++++++++++++++++++++ providers/google_test.go | 94 ++++++++++++++++++++++++++++++++++++++ providers/provider_data.go | 14 ++++++ providers/providers.go | 18 ++++++++ 4 files changed, 188 insertions(+) create mode 100644 providers/google.go create mode 100644 providers/google_test.go create mode 100644 providers/provider_data.go create mode 100644 providers/providers.go diff --git a/providers/google.go b/providers/google.go new file mode 100644 index 0000000..9442113 --- /dev/null +++ b/providers/google.go @@ -0,0 +1,62 @@ +package providers + +import ( + "encoding/base64" + "net/url" + "strings" + + "github.com/bitly/go-simplejson" +) + +type GoogleProvider struct { + *ProviderData +} + +func NewGoogleProvider(p *ProviderData) *GoogleProvider { + if p.LoginUrl.String() == "" { + p.LoginUrl = &url.URL{Scheme: "https", + Host: "accounts.google.com", + Path: "/o/oauth2/auth"} + } + if p.RedeemUrl.String() == "" { + p.RedeemUrl = &url.URL{Scheme: "https", + Host: "accounts.google.com", + Path: "/o/oauth2/token"} + } + if p.Scope == "" { + p.Scope = "profile email" + } + return &GoogleProvider{ProviderData: p} +} + +func (s *GoogleProvider) GetEmailAddress(auth_response *simplejson.Json, + unused_access_token string) (string, error) { + idToken, err := auth_response.Get("id_token").String() + if err != nil { + return "", err + } + // id_token is a base64 encode ID token payload + // https://developers.google.com/accounts/docs/OAuth2Login#obtainuserinfo + jwt := strings.Split(idToken, ".") + b, err := jwtDecodeSegment(jwt[1]) + if err != nil { + return "", err + } + data, err := simplejson.NewJson(b) + if err != nil { + return "", err + } + email, err := data.Get("email").String() + if err != nil { + return "", err + } + return email, nil +} + +func jwtDecodeSegment(seg string) ([]byte, error) { + if l := len(seg) % 4; l > 0 { + seg += strings.Repeat("=", 4-l) + } + + return base64.URLEncoding.DecodeString(seg) +} diff --git a/providers/google_test.go b/providers/google_test.go new file mode 100644 index 0000000..68f2ff0 --- /dev/null +++ b/providers/google_test.go @@ -0,0 +1,94 @@ +package providers + +import ( + "encoding/base64" + "github.com/bitly/go-simplejson" + "github.com/bmizerany/assert" + "net/url" + "testing" +) + +func newGoogleProvider() *GoogleProvider { + return NewGoogleProvider( + &ProviderData{ + LoginUrl: &url.URL{}, + RedeemUrl: &url.URL{}, + ProfileUrl: &url.URL{}, + Scope: ""}) +} + +func TestGoogleProviderDefaults(t *testing.T) { + p := newGoogleProvider() + assert.NotEqual(t, nil, p) + assert.Equal(t, "https://accounts.google.com/o/oauth2/auth", + p.Data().LoginUrl.String()) + assert.Equal(t, "https://accounts.google.com/o/oauth2/token", + p.Data().RedeemUrl.String()) + assert.Equal(t, "", p.Data().ProfileUrl.String()) + assert.Equal(t, "profile email", p.Data().Scope) +} + +func TestGoogleProviderOverrides(t *testing.T) { + p := NewGoogleProvider( + &ProviderData{ + LoginUrl: &url.URL{ + Scheme: "https", + Host: "example.com", + Path: "/oauth/auth"}, + RedeemUrl: &url.URL{ + Scheme: "https", + Host: "example.com", + Path: "/oauth/token"}, + ProfileUrl: &url.URL{ + Scheme: "https", + Host: "example.com", + Path: "/oauth/profile"}, + Scope: "profile"}) + assert.NotEqual(t, nil, p) + assert.Equal(t, "https://example.com/oauth/auth", + p.Data().LoginUrl.String()) + assert.Equal(t, "https://example.com/oauth/token", + p.Data().RedeemUrl.String()) + assert.Equal(t, "https://example.com/oauth/profile", + p.Data().ProfileUrl.String()) + assert.Equal(t, "profile", p.Data().Scope) +} + +func TestGoogleProviderGetEmailAddress(t *testing.T) { + p := newGoogleProvider() + j := simplejson.New() + j.Set("id_token", "ignored prefix."+base64.URLEncoding.EncodeToString( + []byte("{\"email\": \"michael.bland@gsa.gov\"}"))) + email, err := p.GetEmailAddress(j, "ignored access_token") + assert.Equal(t, "michael.bland@gsa.gov", email) + assert.Equal(t, nil, err) +} + +func TestGoogleProviderGetEmailAddressInvalidEncoding(t *testing.T) { + p := newGoogleProvider() + j := simplejson.New() + j.Set("id_token", "ignored prefix.{\"email\": \"michael.bland@gsa.gov\"}") + email, err := p.GetEmailAddress(j, "ignored access_token") + assert.Equal(t, "", email) + assert.NotEqual(t, nil, err) +} + +func TestGoogleProviderGetEmailAddressInvalidJson(t *testing.T) { + p := newGoogleProvider() + j := simplejson.New() + j.Set("id_token", "ignored prefix."+base64.URLEncoding.EncodeToString( + []byte("{email: michael.bland@gsa.gov}"))) + email, err := p.GetEmailAddress(j, "ignored access_token") + assert.Equal(t, "", email) + assert.NotEqual(t, nil, err) +} + +func TestGoogleProviderGetEmailAddressEmailMissing(t *testing.T) { + p := newGoogleProvider() + j := simplejson.New() + j.Set("id_token", "ignored prefix."+base64.URLEncoding.EncodeToString( + []byte("{\"not_email\": \"missing!\"}"))) + email, err := p.GetEmailAddress(j, "ignored access_token") + assert.Equal(t, "", email) + assert.NotEqual(t, nil, err) +} diff --git a/providers/provider_data.go b/providers/provider_data.go new file mode 100644 index 0000000..a533b6e --- /dev/null +++ b/providers/provider_data.go @@ -0,0 +1,14 @@ +package providers + +import ( + "net/url" +) + +type ProviderData struct { + LoginUrl *url.URL + RedeemUrl *url.URL + ProfileUrl *url.URL + Scope string +} + +func (p *ProviderData) Data() *ProviderData { return p } diff --git a/providers/providers.go b/providers/providers.go new file mode 100644 index 0000000..8076497 --- /dev/null +++ b/providers/providers.go @@ -0,0 +1,18 @@ +package providers + +import ( + "github.com/bitly/go-simplejson" +) + +type Provider interface { + Data() *ProviderData + GetEmailAddress(auth_response *simplejson.Json, + access_token string) (string, error) +} + +func New(provider string, p *ProviderData) Provider { + switch provider { + default: + return NewGoogleProvider(p) + } +} From d9a945ebc3a4f5f28d70b7469d212a1c37ad0047 Mon Sep 17 00:00:00 2001 From: Mike Bland Date: Mon, 30 Mar 2015 15:48:30 -0400 Subject: [PATCH 2/2] Integrate Provider into Options and OauthProxy --- main.go | 6 ++++++ oauthproxy.go | 38 +++++++------------------------------- options.go | 37 +++++++++++++++++++++++++++++++------ options_test.go | 12 ++++++++++++ 4 files changed, 56 insertions(+), 37 deletions(-) diff --git a/main.go b/main.go index f0d03f4..5250dcd 100644 --- a/main.go +++ b/main.go @@ -50,6 +50,12 @@ func main() { flagSet.Bool("request-logging", true, "Log requests to stdout") + flagSet.String("provider", "", "Oauth provider (defaults to Google)") + flagSet.String("login-url", "", "Authentication endpoint") + flagSet.String("redeem-url", "", "Token redemption endpoint") + flagSet.String("profile-url", "", "Profile access endpoint") + flagSet.String("scope", "", "Oauth scope specification") + flagSet.Parse(os.Args[1:]) if *showVersion { diff --git a/oauthproxy.go b/oauthproxy.go index e5eb541..ca49f74 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -15,8 +15,8 @@ import ( "strings" "time" - "github.com/bitly/go-simplejson" "github.com/bitly/google_auth_proxy/api" + "github.com/bitly/google_auth_proxy/providers" ) const pingPath = "/ping" @@ -34,6 +34,7 @@ type OauthProxy struct { Validator func(string) bool redirectUrl *url.URL // the url to receive requests at + provider providers.Provider oauthRedemptionUrl *url.URL // endpoint to redeem the code oauthLoginUrl *url.URL // to redirect the user to oauthScope string @@ -83,8 +84,6 @@ func setProxyDirector(proxy *httputil.ReverseProxy) { } func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy { - login, _ := url.Parse("https://accounts.google.com/o/oauth2/auth") - redeem, _ := url.Parse("https://accounts.google.com/o/oauth2/token") serveMux := http.NewServeMux() for _, u := range opts.proxyUrls { path := u.Path @@ -128,9 +127,10 @@ func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy { clientID: opts.ClientID, clientSecret: opts.ClientSecret, - oauthScope: "profile email", - oauthRedemptionUrl: redeem, - oauthLoginUrl: login, + oauthScope: opts.provider.Data().Scope, + provider: opts.provider, + oauthRedemptionUrl: opts.provider.Data().RedeemUrl, + oauthLoginUrl: opts.provider.Data().LoginUrl, serveMux: serveMux, redirectUrl: redirectUrl, skipAuthRegex: opts.SkipAuthRegex, @@ -201,23 +201,7 @@ func (p *OauthProxy) redeemCode(host, code string) (string, string, error) { return "", "", err } - idToken, err := json.Get("id_token").String() - if err != nil { - return "", "", err - } - - // id_token is a base64 encode ID token payload - // https://developers.google.com/accounts/docs/OAuth2Login#obtainuserinfo - jwt := strings.Split(idToken, ".") - b, err := jwtDecodeSegment(jwt[1]) - if err != nil { - return "", "", err - } - data, err := simplejson.NewJson(b) - if err != nil { - return "", "", err - } - email, err := data.Get("email").String() + email, err := p.provider.GetEmailAddress(json, access_token) if err != nil { return "", "", err } @@ -225,14 +209,6 @@ func (p *OauthProxy) redeemCode(host, code string) (string, string, error) { return access_token, email, nil } -func jwtDecodeSegment(seg string) ([]byte, error) { - if l := len(seg) % 4; l > 0 { - seg += strings.Repeat("=", 4-l) - } - - return base64.URLEncoding.DecodeString(seg) -} - func (p *OauthProxy) ClearCookie(rw http.ResponseWriter, req *http.Request) { domain := req.Host if h, _, err := net.SplitHostPort(domain); err == nil { diff --git a/options.go b/options.go index eb099ac..85b4c01 100644 --- a/options.go +++ b/options.go @@ -6,6 +6,8 @@ import ( "regexp" "strings" "time" + + "github.com/bitly/google_auth_proxy/providers" ) // Configuration Options that can be set by Command Line Flag, or Config File @@ -33,12 +35,21 @@ type Options struct { PassBasicAuth bool `flag:"pass-basic-auth" cfg:"pass_basic_auth"` PassHostHeader bool `flag:"pass-host-header" cfg:"pass_host_header"` + // These options allow for other providers besides Google, with + // potential overrides. + Provider string `flag:"provider" cfg:"provider"` + LoginUrl string `flag:"login-url" cfg:"login_url"` + RedeemUrl string `flag:"redeem-url" cfg:"redeem_url"` + ProfileUrl string `flag:"profile-url" cfg:"profile_url"` + Scope string `flag:"scope" cfg:"scope"` + RequestLogging bool `flag:"request-logging" cfg:"request_logging"` // internal values that are set after config validation redirectUrl *url.URL proxyUrls []*url.URL CompiledRegex []*regexp.Regexp + provider providers.Provider } func NewOptions() *Options { @@ -55,6 +66,15 @@ func NewOptions() *Options { } } +func parseUrl(to_parse string, urltype string, msgs []string) (*url.URL, []string) { + parsed, err := url.Parse(to_parse) + if err != nil { + return nil, append(msgs, fmt.Sprintf( + "error parsing %s-url=%q %s", urltype, to_parse, err)) + } + return parsed, msgs +} + func (o *Options) Validate() error { msgs := make([]string, 0) if len(o.Upstreams) < 1 { @@ -70,12 +90,7 @@ func (o *Options) Validate() error { msgs = append(msgs, "missing setting: client-secret") } - redirectUrl, err := url.Parse(o.RedirectUrl) - if err != nil { - msgs = append(msgs, fmt.Sprintf( - "error parsing redirect-url=%q %s", o.RedirectUrl, err)) - } - o.redirectUrl = redirectUrl + o.redirectUrl, msgs = parseUrl(o.RedirectUrl, "redirect", msgs) for _, u := range o.Upstreams { upstreamUrl, err := url.Parse(u) @@ -98,6 +113,7 @@ func (o *Options) Validate() error { } o.CompiledRegex = append(o.CompiledRegex, CompiledRegex) } + msgs = parseProviderInfo(o, msgs) if len(msgs) != 0 { return fmt.Errorf("Invalid configuration:\n %s", @@ -105,3 +121,12 @@ func (o *Options) Validate() error { } return nil } + +func parseProviderInfo(o *Options, msgs []string) []string { + p := &providers.ProviderData{Scope: o.Scope} + p.LoginUrl, msgs = parseUrl(o.LoginUrl, "login", msgs) + p.RedeemUrl, msgs = parseUrl(o.RedeemUrl, "redeem", msgs) + p.ProfileUrl, msgs = parseUrl(o.ProfileUrl, "profile", msgs) + o.provider = providers.New(o.Provider, p) + return msgs +} diff --git a/options_test.go b/options_test.go index 47c711a..515c1c8 100644 --- a/options_test.go +++ b/options_test.go @@ -90,3 +90,15 @@ func TestCompiledRegexError(t *testing.T) { "unexpected ): `barquux)`"}) assert.Equal(t, expected, err.Error()) } + +func TestDefaultProviderApiSettings(t *testing.T) { + o := testOptions() + assert.Equal(t, nil, o.Validate()) + p := o.provider.Data() + assert.Equal(t, "https://accounts.google.com/o/oauth2/auth", + p.LoginUrl.String()) + assert.Equal(t, "https://accounts.google.com/o/oauth2/token", + p.RedeemUrl.String()) + assert.Equal(t, "", p.ProfileUrl.String()) + assert.Equal(t, "profile email", p.Scope) +}