From 1bd90cefe7daa06d9bc542a8a4f924b5a798c3df Mon Sep 17 00:00:00 2001 From: Mike Bland Date: Fri, 8 May 2015 10:09:47 -0400 Subject: [PATCH 01/16] Extract ProcessCookie() from ServeHTTP() --- oauthproxy.go | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/oauthproxy.go b/oauthproxy.go index 33f4698..87d85da 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -270,6 +270,22 @@ func (p *OauthProxy) SetCookie(rw http.ResponseWriter, req *http.Request, val st http.SetCookie(rw, cookie) } +func (p *OauthProxy) ProcessCookie(rw http.ResponseWriter, req *http.Request) (email, user, access_token string, ok bool) { + cookie, err := req.Cookie(p.CookieKey) + if err == nil { + var value string + value, ok = validateCookie(cookie, p.CookieSeed) + if ok { + email, user, access_token, err = parseCookieValue( + value, p.AesCipher) + if err != nil { + log.Printf(err.Error()) + } + } + } + return +} + func (p *OauthProxy) PingPage(rw http.ResponseWriter) { rw.WriteHeader(http.StatusOK) fmt.Fprintf(rw, "OK") @@ -440,18 +456,7 @@ func (p *OauthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { } if !ok { - cookie, err := req.Cookie(p.CookieKey) - if err == nil { - var value string - value, ok = validateCookie(cookie, p.CookieSeed) - if ok { - email, user, access_token, err = parseCookieValue( - value, p.AesCipher) - if err != nil { - log.Printf(err.Error()) - } - } - } + email, user, access_token, ok = p.ProcessCookie(rw, req) } if !ok { From beed9fb9a23fe5f9752dcd360f54e874283d9ff5 Mon Sep 17 00:00:00 2001 From: Mike Bland Date: Fri, 8 May 2015 11:51:11 -0400 Subject: [PATCH 02/16] Extract MakeCookie() --- oauthproxy.go | 40 ++++++++++++++-------------------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/oauthproxy.go b/oauthproxy.go index 87d85da..af4d2d7 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -223,7 +223,7 @@ func (p *OauthProxy) redeemCode(host, code string) (string, string, error) { return access_token, email, nil } -func (p *OauthProxy) ClearCookie(rw http.ResponseWriter, req *http.Request) { +func (p *OauthProxy) MakeCookie(req *http.Request, value string, expiration time.Duration) *http.Cookie { domain := req.Host if h, _, err := net.SplitHostPort(domain); err == nil { domain = h @@ -234,40 +234,28 @@ func (p *OauthProxy) ClearCookie(rw http.ResponseWriter, req *http.Request) { } domain = p.CookieDomain } - cookie := &http.Cookie{ + + if value != "" { + value = signedCookieValue(p.CookieSeed, p.CookieKey, value) + } + + return &http.Cookie{ Name: p.CookieKey, - Value: "", + Value: value, Path: "/", Domain: domain, HttpOnly: p.CookieHttpOnly, Secure: p.CookieSecure, - Expires: time.Now().Add(time.Duration(1) * time.Hour * -1), + Expires: time.Now().Add(expiration), } - http.SetCookie(rw, cookie) +} + +func (p *OauthProxy) ClearCookie(rw http.ResponseWriter, req *http.Request) { + http.SetCookie(rw, p.MakeCookie(req, "", time.Duration(1)*time.Hour*-1)) } func (p *OauthProxy) SetCookie(rw http.ResponseWriter, req *http.Request, val string) { - - domain := req.Host - if h, _, err := net.SplitHostPort(domain); err == nil { - domain = h - } - if p.CookieDomain != "" { - if !strings.HasSuffix(domain, p.CookieDomain) { - log.Printf("Warning: request host is %q but using configured cookie domain of %q", domain, p.CookieDomain) - } - domain = p.CookieDomain - } - cookie := &http.Cookie{ - Name: p.CookieKey, - Value: signedCookieValue(p.CookieSeed, p.CookieKey, val), - Path: "/", - Domain: domain, - HttpOnly: p.CookieHttpOnly, - Secure: p.CookieSecure, - Expires: time.Now().Add(p.CookieExpire), - } - http.SetCookie(rw, cookie) + http.SetCookie(rw, p.MakeCookie(req, val, p.CookieExpire)) } func (p *OauthProxy) ProcessCookie(rw http.ResponseWriter, req *http.Request) (email, user, access_token string, ok bool) { From f554f99abd3da239545b6785625089ac29f45a25 Mon Sep 17 00:00:00 2001 From: Mike Bland Date: Fri, 8 May 2015 11:51:43 -0400 Subject: [PATCH 03/16] Ensure all errors are logged in ProcessCookie() --- oauthproxy.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/oauthproxy.go b/oauthproxy.go index af4d2d7..f28928e 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -266,11 +266,11 @@ func (p *OauthProxy) ProcessCookie(rw http.ResponseWriter, req *http.Request) (e if ok { email, user, access_token, err = parseCookieValue( value, p.AesCipher) - if err != nil { - log.Printf(err.Error()) - } } } + if err != nil { + log.Printf(err.Error()) + } return } From 5cbdb745183a36ce05be9e3b19050edee27084f4 Mon Sep 17 00:00:00 2001 From: Mike Bland Date: Fri, 8 May 2015 11:52:03 -0400 Subject: [PATCH 04/16] Add ProcessCookie() test --- oauthproxy_test.go | 53 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/oauthproxy_test.go b/oauthproxy_test.go index d3fe400..0e436c7 100644 --- a/oauthproxy_test.go +++ b/oauthproxy_test.go @@ -305,3 +305,56 @@ func TestSignInPageDirectAccessRedirectsToRoot(t *testing.T) { t.Fatal(`expected redirect to "/", but was "` + match[1] + `"`) } } + +type ProcessCookieTest struct { + opts *Options + proxy *OauthProxy + rw *httptest.ResponseRecorder + req *http.Request +} + +func NewProcessCookieTest() *ProcessCookieTest { + var pc_test ProcessCookieTest + + pc_test.opts = NewOptions() + pc_test.opts.Upstreams = append(pc_test.opts.Upstreams, "unused") + pc_test.opts.CookieSecret = "foobar" + pc_test.opts.ClientID = "bazquux" + pc_test.opts.ClientSecret = "xyzzyplugh" + pc_test.opts.Validate() + + pc_test.proxy = NewOauthProxy(pc_test.opts, func(email string) bool { + return true + }) + + pc_test.rw = httptest.NewRecorder() + pc_test.req, _ = http.NewRequest("GET", "/", strings.NewReader("")) + return &pc_test +} + +func (p *ProcessCookieTest) MakeCookie(value string) *http.Cookie { + return p.proxy.MakeCookie(p.req, value, p.opts.CookieExpire) +} + +func (p *ProcessCookieTest) AddCookie(value string) { + p.req.AddCookie(p.MakeCookie(value)) +} + +func (p *ProcessCookieTest) ProcessCookie() (email, user, access_token string, ok bool) { + return p.proxy.ProcessCookie(p.rw, p.req) +} + +func TestProcessCookie(t *testing.T) { + pc_test := NewProcessCookieTest() + pc_test.AddCookie("michael.bland@gsa.gov") + email, user, _, ok := pc_test.ProcessCookie() + assert.Equal(t, true, ok) + assert.Equal(t, "michael.bland@gsa.gov", email) + assert.Equal(t, "michael.bland", user) +} + +func TestProcessCookieError(t *testing.T) { + pc_test := NewProcessCookieTest() + _, _, _, ok := pc_test.ProcessCookie() + assert.Equal(t, false, ok) +} From 8e2d83600cfa6682b8969fda0c5e526d8c26d6d8 Mon Sep 17 00:00:00 2001 From: Mike Bland Date: Fri, 8 May 2015 10:00:57 -0400 Subject: [PATCH 05/16] Implement cookie auto-refresh The intention is to refresh the cookie whenever the user accesses an authenticated service with less than `cookie-refresh` time to go before the cookie expires. --- cookies.go | 8 ++++---- main.go | 1 + oauthproxy.go | 12 ++++++++++-- oauthproxy_test.go | 25 ++++++++++++++++++++++++- options.go | 2 ++ 5 files changed, 41 insertions(+), 7 deletions(-) diff --git a/cookies.go b/cookies.go index 9605398..8d7ce63 100644 --- a/cookies.go +++ b/cookies.go @@ -15,11 +15,11 @@ import ( "time" ) -func validateCookie(cookie *http.Cookie, seed string) (string, bool) { +func validateCookie(cookie *http.Cookie, seed string) (string, time.Time, bool) { // value, timestamp, sig parts := strings.Split(cookie.Value, "|") if len(parts) != 3 { - return "", false + return "", time.Unix(0, 0), false } sig := cookieSignature(seed, cookie.Name, parts[0], parts[1]) if checkHmac(parts[2], sig) { @@ -28,11 +28,11 @@ func validateCookie(cookie *http.Cookie, seed string) (string, bool) { // it's a valid cookie. now get the contents rawValue, err := base64.URLEncoding.DecodeString(parts[0]) if err == nil { - return string(rawValue), true + return string(rawValue), time.Unix(int64(ts), 0), true } } } - return "", false + return "", time.Unix(0, 0), false } func signedCookieValue(seed string, key string, value string) string { diff --git a/main.go b/main.go index d6c99da..1b86c1e 100644 --- a/main.go +++ b/main.go @@ -45,6 +45,7 @@ func main() { flagSet.String("cookie-secret", "", "the seed string for secure cookies") flagSet.String("cookie-domain", "", "an optional cookie domain to force cookies to (ie: .yourcompany.com)*") flagSet.Duration("cookie-expire", time.Duration(168)*time.Hour, "expire timeframe for cookie") + flagSet.Duration("cookie-refresh", time.Duration(24)*time.Hour, "refresh the cookie when this much time remains before expiration") flagSet.Bool("cookie-https-only", true, "set secure (HTTPS) cookies (deprecated. use --cookie-secure setting)") flagSet.Bool("cookie-secure", true, "set secure (HTTPS) cookie flag") flagSet.Bool("cookie-httponly", true, "set HttpOnly cookie flag") diff --git a/oauthproxy.go b/oauthproxy.go index f28928e..c236cd3 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -33,6 +33,7 @@ type OauthProxy struct { CookieSecure bool CookieHttpOnly bool CookieExpire time.Duration + CookieRefresh time.Duration Validator func(string) bool redirectUrl *url.URL // the url to receive requests at @@ -136,6 +137,7 @@ func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy { CookieSecure: opts.CookieSecure, CookieHttpOnly: opts.CookieHttpOnly, CookieExpire: opts.CookieExpire, + CookieRefresh: opts.CookieRefresh, Validator: validator, clientID: opts.ClientID, @@ -259,10 +261,11 @@ func (p *OauthProxy) SetCookie(rw http.ResponseWriter, req *http.Request, val st } func (p *OauthProxy) ProcessCookie(rw http.ResponseWriter, req *http.Request) (email, user, access_token string, ok bool) { + var value string + var timestamp time.Time cookie, err := req.Cookie(p.CookieKey) if err == nil { - var value string - value, ok = validateCookie(cookie, p.CookieSeed) + value, timestamp, ok = validateCookie(cookie, p.CookieSeed) if ok { email, user, access_token, err = parseCookieValue( value, p.AesCipher) @@ -270,6 +273,11 @@ func (p *OauthProxy) ProcessCookie(rw http.ResponseWriter, req *http.Request) (e } if err != nil { log.Printf(err.Error()) + } else if p.CookieRefresh != time.Duration(0) { + refresh_threshold := time.Now().Add(p.CookieRefresh) + if refresh_threshold.Unix() > timestamp.Unix() { + p.SetCookie(rw, req, value) + } } return } diff --git a/oauthproxy_test.go b/oauthproxy_test.go index 0e436c7..c38c31e 100644 --- a/oauthproxy_test.go +++ b/oauthproxy_test.go @@ -353,8 +353,31 @@ func TestProcessCookie(t *testing.T) { assert.Equal(t, "michael.bland", user) } -func TestProcessCookieError(t *testing.T) { +func TestProcessCookieNoCookieError(t *testing.T) { pc_test := NewProcessCookieTest() _, _, _, ok := pc_test.ProcessCookie() assert.Equal(t, false, ok) } + +func TestProcessCookieRefreshNotSet(t *testing.T) { + pc_test := NewProcessCookieTest() + cookie := pc_test.MakeCookie("michael.bland@gsa.gov") + cookie.Expires = time.Now().Add(time.Duration(23) * time.Hour) + pc_test.req.AddCookie(cookie) + + _, _, _, ok := pc_test.ProcessCookie() + assert.Equal(t, true, ok) + assert.Equal(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"]) +} + +func TestProcessCookieRefresh(t *testing.T) { + pc_test := NewProcessCookieTest() + cookie := pc_test.MakeCookie("michael.bland@gsa.gov") + cookie.Expires = time.Now().Add(time.Duration(23) * time.Hour) + pc_test.req.AddCookie(cookie) + + pc_test.proxy.CookieRefresh = time.Duration(24) * time.Hour + _, _, _, ok := pc_test.ProcessCookie() + assert.Equal(t, true, ok) + assert.NotEqual(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"]) +} diff --git a/options.go b/options.go index bbfd466..f9147d9 100644 --- a/options.go +++ b/options.go @@ -26,6 +26,7 @@ type Options struct { CookieSecret string `flag:"cookie-secret" cfg:"cookie_secret" env:"GOOGLE_AUTH_PROXY_COOKIE_SECRET"` CookieDomain string `flag:"cookie-domain" cfg:"cookie_domain" env:"GOOGLE_AUTH_PROXY_COOKIE_DOMAIN"` CookieExpire time.Duration `flag:"cookie-expire" cfg:"cookie_expire" env:"GOOGLE_AUTH_PROXY_COOKIE_EXPIRE"` + CookieRefresh time.Duration `flag:"cookie-refresh" cfg:"cookie_refresh" env:"GOOGLE_AUTH_PROXY_COOKIE_REFRESH"` CookieHttpsOnly bool `flag:"cookie-https-only" cfg:"cookie_https_only"` // deprecated use cookie-secure CookieSecure bool `flag:"cookie-secure" cfg:"cookie_secure"` CookieHttpOnly bool `flag:"cookie-httponly" cfg:"cookie_httponly"` @@ -61,6 +62,7 @@ func NewOptions() *Options { CookieSecure: true, CookieHttpOnly: true, CookieExpire: time.Duration(168) * time.Hour, + CookieRefresh: time.Duration(0), PassBasicAuth: true, PassAccessToken: false, PassHostHeader: true, From 72857018eec737548e581a918acfebd94e4b44d9 Mon Sep 17 00:00:00 2001 From: Mike Bland Date: Fri, 8 May 2015 17:13:35 -0400 Subject: [PATCH 06/16] Introduce `validate-url` flag/config --- main.go | 1 + oauthproxy.go | 2 ++ options.go | 12 +++++++----- providers/google.go | 5 +++++ providers/google_test.go | 9 +++++++++ providers/myusa.go | 5 +++++ providers/myusa_test.go | 10 ++++++++++ providers/provider_data.go | 1 + 8 files changed, 40 insertions(+), 5 deletions(-) diff --git a/main.go b/main.go index 1b86c1e..e46a336 100644 --- a/main.go +++ b/main.go @@ -56,6 +56,7 @@ func main() { flagSet.String("login-url", "", "Authentication endpoint") flagSet.String("redeem-url", "", "Token redemption endpoint") flagSet.String("profile-url", "", "Profile access endpoint") + flagSet.String("validate-url", "", "Access token validation endpoint") flagSet.String("scope", "", "Oauth scope specification") flagSet.Parse(os.Args[1:]) diff --git a/oauthproxy.go b/oauthproxy.go index c236cd3..04ab41d 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -40,6 +40,7 @@ type OauthProxy struct { provider providers.Provider oauthRedemptionUrl *url.URL // endpoint to redeem the code oauthLoginUrl *url.URL // to redirect the user to + oauthValidateUrl *url.URL // to validate the access token oauthScope string clientID string clientSecret string @@ -146,6 +147,7 @@ func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy { provider: opts.provider, oauthRedemptionUrl: opts.provider.Data().RedeemUrl, oauthLoginUrl: opts.provider.Data().LoginUrl, + oauthValidateUrl: opts.provider.Data().ValidateUrl, serveMux: serveMux, redirectUrl: redirectUrl, skipAuthRegex: opts.SkipAuthRegex, diff --git a/options.go b/options.go index f9147d9..e6aafac 100644 --- a/options.go +++ b/options.go @@ -39,11 +39,12 @@ type Options struct { // 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"` + 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"` + ValidateUrl string `flag:"validate-url" cfg:"validate_url"` + Scope string `flag:"scope" cfg:"scope"` RequestLogging bool `flag:"request-logging" cfg:"request_logging"` @@ -148,6 +149,7 @@ func parseProviderInfo(o *Options, msgs []string) []string { p.LoginUrl, msgs = parseUrl(o.LoginUrl, "login", msgs) p.RedeemUrl, msgs = parseUrl(o.RedeemUrl, "redeem", msgs) p.ProfileUrl, msgs = parseUrl(o.ProfileUrl, "profile", msgs) + p.ValidateUrl, msgs = parseUrl(o.ValidateUrl, "validate", msgs) o.provider = providers.New(o.Provider, p) return msgs } diff --git a/providers/google.go b/providers/google.go index c9955a9..5fc94be 100644 --- a/providers/google.go +++ b/providers/google.go @@ -24,6 +24,11 @@ func NewGoogleProvider(p *ProviderData) *GoogleProvider { Host: "accounts.google.com", Path: "/o/oauth2/token"} } + if p.ValidateUrl.String() == "" { + p.ValidateUrl = &url.URL{Scheme: "https", + Host: "www.googleapis.com", + Path: "/oauth2/v1/tokeninfo"} + } if p.Scope == "" { p.Scope = "profile email" } diff --git a/providers/google_test.go b/providers/google_test.go index 9ff4d00..532199c 100644 --- a/providers/google_test.go +++ b/providers/google_test.go @@ -15,6 +15,7 @@ func newGoogleProvider() *GoogleProvider { LoginUrl: &url.URL{}, RedeemUrl: &url.URL{}, ProfileUrl: &url.URL{}, + ValidateUrl: &url.URL{}, Scope: ""}) } @@ -26,6 +27,8 @@ func TestGoogleProviderDefaults(t *testing.T) { p.Data().LoginUrl.String()) assert.Equal(t, "https://accounts.google.com/o/oauth2/token", p.Data().RedeemUrl.String()) + assert.Equal(t, "https://www.googleapis.com/oauth2/v1/tokeninfo", + p.Data().ValidateUrl.String()) assert.Equal(t, "", p.Data().ProfileUrl.String()) assert.Equal(t, "profile email", p.Data().Scope) } @@ -45,6 +48,10 @@ func TestGoogleProviderOverrides(t *testing.T) { Scheme: "https", Host: "example.com", Path: "/oauth/profile"}, + ValidateUrl: &url.URL{ + Scheme: "https", + Host: "example.com", + Path: "/oauth/tokeninfo"}, Scope: "profile"}) assert.NotEqual(t, nil, p) assert.Equal(t, "Google", p.Data().ProviderName) @@ -54,6 +61,8 @@ func TestGoogleProviderOverrides(t *testing.T) { p.Data().RedeemUrl.String()) assert.Equal(t, "https://example.com/oauth/profile", p.Data().ProfileUrl.String()) + assert.Equal(t, "https://example.com/oauth/tokeninfo", + p.Data().ValidateUrl.String()) assert.Equal(t, "profile", p.Data().Scope) } diff --git a/providers/myusa.go b/providers/myusa.go index 2c9119a..69014ba 100644 --- a/providers/myusa.go +++ b/providers/myusa.go @@ -32,6 +32,11 @@ func NewMyUsaProvider(p *ProviderData) *MyUsaProvider { Host: myUsaHost, Path: "/api/v1/profile"} } + if p.ValidateUrl.String() == "" { + p.ValidateUrl = &url.URL{Scheme: "https", + Host: myUsaHost, + Path: "/api/v1/tokeninfo"} + } if p.Scope == "" { p.Scope = "profile.email" } diff --git a/providers/myusa_test.go b/providers/myusa_test.go index 74bb1a9..20df092 100644 --- a/providers/myusa_test.go +++ b/providers/myusa_test.go @@ -21,11 +21,13 @@ func testMyUsaProvider(hostname string) *MyUsaProvider { LoginUrl: &url.URL{}, RedeemUrl: &url.URL{}, ProfileUrl: &url.URL{}, + ValidateUrl: &url.URL{}, Scope: ""}) if hostname != "" { updateUrl(p.Data().LoginUrl, hostname) updateUrl(p.Data().RedeemUrl, hostname) updateUrl(p.Data().ProfileUrl, hostname) + updateUrl(p.Data().ValidateUrl, hostname) } return p } @@ -56,6 +58,8 @@ func TestMyUsaProviderDefaults(t *testing.T) { p.Data().RedeemUrl.String()) assert.Equal(t, "https://alpha.my.usa.gov/api/v1/profile", p.Data().ProfileUrl.String()) + assert.Equal(t, "https://alpha.my.usa.gov/api/v1/tokeninfo", + p.Data().ValidateUrl.String()) assert.Equal(t, "profile.email", p.Data().Scope) } @@ -74,6 +78,10 @@ func TestMyUsaProviderOverrides(t *testing.T) { Scheme: "https", Host: "example.com", Path: "/oauth/profile"}, + ValidateUrl: &url.URL{ + Scheme: "https", + Host: "example.com", + Path: "/oauth/tokeninfo"}, Scope: "profile"}) assert.NotEqual(t, nil, p) assert.Equal(t, "MyUSA", p.Data().ProviderName) @@ -83,6 +91,8 @@ func TestMyUsaProviderOverrides(t *testing.T) { p.Data().RedeemUrl.String()) assert.Equal(t, "https://example.com/oauth/profile", p.Data().ProfileUrl.String()) + assert.Equal(t, "https://example.com/oauth/tokeninfo", + p.Data().ValidateUrl.String()) assert.Equal(t, "profile", p.Data().Scope) } diff --git a/providers/provider_data.go b/providers/provider_data.go index 7b665cf..097f065 100644 --- a/providers/provider_data.go +++ b/providers/provider_data.go @@ -9,6 +9,7 @@ type ProviderData struct { LoginUrl *url.URL RedeemUrl *url.URL ProfileUrl *url.URL + ValidateUrl *url.URL Scope string } From 25372567ac51e70aa052e0e17a1ebf2242110971 Mon Sep 17 00:00:00 2001 From: Mike Bland Date: Sat, 9 May 2015 13:01:09 -0400 Subject: [PATCH 07/16] ValidateToken() to check access_token validity --- oauthproxy.go | 21 ++++++++++++ oauthproxy_test.go | 84 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/oauthproxy.go b/oauthproxy.go index 04ab41d..4943bdf 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -262,6 +262,27 @@ func (p *OauthProxy) SetCookie(rw http.ResponseWriter, req *http.Request, val st http.SetCookie(rw, p.MakeCookie(req, val, p.CookieExpire)) } +func (p *OauthProxy) ValidateToken(access_token string) bool { + if access_token == "" || p.oauthValidateUrl == nil { + return false + } + + req, err := http.NewRequest("GET", + p.oauthValidateUrl.String()+"?access_token="+access_token, nil) + if err != nil { + log.Printf("failed building token validation request: %s", err) + return false + } + + httpclient := &http.Client{} + resp, err := httpclient.Do(req) + if err != nil { + log.Printf("token validation request failed: %s", err) + return false + } + return resp.StatusCode == 200 +} + func (p *OauthProxy) ProcessCookie(rw http.ResponseWriter, req *http.Request) (email, user, access_token string, ok bool) { var value string var timestamp time.Time diff --git a/oauthproxy_test.go b/oauthproxy_test.go index c38c31e..e0d6f6b 100644 --- a/oauthproxy_test.go +++ b/oauthproxy_test.go @@ -306,6 +306,90 @@ func TestSignInPageDirectAccessRedirectsToRoot(t *testing.T) { } } +type ValidateTokenTest struct { + opts *Options + proxy *OauthProxy + backend *httptest.Server + response_code int +} + +func NewValidateTokenTest() *ValidateTokenTest { + var vt_test ValidateTokenTest + + vt_test.opts = NewOptions() + vt_test.opts.Upstreams = append(vt_test.opts.Upstreams, "unused") + vt_test.opts.CookieSecret = "foobar" + vt_test.opts.ClientID = "bazquux" + vt_test.opts.ClientSecret = "xyzzyplugh" + vt_test.opts.Validate() + + vt_test.backend = httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/oauth/tokeninfo": + w.WriteHeader(vt_test.response_code) + w.Write([]byte("only code matters; contents disregarded")) + default: + w.WriteHeader(500) + w.Write([]byte("unknown URL")) + } + })) + backend_url, _ := url.Parse(vt_test.backend.URL) + vt_test.opts.provider.Data().ValidateUrl = &url.URL{ + Scheme: "http", + Host: backend_url.Host, + Path: "/oauth/tokeninfo", + } + vt_test.response_code = 200 + + vt_test.proxy = NewOauthProxy(vt_test.opts, func(email string) bool { + return true + }) + return &vt_test +} + +func (vt_test *ValidateTokenTest) Close() { + vt_test.backend.Close() +} + +func TestValidateTokenEmptyToken(t *testing.T) { + vt_test := NewValidateTokenTest() + defer vt_test.Close() + + assert.Equal(t, false, vt_test.proxy.ValidateToken("")) +} + +func TestValidateTokenEmptyValidateUrl(t *testing.T) { + vt_test := NewValidateTokenTest() + defer vt_test.Close() + + vt_test.proxy.oauthValidateUrl = nil + assert.Equal(t, false, vt_test.proxy.ValidateToken("foobar")) +} + +func TestValidateTokenRequestNetworkFailure(t *testing.T) { + vt_test := NewValidateTokenTest() + // Close immediately to simulate a network failure + vt_test.Close() + + assert.Equal(t, false, vt_test.proxy.ValidateToken("foobar")) +} + +func TestValidateTokenExpiredToken(t *testing.T) { + vt_test := NewValidateTokenTest() + defer vt_test.Close() + + vt_test.response_code = 401 + assert.Equal(t, false, vt_test.proxy.ValidateToken("foobar")) +} + +func TestValidateTokenValidToken(t *testing.T) { + vt_test := NewValidateTokenTest() + defer vt_test.Close() + + assert.Equal(t, true, vt_test.proxy.ValidateToken("foobar")) +} + type ProcessCookieTest struct { opts *Options proxy *OauthProxy From b6e07d51b2af41b17d5ffc55d6a6d13521587eb9 Mon Sep 17 00:00:00 2001 From: Mike Bland Date: Sat, 9 May 2015 15:09:31 -0400 Subject: [PATCH 08/16] Validate access_token when auto-refreshing cookie --- oauthproxy.go | 5 +++- oauthproxy_test.go | 72 ++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 64 insertions(+), 13 deletions(-) diff --git a/oauthproxy.go b/oauthproxy.go index 4943bdf..1726515 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -299,7 +299,10 @@ func (p *OauthProxy) ProcessCookie(rw http.ResponseWriter, req *http.Request) (e } else if p.CookieRefresh != time.Duration(0) { refresh_threshold := time.Now().Add(p.CookieRefresh) if refresh_threshold.Unix() > timestamp.Unix() { - p.SetCookie(rw, req, value) + ok = p.ValidateToken(access_token) + if ok { + p.SetCookie(rw, req, value) + } } } return diff --git a/oauthproxy_test.go b/oauthproxy_test.go index e0d6f6b..4946a43 100644 --- a/oauthproxy_test.go +++ b/oauthproxy_test.go @@ -391,10 +391,12 @@ func TestValidateTokenValidToken(t *testing.T) { } type ProcessCookieTest struct { - opts *Options - proxy *OauthProxy - rw *httptest.ResponseRecorder - req *http.Request + opts *Options + proxy *OauthProxy + rw *httptest.ResponseRecorder + req *http.Request + backend *httptest.Server + response_code int } func NewProcessCookieTest() *ProcessCookieTest { @@ -405,6 +407,8 @@ func NewProcessCookieTest() *ProcessCookieTest { pc_test.opts.CookieSecret = "foobar" pc_test.opts.ClientID = "bazquux" pc_test.opts.ClientSecret = "xyzzyplugh" + pc_test.opts.PassAccessToken = true + pc_test.opts.CookieSecret = "0123456789abcdef" pc_test.opts.Validate() pc_test.proxy = NewOauthProxy(pc_test.opts, func(email string) bool { @@ -416,12 +420,32 @@ func NewProcessCookieTest() *ProcessCookieTest { return &pc_test } -func (p *ProcessCookieTest) MakeCookie(value string) *http.Cookie { - return p.proxy.MakeCookie(p.req, value, p.opts.CookieExpire) +func (p *ProcessCookieTest) InstantiateBackend() { + p.backend = httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(p.response_code) + })) + backend_url, _ := url.Parse(p.backend.URL) + p.proxy.oauthValidateUrl = &url.URL{ + Scheme: "http", + Host: backend_url.Host, + Path: "/oauth/tokeninfo", + } + p.response_code = 200 } -func (p *ProcessCookieTest) AddCookie(value string) { - p.req.AddCookie(p.MakeCookie(value)) +func (p *ProcessCookieTest) Close() { + p.backend.Close() +} + +func (p *ProcessCookieTest) MakeCookie(value, access_token string) *http.Cookie { + cookie_value, _ := buildCookieValue( + value, p.proxy.AesCipher, access_token) + return p.proxy.MakeCookie(p.req, cookie_value, p.opts.CookieExpire) +} + +func (p *ProcessCookieTest) AddCookie(value, access_token string) { + p.req.AddCookie(p.MakeCookie(value, access_token)) } func (p *ProcessCookieTest) ProcessCookie() (email, user, access_token string, ok bool) { @@ -430,11 +454,13 @@ func (p *ProcessCookieTest) ProcessCookie() (email, user, access_token string, o func TestProcessCookie(t *testing.T) { pc_test := NewProcessCookieTest() - pc_test.AddCookie("michael.bland@gsa.gov") - email, user, _, ok := pc_test.ProcessCookie() + + pc_test.AddCookie("michael.bland@gsa.gov", "my_access_token") + email, user, access_token, ok := pc_test.ProcessCookie() assert.Equal(t, true, ok) assert.Equal(t, "michael.bland@gsa.gov", email) assert.Equal(t, "michael.bland", user) + assert.Equal(t, "my_access_token", access_token) } func TestProcessCookieNoCookieError(t *testing.T) { @@ -445,7 +471,10 @@ func TestProcessCookieNoCookieError(t *testing.T) { func TestProcessCookieRefreshNotSet(t *testing.T) { pc_test := NewProcessCookieTest() - cookie := pc_test.MakeCookie("michael.bland@gsa.gov") + pc_test.InstantiateBackend() + defer pc_test.Close() + + cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "") cookie.Expires = time.Now().Add(time.Duration(23) * time.Hour) pc_test.req.AddCookie(cookie) @@ -456,7 +485,10 @@ func TestProcessCookieRefreshNotSet(t *testing.T) { func TestProcessCookieRefresh(t *testing.T) { pc_test := NewProcessCookieTest() - cookie := pc_test.MakeCookie("michael.bland@gsa.gov") + pc_test.InstantiateBackend() + defer pc_test.Close() + + cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token") cookie.Expires = time.Now().Add(time.Duration(23) * time.Hour) pc_test.req.AddCookie(cookie) @@ -465,3 +497,19 @@ func TestProcessCookieRefresh(t *testing.T) { assert.Equal(t, true, ok) assert.NotEqual(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"]) } + +func TestProcessCookieFailIfRefreshSetAndTokenNoLongerValid(t *testing.T) { + pc_test := NewProcessCookieTest() + pc_test.InstantiateBackend() + defer pc_test.Close() + pc_test.response_code = 401 + + cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token") + cookie.Expires = time.Now().Add(time.Duration(23) * time.Hour) + pc_test.req.AddCookie(cookie) + + pc_test.proxy.CookieRefresh = time.Duration(24) * time.Hour + _, _, _, ok := pc_test.ProcessCookie() + assert.Equal(t, false, ok) + assert.Equal(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"]) +} From bd4eae8fec75d885295a936c53eaf33e8506e217 Mon Sep 17 00:00:00 2001 From: Mike Bland Date: Sat, 9 May 2015 16:08:55 -0400 Subject: [PATCH 09/16] Store access token when cookie-refresh is set cookie-refresh now no longer requires pass-access-token in order to work. --- oauthproxy.go | 6 ++++-- oauthproxy_test.go | 7 ++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/oauthproxy.go b/oauthproxy.go index 1726515..c91441b 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -49,6 +49,7 @@ type OauthProxy struct { DisplayHtpasswdForm bool serveMux http.Handler PassBasicAuth bool + PassAccessToken bool AesCipher cipher.Block skipAuthRegex []string compiledRegex []*regexp.Regexp @@ -122,7 +123,7 @@ func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy { log.Printf("Cookie settings: secure (https):%v httponly:%v expiry:%s domain:%s", opts.CookieSecure, opts.CookieHttpOnly, opts.CookieExpire, domain) var aes_cipher cipher.Block - if opts.PassAccessToken { + if opts.PassAccessToken || (opts.CookieRefresh != time.Duration(0)) { var err error aes_cipher, err = aes.NewCipher([]byte(opts.CookieSecret)) if err != nil { @@ -153,6 +154,7 @@ func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy { skipAuthRegex: opts.SkipAuthRegex, compiledRegex: opts.CompiledRegex, PassBasicAuth: opts.PassBasicAuth, + PassAccessToken: opts.PassAccessToken, AesCipher: aes_cipher, templates: loadTemplates(opts.CustomTemplatesDir), } @@ -496,7 +498,7 @@ func (p *OauthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { req.Header["X-Forwarded-User"] = []string{user} req.Header["X-Forwarded-Email"] = []string{email} } - if access_token != "" { + if p.PassAccessToken { req.Header["X-Forwarded-Access-Token"] = []string{access_token} } if email == "" { diff --git a/oauthproxy_test.go b/oauthproxy_test.go index 4946a43..1823896 100644 --- a/oauthproxy_test.go +++ b/oauthproxy_test.go @@ -407,14 +407,19 @@ func NewProcessCookieTest() *ProcessCookieTest { pc_test.opts.CookieSecret = "foobar" pc_test.opts.ClientID = "bazquux" pc_test.opts.ClientSecret = "xyzzyplugh" - pc_test.opts.PassAccessToken = true pc_test.opts.CookieSecret = "0123456789abcdef" + // First, set the CookieRefresh option so proxy.AesCipher is created, + // needed to encrypt the access_token. + pc_test.opts.CookieRefresh = time.Duration(24) * time.Hour pc_test.opts.Validate() pc_test.proxy = NewOauthProxy(pc_test.opts, func(email string) bool { return true }) + // Now, zero-out proxy.CookieRefresh for the cases that don't involve + // access_token validation. + pc_test.proxy.CookieRefresh = time.Duration(0) pc_test.rw = httptest.NewRecorder() pc_test.req, _ = http.NewRequest("GET", "/", strings.NewReader("")) return &pc_test From 610341a068999e89a53dfdab16c33e41d3b4bf76 Mon Sep 17 00:00:00 2001 From: Mike Bland Date: Sat, 9 May 2015 16:31:18 -0400 Subject: [PATCH 10/16] Make ProcessCookie() fail when cookie parse fails --- oauthproxy.go | 1 + oauthproxy_test.go | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/oauthproxy.go b/oauthproxy.go index c91441b..9f099df 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -298,6 +298,7 @@ func (p *OauthProxy) ProcessCookie(rw http.ResponseWriter, req *http.Request) (e } if err != nil { log.Printf(err.Error()) + ok = false } else if p.CookieRefresh != time.Duration(0) { refresh_threshold := time.Now().Add(p.CookieRefresh) if refresh_threshold.Unix() > timestamp.Unix() { diff --git a/oauthproxy_test.go b/oauthproxy_test.go index 1823896..2627341 100644 --- a/oauthproxy_test.go +++ b/oauthproxy_test.go @@ -474,6 +474,17 @@ func TestProcessCookieNoCookieError(t *testing.T) { assert.Equal(t, false, ok) } +func TestProcessCookieFailIfParsingCookieValueFails(t *testing.T) { + pc_test := NewProcessCookieTest() + value, _ := buildCookieValue("michael.bland@gsa.gov", + pc_test.proxy.AesCipher, "my_access_token") + pc_test.req.AddCookie(pc_test.proxy.MakeCookie( + pc_test.req, value+"some bogus bytes", + pc_test.opts.CookieExpire)) + _, _, _, ok := pc_test.ProcessCookie() + assert.Equal(t, false, ok) +} + func TestProcessCookieRefreshNotSet(t *testing.T) { pc_test := NewProcessCookieTest() pc_test.InstantiateBackend() From 84190ab19aa9fe414e2440d7582eb8748337c1cd Mon Sep 17 00:00:00 2001 From: Mike Bland Date: Sat, 9 May 2015 16:48:39 -0400 Subject: [PATCH 11/16] Validate user during cookie refresh --- oauthproxy.go | 2 +- oauthproxy_test.go | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/oauthproxy.go b/oauthproxy.go index 9f099df..12023eb 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -302,7 +302,7 @@ func (p *OauthProxy) ProcessCookie(rw http.ResponseWriter, req *http.Request) (e } else if p.CookieRefresh != time.Duration(0) { refresh_threshold := time.Now().Add(p.CookieRefresh) if refresh_threshold.Unix() > timestamp.Unix() { - ok = p.ValidateToken(access_token) + ok = p.Validator(email) && p.ValidateToken(access_token) if ok { p.SetCookie(rw, req, value) } diff --git a/oauthproxy_test.go b/oauthproxy_test.go index 2627341..2b792d8 100644 --- a/oauthproxy_test.go +++ b/oauthproxy_test.go @@ -397,6 +397,7 @@ type ProcessCookieTest struct { req *http.Request backend *httptest.Server response_code int + validate_user bool } func NewProcessCookieTest() *ProcessCookieTest { @@ -414,7 +415,7 @@ func NewProcessCookieTest() *ProcessCookieTest { pc_test.opts.Validate() pc_test.proxy = NewOauthProxy(pc_test.opts, func(email string) bool { - return true + return pc_test.validate_user }) // Now, zero-out proxy.CookieRefresh for the cases that don't involve @@ -422,6 +423,7 @@ func NewProcessCookieTest() *ProcessCookieTest { pc_test.proxy.CookieRefresh = time.Duration(0) pc_test.rw = httptest.NewRecorder() pc_test.req, _ = http.NewRequest("GET", "/", strings.NewReader("")) + pc_test.validate_user = true return &pc_test } @@ -529,3 +531,19 @@ func TestProcessCookieFailIfRefreshSetAndTokenNoLongerValid(t *testing.T) { assert.Equal(t, false, ok) assert.Equal(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"]) } + +func TestProcessCookieFailIfRefreshSetAndUserNoLongerValid(t *testing.T) { + pc_test := NewProcessCookieTest() + pc_test.InstantiateBackend() + defer pc_test.Close() + pc_test.validate_user = false + + cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token") + cookie.Expires = time.Now().Add(time.Duration(23) * time.Hour) + pc_test.req.AddCookie(cookie) + + pc_test.proxy.CookieRefresh = time.Duration(24) * time.Hour + _, _, _, ok := pc_test.ProcessCookie() + assert.Equal(t, false, ok) + assert.Equal(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"]) +} From 082b7c0ec8393d2cc3904f5ecfaca07db5e10b94 Mon Sep 17 00:00:00 2001 From: Mike Bland Date: Sat, 9 May 2015 15:16:26 -0400 Subject: [PATCH 12/16] Set cookie-refresh flag = 0; update README, config --- README.md | 2 ++ contrib/google_auth_proxy.cfg.example | 9 +++++++-- main.go | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8420d7c..bec6315 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ Usage of google_auth_proxy: -cookie-expire=168h0m0s: expire timeframe for cookie -cookie-httponly=true: set HttpOnly cookie flag -cookie-https-only=true: set secure (HTTPS) cookies (deprecated. use --cookie-secure setting) + -cookie-refresh=144h0m0s: refresh the cookie when this much time remains before expiration -cookie-secret="": the seed string for secure cookies -cookie-secure=true: set secure (HTTPS) cookie flag -custom-templates-dir="": path to custom html templates @@ -96,6 +97,7 @@ Usage of google_auth_proxy: -scope="": Oauth scope specification -skip-auth-regex=: bypass authentication for requests path's that match (may be given multiple times) -upstream=: the http url(s) of the upstream endpoint. If multiple, routing is based on path + -validate-url="": Access token validation endpoint -version=false: print version string ``` diff --git a/contrib/google_auth_proxy.cfg.example b/contrib/google_auth_proxy.cfg.example index cf4ff06..114b8ab 100644 --- a/contrib/google_auth_proxy.cfg.example +++ b/contrib/google_auth_proxy.cfg.example @@ -46,12 +46,17 @@ ## Cookie Settings -## Secret - the seed string for secure cookies +## Secret - the seed string for secure cookies; should be 16, 24, or 32 bytes +## for use with an AES cipher when cookie_refresh or pass_access_code +## is set ## Domain - optional cookie domain to force cookies to (ie: .yourcompany.com) ## Expire - expire timeframe for cookie +## Refresh - refresh the cookie when less than this much time remains before +## expiration; should be less than cookie_expire; set to 0 to disable # cookie_secret = "" # cookie_domain = "" # cookie_expire = "168h" +# cookie_refresh = "144h" # cookie_secure = true # cookie_httponly = true - +# pass_access_code = true diff --git a/main.go b/main.go index e46a336..91da9ea 100644 --- a/main.go +++ b/main.go @@ -45,7 +45,7 @@ func main() { flagSet.String("cookie-secret", "", "the seed string for secure cookies") flagSet.String("cookie-domain", "", "an optional cookie domain to force cookies to (ie: .yourcompany.com)*") flagSet.Duration("cookie-expire", time.Duration(168)*time.Hour, "expire timeframe for cookie") - flagSet.Duration("cookie-refresh", time.Duration(24)*time.Hour, "refresh the cookie when this much time remains before expiration") + flagSet.Duration("cookie-refresh", time.Duration(0)*time.Hour, "refresh the cookie when this much time remains before expiration") flagSet.Bool("cookie-https-only", true, "set secure (HTTPS) cookies (deprecated. use --cookie-secure setting)") flagSet.Bool("cookie-secure", true, "set secure (HTTPS) cookie flag") flagSet.Bool("cookie-httponly", true, "set HttpOnly cookie flag") From 8ec967ac324734f0180f9b7307e0406aefe31246 Mon Sep 17 00:00:00 2001 From: Mike Bland Date: Sat, 9 May 2015 17:31:13 -0400 Subject: [PATCH 13/16] Check cookie_secret size when cookie_refresh set --- oauthproxy.go | 2 +- options.go | 6 +++--- options_test.go | 4 ++++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/oauthproxy.go b/oauthproxy.go index 12023eb..d4208de 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -128,7 +128,7 @@ func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy { aes_cipher, err = aes.NewCipher([]byte(opts.CookieSecret)) if err != nil { log.Fatal("error creating AES cipher with "+ - "pass_access_token == true: %s", err) + "cookie-secret ", opts.CookieSecret, ": ", err) } } diff --git a/options.go b/options.go index e6aafac..262e27f 100644 --- a/options.go +++ b/options.go @@ -120,7 +120,7 @@ func (o *Options) Validate() error { } msgs = parseProviderInfo(o, msgs) - if o.PassAccessToken { + if o.PassAccessToken || (o.CookieRefresh != time.Duration(0)) { valid_cookie_secret_size := false for _, i := range []int{16, 24, 32} { if len(o.CookieSecret) == i { @@ -131,8 +131,8 @@ func (o *Options) Validate() error { msgs = append(msgs, fmt.Sprintf( "cookie_secret must be 16, 24, or 32 bytes "+ "to create an AES cipher when "+ - "pass_access_token == true, "+ - "but is %d bytes", + "pass_access_token == true or "+ + "cookie_refresh != 0, but is %d bytes", len(o.CookieSecret))) } } diff --git a/options_test.go b/options_test.go index dcb5421..55eda29 100644 --- a/options_test.go +++ b/options_test.go @@ -112,6 +112,10 @@ func TestPassAccessTokenRequiresSpecificCookieSecretLengths(t *testing.T) { o.CookieSecret = "cookie of invalid length-" assert.NotEqual(t, nil, o.Validate()) + o.PassAccessToken = false + o.CookieRefresh = time.Duration(24) * time.Hour + assert.NotEqual(t, nil, o.Validate()) + o.CookieSecret = "16 bytes AES-128" assert.Equal(t, nil, o.Validate()) From 41b21dd0b14ce867dc5130b5aadc6b964fda2409 Mon Sep 17 00:00:00 2001 From: Mike Bland Date: Sat, 9 May 2015 17:16:19 -0400 Subject: [PATCH 14/16] Enforce that cookie_refresh < cookie_expire --- options.go | 8 ++++++++ options_test.go | 13 +++++++++++++ 2 files changed, 21 insertions(+) diff --git a/options.go b/options.go index 262e27f..3767546 100644 --- a/options.go +++ b/options.go @@ -137,6 +137,14 @@ func (o *Options) Validate() error { } } + if o.CookieRefresh >= o.CookieExpire { + msgs = append(msgs, fmt.Sprintf( + "cookie_refresh (%s) must be less than "+ + "cookie_expire (%s)", + o.CookieRefresh.String(), + o.CookieExpire.String())) + } + if len(msgs) != 0 { return fmt.Errorf("Invalid configuration:\n %s", strings.Join(msgs, "\n ")) diff --git a/options_test.go b/options_test.go index 55eda29..fc1233b 100644 --- a/options_test.go +++ b/options_test.go @@ -4,6 +4,7 @@ import ( "net/url" "strings" "testing" + "time" "github.com/bmizerany/assert" ) @@ -125,3 +126,15 @@ func TestPassAccessTokenRequiresSpecificCookieSecretLengths(t *testing.T) { o.CookieSecret = "32 byte secret for AES-256------" assert.Equal(t, nil, o.Validate()) } + +func TestCookieRefreshMustBeLessThanCookieExpire(t *testing.T) { + o := testOptions() + assert.Equal(t, nil, o.Validate()) + + o.CookieSecret = "0123456789abcdef" + o.CookieRefresh = o.CookieExpire + assert.NotEqual(t, nil, o.Validate()) + + o.CookieRefresh -= time.Duration(1) + assert.Equal(t, nil, o.Validate()) +} From 37f287bef46c11cee2e8c0d9af364b9f148a9225 Mon Sep 17 00:00:00 2001 From: Mike Bland Date: Sun, 10 May 2015 00:11:26 -0400 Subject: [PATCH 15/16] Calculate cookie expiration from encoded timestamp Found out the hard way that _incoming_ cookies do _not_ have their expiration timestamps encoded. To perform auto-refresh based on expiration time, we have to recalculate it from the time encoded in the cookie value. --- oauthproxy.go | 3 ++- oauthproxy_test.go | 23 +++++++++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/oauthproxy.go b/oauthproxy.go index d4208de..82de2df 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -300,8 +300,9 @@ func (p *OauthProxy) ProcessCookie(rw http.ResponseWriter, req *http.Request) (e log.Printf(err.Error()) ok = false } else if p.CookieRefresh != time.Duration(0) { + expires := timestamp.Add(p.CookieExpire) refresh_threshold := time.Now().Add(p.CookieRefresh) - if refresh_threshold.Unix() > timestamp.Unix() { + if refresh_threshold.Unix() > expires.Unix() { ok = p.Validator(email) && p.ValidateToken(access_token) if ok { p.SetCookie(rw, req, value) diff --git a/oauthproxy_test.go b/oauthproxy_test.go index 2b792d8..2a17436 100644 --- a/oauthproxy_test.go +++ b/oauthproxy_test.go @@ -492,8 +492,8 @@ func TestProcessCookieRefreshNotSet(t *testing.T) { pc_test.InstantiateBackend() defer pc_test.Close() + pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "") - cookie.Expires = time.Now().Add(time.Duration(23) * time.Hour) pc_test.req.AddCookie(cookie) _, _, _, ok := pc_test.ProcessCookie() @@ -506,8 +506,8 @@ func TestProcessCookieRefresh(t *testing.T) { pc_test.InstantiateBackend() defer pc_test.Close() + pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token") - cookie.Expires = time.Now().Add(time.Duration(23) * time.Hour) pc_test.req.AddCookie(cookie) pc_test.proxy.CookieRefresh = time.Duration(24) * time.Hour @@ -516,14 +516,29 @@ func TestProcessCookieRefresh(t *testing.T) { assert.NotEqual(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"]) } +func TestProcessCookieRefreshThresholdNotCrossed(t *testing.T) { + pc_test := NewProcessCookieTest() + pc_test.InstantiateBackend() + defer pc_test.Close() + + pc_test.proxy.CookieExpire = time.Duration(25) * time.Hour + cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token") + pc_test.req.AddCookie(cookie) + + pc_test.proxy.CookieRefresh = time.Duration(24) * time.Hour + _, _, _, ok := pc_test.ProcessCookie() + assert.Equal(t, true, ok) + assert.Equal(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"]) +} + func TestProcessCookieFailIfRefreshSetAndTokenNoLongerValid(t *testing.T) { pc_test := NewProcessCookieTest() pc_test.InstantiateBackend() defer pc_test.Close() pc_test.response_code = 401 + pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token") - cookie.Expires = time.Now().Add(time.Duration(23) * time.Hour) pc_test.req.AddCookie(cookie) pc_test.proxy.CookieRefresh = time.Duration(24) * time.Hour @@ -538,8 +553,8 @@ func TestProcessCookieFailIfRefreshSetAndUserNoLongerValid(t *testing.T) { defer pc_test.Close() pc_test.validate_user = false + pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token") - cookie.Expires = time.Now().Add(time.Duration(23) * time.Hour) pc_test.req.AddCookie(cookie) pc_test.proxy.CookieRefresh = time.Duration(24) * time.Hour From 2808ba7beb31e678b69b059dd528c9edb42194ed Mon Sep 17 00:00:00 2001 From: Mike Bland Date: Mon, 11 May 2015 09:55:07 -0400 Subject: [PATCH 16/16] Update cookie-refresh doc string --- README.md | 2 +- main.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bec6315..f12bd5c 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ Usage of google_auth_proxy: -cookie-expire=168h0m0s: expire timeframe for cookie -cookie-httponly=true: set HttpOnly cookie flag -cookie-https-only=true: set secure (HTTPS) cookies (deprecated. use --cookie-secure setting) - -cookie-refresh=144h0m0s: refresh the cookie when this much time remains before expiration + -cookie-refresh=0: refresh the cookie when less than this much time remains before expiration; 0 to disable -cookie-secret="": the seed string for secure cookies -cookie-secure=true: set secure (HTTPS) cookie flag -custom-templates-dir="": path to custom html templates diff --git a/main.go b/main.go index 91da9ea..9880342 100644 --- a/main.go +++ b/main.go @@ -45,7 +45,7 @@ func main() { flagSet.String("cookie-secret", "", "the seed string for secure cookies") flagSet.String("cookie-domain", "", "an optional cookie domain to force cookies to (ie: .yourcompany.com)*") flagSet.Duration("cookie-expire", time.Duration(168)*time.Hour, "expire timeframe for cookie") - flagSet.Duration("cookie-refresh", time.Duration(0)*time.Hour, "refresh the cookie when this much time remains before expiration") + flagSet.Duration("cookie-refresh", time.Duration(0)*time.Hour, "refresh the cookie when less than this much time remains before expiration; 0 to disable") flagSet.Bool("cookie-https-only", true, "set secure (HTTPS) cookies (deprecated. use --cookie-secure setting)") flagSet.Bool("cookie-secure", true, "set secure (HTTPS) cookie flag") flagSet.Bool("cookie-httponly", true, "set HttpOnly cookie flag")