From 7dd5d299e18162e9ec7663699133d36ec2a7844c Mon Sep 17 00:00:00 2001 From: Justin Burnham Date: Fri, 24 Jul 2015 09:17:43 +0000 Subject: [PATCH] Add support for setting the basic auth password. For tools that don't like empty passwords, this change allows one to set a shared secret password for all users. --- README.md | 1 + main.go | 1 + oauthproxy.go | 24 ++++++------ oauthproxy_test.go | 96 ++++++++++++++++++++++++++++++++++++++++++++++ options.go | 11 +++--- 5 files changed, 117 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 64e7948..bb42c90 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,7 @@ Usage of oauth2_proxy: -login-url="": Authentication endpoint -pass-access-token=false: pass OAuth access_token to upstream via X-Forwarded-Access-Token header -pass-basic-auth=true: pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream + -basic-auth-password="": the password to set when passing the HTTP Basic Auth header -pass-host-header=true: pass the request Host Header to upstream -profile-url="": Profile access endpoint -provider="google": OAuth provider diff --git a/main.go b/main.go index acf18fe..e752409 100644 --- a/main.go +++ b/main.go @@ -31,6 +31,7 @@ func main() { flagSet.String("redirect-url", "", "the OAuth Redirect URL. ie: \"https://internalapp.yourcompany.com/oauth2/callback\"") flagSet.Var(&upstreams, "upstream", "the http url(s) of the upstream endpoint. If multiple, routing is based on path") flagSet.Bool("pass-basic-auth", true, "pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream") + flagSet.String("basic-auth-password", "", "the password to set when passing the HTTP Basic Auth header") flagSet.Bool("pass-access-token", false, "pass OAuth access_token to upstream via X-Forwarded-Access-Token header") flagSet.Bool("pass-host-header", true, "pass the request Host Header to upstream") flagSet.Var(&skipAuthRegex, "skip-auth-regex", "bypass authentication for requests path's that match (may be given multiple times)") diff --git a/oauthproxy.go b/oauthproxy.go index 2c65131..07c3ec9 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -42,6 +42,7 @@ type OauthProxy struct { DisplayHtpasswdForm bool serveMux http.Handler PassBasicAuth bool + BasicAuthPassword string PassAccessToken bool CookieCipher *cookie.Cipher skipAuthRegex []string @@ -141,16 +142,17 @@ func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy { OauthStartPath: fmt.Sprintf("%s/start", opts.ProxyPrefix), OauthCallbackPath: fmt.Sprintf("%s/callback", opts.ProxyPrefix), - ProxyPrefix: opts.ProxyPrefix, - provider: opts.provider, - serveMux: serveMux, - redirectUrl: redirectUrl, - skipAuthRegex: opts.SkipAuthRegex, - compiledRegex: opts.CompiledRegex, - PassBasicAuth: opts.PassBasicAuth, - PassAccessToken: opts.PassAccessToken, - CookieCipher: cipher, - templates: loadTemplates(opts.CustomTemplatesDir), + ProxyPrefix: opts.ProxyPrefix, + provider: opts.provider, + serveMux: serveMux, + redirectUrl: redirectUrl, + skipAuthRegex: opts.SkipAuthRegex, + compiledRegex: opts.CompiledRegex, + PassBasicAuth: opts.PassBasicAuth, + BasicAuthPassword: opts.BasicAuthPassword, + PassAccessToken: opts.PassAccessToken, + CookieCipher: cipher, + templates: loadTemplates(opts.CustomTemplatesDir), } } @@ -518,7 +520,7 @@ func (p *OauthProxy) Proxy(rw http.ResponseWriter, req *http.Request) { // At this point, the user is authenticated. proxy normally if p.PassBasicAuth { - req.SetBasicAuth(session.User, "") + req.SetBasicAuth(session.User, p.BasicAuthPassword) req.Header["X-Forwarded-User"] = []string{session.User} if session.Email != "" { req.Header["X-Forwarded-Email"] = []string{session.Email} diff --git a/oauthproxy_test.go b/oauthproxy_test.go index 5b5a935..52b48bb 100644 --- a/oauthproxy_test.go +++ b/oauthproxy_test.go @@ -1,6 +1,7 @@ package main import ( + "encoding/base64" "github.com/bitly/oauth2_proxy/providers" "github.com/bmizerany/assert" "io/ioutil" @@ -88,6 +89,101 @@ func TestRobotsTxt(t *testing.T) { assert.Equal(t, "User-agent: *\nDisallow: /", rw.Body.String()) } +func TestBasicAuthPassword(t *testing.T) { + provider_server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log.Printf("%#v", r) + url := r.URL + payload := "" + switch url.Path { + case "/oauth/token": + payload = `{"access_token": "my_auth_token"}` + default: + payload = r.Header.Get("Authorization") + if payload == "" { + payload = "No Authorization header found." + } + } + w.WriteHeader(200) + w.Write([]byte(payload)) + })) + opts := NewOptions() + opts.Upstreams = append(opts.Upstreams, provider_server.URL) + // The CookieSecret must be 32 bytes in order to create the AES + // cipher. + opts.CookieSecret = "xyzzyplughxyzzyplughxyzzyplughxp" + opts.ClientID = "bazquux" + opts.ClientSecret = "foobar" + opts.CookieSecure = false + opts.PassBasicAuth = true + opts.BasicAuthPassword = "This is a secure password" + opts.Validate() + + provider_url, _ := url.Parse(provider_server.URL) + const email_address = "michael.bland@gsa.gov" + const user_name = "michael.bland" + + opts.provider = &TestProvider{ + ProviderData: &providers.ProviderData{ + ProviderName: "Test Provider", + LoginUrl: &url.URL{ + Scheme: "http", + Host: provider_url.Host, + Path: "/oauth/authorize", + }, + RedeemUrl: &url.URL{ + Scheme: "http", + Host: provider_url.Host, + Path: "/oauth/token", + }, + ProfileUrl: &url.URL{ + Scheme: "http", + Host: provider_url.Host, + Path: "/api/v1/profile", + }, + Scope: "profile.email", + }, + EmailAddress: email_address, + } + + proxy := NewOauthProxy(opts, func(email string) bool { + return email == email_address + }) + + rw := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/oauth2/callback?code=callback_code", + strings.NewReader("")) + proxy.ServeHTTP(rw, req) + cookie := rw.HeaderMap["Set-Cookie"][0] + + cookieName := proxy.CookieName + var value string + key_prefix := cookieName + "=" + + for _, field := range strings.Split(cookie, "; ") { + value = strings.TrimPrefix(field, key_prefix) + if value != field { + break + } else { + value = "" + } + } + + req, _ = http.NewRequest("GET", "/", strings.NewReader("")) + req.AddCookie(&http.Cookie{ + Name: cookieName, + Value: value, + Path: "/", + Expires: time.Now().Add(time.Duration(24)), + HttpOnly: true, + }) + + rw = httptest.NewRecorder() + proxy.ServeHTTP(rw, req) + expectedHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte(user_name+":"+opts.BasicAuthPassword)) + assert.Equal(t, expectedHeader, rw.Body.String()) + provider_server.Close() +} + type TestProvider struct { *providers.ProviderData EmailAddress string diff --git a/options.go b/options.go index 7bf488f..bcf7d29 100644 --- a/options.go +++ b/options.go @@ -37,11 +37,12 @@ type Options struct { CookieSecure bool `flag:"cookie-secure" cfg:"cookie_secure"` CookieHttpOnly bool `flag:"cookie-httponly" cfg:"cookie_httponly"` - Upstreams []string `flag:"upstream" cfg:"upstreams"` - SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex"` - PassBasicAuth bool `flag:"pass-basic-auth" cfg:"pass_basic_auth"` - PassAccessToken bool `flag:"pass-access-token" cfg:"pass_access_token"` - PassHostHeader bool `flag:"pass-host-header" cfg:"pass_host_header"` + Upstreams []string `flag:"upstream" cfg:"upstreams"` + SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex"` + PassBasicAuth bool `flag:"pass-basic-auth" cfg:"pass_basic_auth"` + BasicAuthPassword string `flag:"basic-auth-password" cfg:"basic_auth_password"` + PassAccessToken bool `flag:"pass-access-token" cfg:"pass_access_token"` + PassHostHeader bool `flag:"pass-host-header" cfg:"pass_host_header"` // These options allow for other providers besides Google, with // potential overrides.