git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge pull request #88 from 18F/auto-refresh

Auto refresh auth token

commit9047920e90f0f571d1870c9b8677b26089740199 authorJehiah Czebotar <jehiah@gmail.com> dateMon 11 May 2015, 22:24 (11 years ago) parent5c03fe32808ba7
13 files changed+406−50
README.md+2 contrib/google_auth_proxy.cfg.example+7−2 cookies.go+4−4 main.go+2 oauthproxy.go+67−36 oauthproxy_test.go+257 options.go+20−8 options_test.go+17 providers/google.go+5 providers/google_test.go+9 providers/myusa.go+5 providers/myusa_test.go+10 providers/provider_data.go+1
README.md+2
@@ -77,6 +77,7 @@ Usage of google_auth_proxy:
7777 -cookie-expire=168h0m0s: expire timeframe for cookie
7878 -cookie-httponly=true: set HttpOnly cookie flag
7979 -cookie-https-only=true: set secure (HTTPS) cookies (deprecated. use --cookie-secure setting)
80+ -cookie-refresh=0: refresh the cookie when less than this much time remains before expiration; 0 to disable
8081 -cookie-secret="": the seed string for secure cookies
8182 -cookie-secure=true: set secure (HTTPS) cookie flag
8283 -custom-templates-dir="": path to custom html templates
@@ -96,6 +97,7 @@ Usage of google_auth_proxy:
9697 -scope="": Oauth scope specification
9798 -skip-auth-regex=: bypass authentication for requests path's that match (may be given multiple times)
9899 -upstream=: the http url(s) of the upstream endpoint. If multiple, routing is based on path
100+ -validate-url="": Access token validation endpoint
99101 -version=false: print version string
100102```
101103
contrib/google_auth_proxy.cfg.example+7−2
@@ -46,12 +46,17 @@
4646
4747
4848## Cookie Settings
49−## Secret - the seed string for secure cookies
49+## Secret - the seed string for secure cookies; should be 16, 24, or 32 bytes
50+## for use with an AES cipher when cookie_refresh or pass_access_code
51+## is set
5052## Domain - optional cookie domain to force cookies to (ie: .yourcompany.com)
5153## Expire - expire timeframe for cookie
54+## Refresh - refresh the cookie when less than this much time remains before
55+## expiration; should be less than cookie_expire; set to 0 to disable
5256# cookie_secret = ""
5357# cookie_domain = ""
5458# cookie_expire = "168h"
59+# cookie_refresh = "144h"
5560# cookie_secure = true
5661# cookie_httponly = true
57−
62+# pass_access_code = true
cookies.go+4−4
@@ -15,11 +15,11 @@ import (
1515 "time"
1616)
1717
18−func validateCookie(cookie *http.Cookie, seed string) (string, bool) {
18+func validateCookie(cookie *http.Cookie, seed string) (string, time.Time, bool) {
1919 // value, timestamp, sig
2020 parts := strings.Split(cookie.Value, "|")
2121 if len(parts) != 3 {
22− return "", false
22+ return "", time.Unix(0, 0), false
2323 }
2424 sig := cookieSignature(seed, cookie.Name, parts[0], parts[1])
2525 if checkHmac(parts[2], sig) {
@@ -28,11 +28,11 @@ func validateCookie(cookie *http.Cookie, seed string) (string, bool) {
2828 // it's a valid cookie. now get the contents
2929 rawValue, err := base64.URLEncoding.DecodeString(parts[0])
3030 if err == nil {
31− return string(rawValue), true
31+ return string(rawValue), time.Unix(int64(ts), 0), true
3232 }
3333 }
3434 }
35− return "", false
35+ return "", time.Unix(0, 0), false
3636}
3737
3838func signedCookieValue(seed string, key string, value string) string {
main.go+2
@@ -45,6 +45,7 @@ func main() {
4545 flagSet.String("cookie-secret", "", "the seed string for secure cookies")
4646 flagSet.String("cookie-domain", "", "an optional cookie domain to force cookies to (ie: .yourcompany.com)*")
4747 flagSet.Duration("cookie-expire", time.Duration(168)*time.Hour, "expire timeframe for cookie")
48+ flagSet.Duration("cookie-refresh", time.Duration(0)*time.Hour, "refresh the cookie when less than this much time remains before expiration; 0 to disable")
4849 flagSet.Bool("cookie-https-only", true, "set secure (HTTPS) cookies (deprecated. use --cookie-secure setting)")
4950 flagSet.Bool("cookie-secure", true, "set secure (HTTPS) cookie flag")
5051 flagSet.Bool("cookie-httponly", true, "set HttpOnly cookie flag")
@@ -55,6 +56,7 @@ func main() {
5556 flagSet.String("login-url", "", "Authentication endpoint")
5657 flagSet.String("redeem-url", "", "Token redemption endpoint")
5758 flagSet.String("profile-url", "", "Profile access endpoint")
59+ flagSet.String("validate-url", "", "Access token validation endpoint")
5860 flagSet.String("scope", "", "Oauth scope specification")
5961
6062 flagSet.Parse(os.Args[1:])
oauthproxy.go+67−36
@@ -34,12 +34,14 @@ type OauthProxy struct {
3434 CookieSecure bool
3535 CookieHttpOnly bool
3636 CookieExpire time.Duration
37+ CookieRefresh time.Duration
3738 Validator func(string) bool
3839
3940 redirectUrl *url.URL // the url to receive requests at
4041 provider providers.Provider
4142 oauthRedemptionUrl *url.URL // endpoint to redeem the code
4243 oauthLoginUrl *url.URL // to redirect the user to
44+ oauthValidateUrl *url.URL // to validate the access token
4345 oauthScope string
4446 clientID string
4547 clientSecret string
@@ -48,6 +50,7 @@ type OauthProxy struct {
4850 DisplayHtpasswdForm bool
4951 serveMux http.Handler
5052 PassBasicAuth bool
53+ PassAccessToken bool
5154 AesCipher cipher.Block
5255 skipAuthRegex []string
5356 compiledRegex []*regexp.Regexp
@@ -121,12 +124,12 @@ func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy {
121124 log.Printf("Cookie settings: secure (https):%v httponly:%v expiry:%s domain:%s", opts.CookieSecure, opts.CookieHttpOnly, opts.CookieExpire, domain)
122125
123126 var aes_cipher cipher.Block
124− if opts.PassAccessToken {
127+ if opts.PassAccessToken || (opts.CookieRefresh != time.Duration(0)) {
125128 var err error
126129 aes_cipher, err = aes.NewCipher([]byte(opts.CookieSecret))
127130 if err != nil {
128131 log.Fatal("error creating AES cipher with "+
129− "pass_access_token == true: %s", err)
132+ "cookie-secret ", opts.CookieSecret, ": ", err)
130133 }
131134 }
132135
@@ -137,6 +140,7 @@ func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy {
137140 CookieSecure: opts.CookieSecure,
138141 CookieHttpOnly: opts.CookieHttpOnly,
139142 CookieExpire: opts.CookieExpire,
143+ CookieRefresh: opts.CookieRefresh,
140144 Validator: validator,
141145
142146 clientID: opts.ClientID,
@@ -145,11 +149,13 @@ func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy {
145149 provider: opts.provider,
146150 oauthRedemptionUrl: opts.provider.Data().RedeemUrl,
147151 oauthLoginUrl: opts.provider.Data().LoginUrl,
152+ oauthValidateUrl: opts.provider.Data().ValidateUrl,
148153 serveMux: serveMux,
149154 redirectUrl: redirectUrl,
150155 skipAuthRegex: opts.SkipAuthRegex,
151156 compiledRegex: opts.CompiledRegex,
152157 PassBasicAuth: opts.PassBasicAuth,
158+ PassAccessToken: opts.PassAccessToken,
153159 AesCipher: aes_cipher,
154160 templates: loadTemplates(opts.CustomTemplatesDir),
155161 }
@@ -224,7 +230,7 @@ func (p *OauthProxy) redeemCode(host, code string) (string, string, error) {
224230 return access_token, email, nil
225231}
226232
227−func (p *OauthProxy) ClearCookie(rw http.ResponseWriter, req *http.Request) {
233+func (p *OauthProxy) MakeCookie(req *http.Request, value string, expiration time.Duration) *http.Cookie {
228234 domain := req.Host
229235 if h, _, err := net.SplitHostPort(domain); err == nil {
230236 domain = h
@@ -235,40 +241,76 @@ func (p *OauthProxy) ClearCookie(rw http.ResponseWriter, req *http.Request) {
235241 }
236242 domain = p.CookieDomain
237243 }
238− cookie := &http.Cookie{
244+
245+ if value != "" {
246+ value = signedCookieValue(p.CookieSeed, p.CookieKey, value)
247+ }
248+
249+ return &http.Cookie{
239250 Name: p.CookieKey,
240− Value: "",
251+ Value: value,
241252 Path: "/",
242253 Domain: domain,
243254 HttpOnly: p.CookieHttpOnly,
244255 Secure: p.CookieSecure,
245− Expires: time.Now().Add(time.Duration(1) * time.Hour * -1),
256+ Expires: time.Now().Add(expiration),
246257 }
247− http.SetCookie(rw, cookie)
258+}
259+
260+func (p *OauthProxy) ClearCookie(rw http.ResponseWriter, req *http.Request) {
261+ http.SetCookie(rw, p.MakeCookie(req, "", time.Duration(1)*time.Hour*-1))
248262}
249263
250264func (p *OauthProxy) SetCookie(rw http.ResponseWriter, req *http.Request, val string) {
265+ http.SetCookie(rw, p.MakeCookie(req, val, p.CookieExpire))
266+}
251267
252− domain := req.Host
253− if h, _, err := net.SplitHostPort(domain); err == nil {
254− domain = h
268+func (p *OauthProxy) ValidateToken(access_token string) bool {
269+ if access_token == "" || p.oauthValidateUrl == nil {
270+ return false
255271 }
256− if p.CookieDomain != "" {
257− if !strings.HasSuffix(domain, p.CookieDomain) {
258− log.Printf("Warning: request host is %q but using configured cookie domain of %q", domain, p.CookieDomain)
272+
273+ req, err := http.NewRequest("GET",
274+ p.oauthValidateUrl.String()+"?access_token="+access_token, nil)
275+ if err != nil {
276+ log.Printf("failed building token validation request: %s", err)
277+ return false
278+ }
279+
280+ httpclient := &http.Client{}
281+ resp, err := httpclient.Do(req)
282+ if err != nil {
283+ log.Printf("token validation request failed: %s", err)
284+ return false
285+ }
286+ return resp.StatusCode == 200
287+}
288+
289+func (p *OauthProxy) ProcessCookie(rw http.ResponseWriter, req *http.Request) (email, user, access_token string, ok bool) {
290+ var value string
291+ var timestamp time.Time
292+ cookie, err := req.Cookie(p.CookieKey)
293+ if err == nil {
294+ value, timestamp, ok = validateCookie(cookie, p.CookieSeed)
295+ if ok {
296+ email, user, access_token, err = parseCookieValue(
297+ value, p.AesCipher)
259298 }
260− domain = p.CookieDomain
261299 }
262− cookie := &http.Cookie{
263− Name: p.CookieKey,
264− Value: signedCookieValue(p.CookieSeed, p.CookieKey, val),
265− Path: "/",
266− Domain: domain,
267− HttpOnly: p.CookieHttpOnly,
268− Secure: p.CookieSecure,
269− Expires: time.Now().Add(p.CookieExpire),
300+ if err != nil {
301+ log.Printf(err.Error())
302+ ok = false
303+ } else if p.CookieRefresh != time.Duration(0) {
304+ expires := timestamp.Add(p.CookieExpire)
305+ refresh_threshold := time.Now().Add(p.CookieRefresh)
306+ if refresh_threshold.Unix() > expires.Unix() {
307+ ok = p.Validator(email) && p.ValidateToken(access_token)
308+ if ok {
309+ p.SetCookie(rw, req, value)
310+ }
311+ }
270312 }
271− http.SetCookie(rw, cookie)
313+ return
272314}
273315
274316func (p *OauthProxy) RobotsTxt(rw http.ResponseWriter) {
@@ -451,18 +493,7 @@ func (p *OauthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
451493 }
452494
453495 if !ok {
454− cookie, err := req.Cookie(p.CookieKey)
455− if err == nil {
456− var value string
457− value, ok = validateCookie(cookie, p.CookieSeed)
458− if ok {
459− email, user, access_token, err = parseCookieValue(
460− value, p.AesCipher)
461− if err != nil {
462− log.Printf(err.Error())
463− }
464− }
465− }
496+ email, user, access_token, ok = p.ProcessCookie(rw, req)
466497 }
467498
468499 if !ok {
@@ -480,7 +511,7 @@ func (p *OauthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
480511 req.Header["X-Forwarded-User"] = []string{user}
481512 req.Header["X-Forwarded-Email"] = []string{email}
482513 }
483− if access_token != "" {
514+ if p.PassAccessToken {
484515 req.Header["X-Forwarded-Access-Token"] = []string{access_token}
485516 }
486517 if email == "" {
oauthproxy_test.go+257
@@ -321,3 +321,260 @@ func TestSignInPageDirectAccessRedirectsToRoot(t *testing.T) {
321321 t.Fatal(`expected redirect to "/", but was "` + match[1] + `"`)
322322 }
323323}
324+
325+type ValidateTokenTest struct {
326+ opts *Options
327+ proxy *OauthProxy
328+ backend *httptest.Server
329+ response_code int
330+}
331+
332+func NewValidateTokenTest() *ValidateTokenTest {
333+ var vt_test ValidateTokenTest
334+
335+ vt_test.opts = NewOptions()
336+ vt_test.opts.Upstreams = append(vt_test.opts.Upstreams, "unused")
337+ vt_test.opts.CookieSecret = "foobar"
338+ vt_test.opts.ClientID = "bazquux"
339+ vt_test.opts.ClientSecret = "xyzzyplugh"
340+ vt_test.opts.Validate()
341+
342+ vt_test.backend = httptest.NewServer(
343+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
344+ switch r.URL.Path {
345+ case "/oauth/tokeninfo":
346+ w.WriteHeader(vt_test.response_code)
347+ w.Write([]byte("only code matters; contents disregarded"))
348+ default:
349+ w.WriteHeader(500)
350+ w.Write([]byte("unknown URL"))
351+ }
352+ }))
353+ backend_url, _ := url.Parse(vt_test.backend.URL)
354+ vt_test.opts.provider.Data().ValidateUrl = &url.URL{
355+ Scheme: "http",
356+ Host: backend_url.Host,
357+ Path: "/oauth/tokeninfo",
358+ }
359+ vt_test.response_code = 200
360+
361+ vt_test.proxy = NewOauthProxy(vt_test.opts, func(email string) bool {
362+ return true
363+ })
364+ return &vt_test
365+}
366+
367+func (vt_test *ValidateTokenTest) Close() {
368+ vt_test.backend.Close()
369+}
370+
371+func TestValidateTokenEmptyToken(t *testing.T) {
372+ vt_test := NewValidateTokenTest()
373+ defer vt_test.Close()
374+
375+ assert.Equal(t, false, vt_test.proxy.ValidateToken(""))
376+}
377+
378+func TestValidateTokenEmptyValidateUrl(t *testing.T) {
379+ vt_test := NewValidateTokenTest()
380+ defer vt_test.Close()
381+
382+ vt_test.proxy.oauthValidateUrl = nil
383+ assert.Equal(t, false, vt_test.proxy.ValidateToken("foobar"))
384+}
385+
386+func TestValidateTokenRequestNetworkFailure(t *testing.T) {
387+ vt_test := NewValidateTokenTest()
388+ // Close immediately to simulate a network failure
389+ vt_test.Close()
390+
391+ assert.Equal(t, false, vt_test.proxy.ValidateToken("foobar"))
392+}
393+
394+func TestValidateTokenExpiredToken(t *testing.T) {
395+ vt_test := NewValidateTokenTest()
396+ defer vt_test.Close()
397+
398+ vt_test.response_code = 401
399+ assert.Equal(t, false, vt_test.proxy.ValidateToken("foobar"))
400+}
401+
402+func TestValidateTokenValidToken(t *testing.T) {
403+ vt_test := NewValidateTokenTest()
404+ defer vt_test.Close()
405+
406+ assert.Equal(t, true, vt_test.proxy.ValidateToken("foobar"))
407+}
408+
409+type ProcessCookieTest struct {
410+ opts *Options
411+ proxy *OauthProxy
412+ rw *httptest.ResponseRecorder
413+ req *http.Request
414+ backend *httptest.Server
415+ response_code int
416+ validate_user bool
417+}
418+
419+func NewProcessCookieTest() *ProcessCookieTest {
420+ var pc_test ProcessCookieTest
421+
422+ pc_test.opts = NewOptions()
423+ pc_test.opts.Upstreams = append(pc_test.opts.Upstreams, "unused")
424+ pc_test.opts.CookieSecret = "foobar"
425+ pc_test.opts.ClientID = "bazquux"
426+ pc_test.opts.ClientSecret = "xyzzyplugh"
427+ pc_test.opts.CookieSecret = "0123456789abcdef"
428+ // First, set the CookieRefresh option so proxy.AesCipher is created,
429+ // needed to encrypt the access_token.
430+ pc_test.opts.CookieRefresh = time.Duration(24) * time.Hour
431+ pc_test.opts.Validate()
432+
433+ pc_test.proxy = NewOauthProxy(pc_test.opts, func(email string) bool {
434+ return pc_test.validate_user
435+ })
436+
437+ // Now, zero-out proxy.CookieRefresh for the cases that don't involve
438+ // access_token validation.
439+ pc_test.proxy.CookieRefresh = time.Duration(0)
440+ pc_test.rw = httptest.NewRecorder()
441+ pc_test.req, _ = http.NewRequest("GET", "/", strings.NewReader(""))
442+ pc_test.validate_user = true
443+ return &pc_test
444+}
445+
446+func (p *ProcessCookieTest) InstantiateBackend() {
447+ p.backend = httptest.NewServer(
448+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
449+ w.WriteHeader(p.response_code)
450+ }))
451+ backend_url, _ := url.Parse(p.backend.URL)
452+ p.proxy.oauthValidateUrl = &url.URL{
453+ Scheme: "http",
454+ Host: backend_url.Host,
455+ Path: "/oauth/tokeninfo",
456+ }
457+ p.response_code = 200
458+}
459+
460+func (p *ProcessCookieTest) Close() {
461+ p.backend.Close()
462+}
463+
464+func (p *ProcessCookieTest) MakeCookie(value, access_token string) *http.Cookie {
465+ cookie_value, _ := buildCookieValue(
466+ value, p.proxy.AesCipher, access_token)
467+ return p.proxy.MakeCookie(p.req, cookie_value, p.opts.CookieExpire)
468+}
469+
470+func (p *ProcessCookieTest) AddCookie(value, access_token string) {
471+ p.req.AddCookie(p.MakeCookie(value, access_token))
472+}
473+
474+func (p *ProcessCookieTest) ProcessCookie() (email, user, access_token string, ok bool) {
475+ return p.proxy.ProcessCookie(p.rw, p.req)
476+}
477+
478+func TestProcessCookie(t *testing.T) {
479+ pc_test := NewProcessCookieTest()
480+
481+ pc_test.AddCookie("michael.bland@gsa.gov", "my_access_token")
482+ email, user, access_token, ok := pc_test.ProcessCookie()
483+ assert.Equal(t, true, ok)
484+ assert.Equal(t, "michael.bland@gsa.gov", email)
485+ assert.Equal(t, "michael.bland", user)
486+ assert.Equal(t, "my_access_token", access_token)
487+}
488+
489+func TestProcessCookieNoCookieError(t *testing.T) {
490+ pc_test := NewProcessCookieTest()
491+ _, _, _, ok := pc_test.ProcessCookie()
492+ assert.Equal(t, false, ok)
493+}
494+
495+func TestProcessCookieFailIfParsingCookieValueFails(t *testing.T) {
496+ pc_test := NewProcessCookieTest()
497+ value, _ := buildCookieValue("michael.bland@gsa.gov",
498+ pc_test.proxy.AesCipher, "my_access_token")
499+ pc_test.req.AddCookie(pc_test.proxy.MakeCookie(
500+ pc_test.req, value+"some bogus bytes",
501+ pc_test.opts.CookieExpire))
502+ _, _, _, ok := pc_test.ProcessCookie()
503+ assert.Equal(t, false, ok)
504+}
505+
506+func TestProcessCookieRefreshNotSet(t *testing.T) {
507+ pc_test := NewProcessCookieTest()
508+ pc_test.InstantiateBackend()
509+ defer pc_test.Close()
510+
511+ pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour
512+ cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "")
513+ pc_test.req.AddCookie(cookie)
514+
515+ _, _, _, ok := pc_test.ProcessCookie()
516+ assert.Equal(t, true, ok)
517+ assert.Equal(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"])
518+}
519+
520+func TestProcessCookieRefresh(t *testing.T) {
521+ pc_test := NewProcessCookieTest()
522+ pc_test.InstantiateBackend()
523+ defer pc_test.Close()
524+
525+ pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour
526+ cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token")
527+ pc_test.req.AddCookie(cookie)
528+
529+ pc_test.proxy.CookieRefresh = time.Duration(24) * time.Hour
530+ _, _, _, ok := pc_test.ProcessCookie()
531+ assert.Equal(t, true, ok)
532+ assert.NotEqual(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"])
533+}
534+
535+func TestProcessCookieRefreshThresholdNotCrossed(t *testing.T) {
536+ pc_test := NewProcessCookieTest()
537+ pc_test.InstantiateBackend()
538+ defer pc_test.Close()
539+
540+ pc_test.proxy.CookieExpire = time.Duration(25) * time.Hour
541+ cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token")
542+ pc_test.req.AddCookie(cookie)
543+
544+ pc_test.proxy.CookieRefresh = time.Duration(24) * time.Hour
545+ _, _, _, ok := pc_test.ProcessCookie()
546+ assert.Equal(t, true, ok)
547+ assert.Equal(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"])
548+}
549+
550+func TestProcessCookieFailIfRefreshSetAndTokenNoLongerValid(t *testing.T) {
551+ pc_test := NewProcessCookieTest()
552+ pc_test.InstantiateBackend()
553+ defer pc_test.Close()
554+ pc_test.response_code = 401
555+
556+ pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour
557+ cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token")
558+ pc_test.req.AddCookie(cookie)
559+
560+ pc_test.proxy.CookieRefresh = time.Duration(24) * time.Hour
561+ _, _, _, ok := pc_test.ProcessCookie()
562+ assert.Equal(t, false, ok)
563+ assert.Equal(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"])
564+}
565+
566+func TestProcessCookieFailIfRefreshSetAndUserNoLongerValid(t *testing.T) {
567+ pc_test := NewProcessCookieTest()
568+ pc_test.InstantiateBackend()
569+ defer pc_test.Close()
570+ pc_test.validate_user = false
571+
572+ pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour
573+ cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token")
574+ pc_test.req.AddCookie(cookie)
575+
576+ pc_test.proxy.CookieRefresh = time.Duration(24) * time.Hour
577+ _, _, _, ok := pc_test.ProcessCookie()
578+ assert.Equal(t, false, ok)
579+ assert.Equal(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"])
580+}
options.go+20−8
@@ -26,6 +26,7 @@ type Options struct {
2626 CookieSecret string `flag:"cookie-secret" cfg:"cookie_secret" env:"GOOGLE_AUTH_PROXY_COOKIE_SECRET"`
2727 CookieDomain string `flag:"cookie-domain" cfg:"cookie_domain" env:"GOOGLE_AUTH_PROXY_COOKIE_DOMAIN"`
2828 CookieExpire time.Duration `flag:"cookie-expire" cfg:"cookie_expire" env:"GOOGLE_AUTH_PROXY_COOKIE_EXPIRE"`
29+ CookieRefresh time.Duration `flag:"cookie-refresh" cfg:"cookie_refresh" env:"GOOGLE_AUTH_PROXY_COOKIE_REFRESH"`
2930 CookieHttpsOnly bool `flag:"cookie-https-only" cfg:"cookie_https_only"` // deprecated use cookie-secure
3031 CookieSecure bool `flag:"cookie-secure" cfg:"cookie_secure"`
3132 CookieHttpOnly bool `flag:"cookie-httponly" cfg:"cookie_httponly"`
@@ -38,11 +39,12 @@ type Options struct {
3839
3940 // These options allow for other providers besides Google, with
4041 // potential overrides.
41− Provider string `flag:"provider" cfg:"provider"`
42− LoginUrl string `flag:"login-url" cfg:"login_url"`
43− RedeemUrl string `flag:"redeem-url" cfg:"redeem_url"`
44− ProfileUrl string `flag:"profile-url" cfg:"profile_url"`
45− Scope string `flag:"scope" cfg:"scope"`
42+ Provider string `flag:"provider" cfg:"provider"`
43+ LoginUrl string `flag:"login-url" cfg:"login_url"`
44+ RedeemUrl string `flag:"redeem-url" cfg:"redeem_url"`
45+ ProfileUrl string `flag:"profile-url" cfg:"profile_url"`
46+ ValidateUrl string `flag:"validate-url" cfg:"validate_url"`
47+ Scope string `flag:"scope" cfg:"scope"`
4648
4749 RequestLogging bool `flag:"request-logging" cfg:"request_logging"`
4850
@@ -61,6 +63,7 @@ func NewOptions() *Options {
6163 CookieSecure: true,
6264 CookieHttpOnly: true,
6365 CookieExpire: time.Duration(168) * time.Hour,
66+ CookieRefresh: time.Duration(0),
6467 PassBasicAuth: true,
6568 PassAccessToken: false,
6669 PassHostHeader: true,
@@ -117,7 +120,7 @@ func (o *Options) Validate() error {
117120 }
118121 msgs = parseProviderInfo(o, msgs)
119122
120− if o.PassAccessToken {
123+ if o.PassAccessToken || (o.CookieRefresh != time.Duration(0)) {
121124 valid_cookie_secret_size := false
122125 for _, i := range []int{16, 24, 32} {
123126 if len(o.CookieSecret) == i {
@@ -128,12 +131,20 @@ func (o *Options) Validate() error {
128131 msgs = append(msgs, fmt.Sprintf(
129132 "cookie_secret must be 16, 24, or 32 bytes "+
130133 "to create an AES cipher when "+
131− "pass_access_token == true, "+
132− "but is %d bytes",
134+ "pass_access_token == true or "+
135+ "cookie_refresh != 0, but is %d bytes",
133136 len(o.CookieSecret)))
134137 }
135138 }
136139
140+ if o.CookieRefresh >= o.CookieExpire {
141+ msgs = append(msgs, fmt.Sprintf(
142+ "cookie_refresh (%s) must be less than "+
143+ "cookie_expire (%s)",
144+ o.CookieRefresh.String(),
145+ o.CookieExpire.String()))
146+ }
147+
137148 if len(msgs) != 0 {
138149 return fmt.Errorf("Invalid configuration:\n %s",
139150 strings.Join(msgs, "\n "))
@@ -146,6 +157,7 @@ func parseProviderInfo(o *Options, msgs []string) []string {
146157 p.LoginUrl, msgs = parseUrl(o.LoginUrl, "login", msgs)
147158 p.RedeemUrl, msgs = parseUrl(o.RedeemUrl, "redeem", msgs)
148159 p.ProfileUrl, msgs = parseUrl(o.ProfileUrl, "profile", msgs)
160+ p.ValidateUrl, msgs = parseUrl(o.ValidateUrl, "validate", msgs)
149161 o.provider = providers.New(o.Provider, p)
150162 return msgs
151163}
options_test.go+17
@@ -4,6 +4,7 @@ import (
44 "net/url"
55 "strings"
66 "testing"
7+ "time"
78
89 "github.com/bmizerany/assert"
910)
@@ -112,6 +113,10 @@ func TestPassAccessTokenRequiresSpecificCookieSecretLengths(t *testing.T) {
112113 o.CookieSecret = "cookie of invalid length-"
113114 assert.NotEqual(t, nil, o.Validate())
114115
116+ o.PassAccessToken = false
117+ o.CookieRefresh = time.Duration(24) * time.Hour
118+ assert.NotEqual(t, nil, o.Validate())
119+
115120 o.CookieSecret = "16 bytes AES-128"
116121 assert.Equal(t, nil, o.Validate())
117122
@@ -121,3 +126,15 @@ func TestPassAccessTokenRequiresSpecificCookieSecretLengths(t *testing.T) {
121126 o.CookieSecret = "32 byte secret for AES-256------"
122127 assert.Equal(t, nil, o.Validate())
123128}
129+
130+func TestCookieRefreshMustBeLessThanCookieExpire(t *testing.T) {
131+ o := testOptions()
132+ assert.Equal(t, nil, o.Validate())
133+
134+ o.CookieSecret = "0123456789abcdef"
135+ o.CookieRefresh = o.CookieExpire
136+ assert.NotEqual(t, nil, o.Validate())
137+
138+ o.CookieRefresh -= time.Duration(1)
139+ assert.Equal(t, nil, o.Validate())
140+}
providers/google.go+5
@@ -24,6 +24,11 @@ func NewGoogleProvider(p *ProviderData) *GoogleProvider {
2424 Host: "accounts.google.com",
2525 Path: "/o/oauth2/token"}
2626 }
27+ if p.ValidateUrl.String() == "" {
28+ p.ValidateUrl = &url.URL{Scheme: "https",
29+ Host: "www.googleapis.com",
30+ Path: "/oauth2/v1/tokeninfo"}
31+ }
2732 if p.Scope == "" {
2833 p.Scope = "profile email"
2934 }
providers/google_test.go+9
@@ -15,6 +15,7 @@ func newGoogleProvider() *GoogleProvider {
1515 LoginUrl: &url.URL{},
1616 RedeemUrl: &url.URL{},
1717 ProfileUrl: &url.URL{},
18+ ValidateUrl: &url.URL{},
1819 Scope: ""})
1920}
2021
@@ -26,6 +27,8 @@ func TestGoogleProviderDefaults(t *testing.T) {
2627 p.Data().LoginUrl.String())
2728 assert.Equal(t, "https://accounts.google.com/o/oauth2/token",
2829 p.Data().RedeemUrl.String())
30+ assert.Equal(t, "https://www.googleapis.com/oauth2/v1/tokeninfo",
31+ p.Data().ValidateUrl.String())
2932 assert.Equal(t, "", p.Data().ProfileUrl.String())
3033 assert.Equal(t, "profile email", p.Data().Scope)
3134}
@@ -45,6 +48,10 @@ func TestGoogleProviderOverrides(t *testing.T) {
4548 Scheme: "https",
4649 Host: "example.com",
4750 Path: "/oauth/profile"},
51+ ValidateUrl: &url.URL{
52+ Scheme: "https",
53+ Host: "example.com",
54+ Path: "/oauth/tokeninfo"},
4855 Scope: "profile"})
4956 assert.NotEqual(t, nil, p)
5057 assert.Equal(t, "Google", p.Data().ProviderName)
@@ -54,6 +61,8 @@ func TestGoogleProviderOverrides(t *testing.T) {
5461 p.Data().RedeemUrl.String())
5562 assert.Equal(t, "https://example.com/oauth/profile",
5663 p.Data().ProfileUrl.String())
64+ assert.Equal(t, "https://example.com/oauth/tokeninfo",
65+ p.Data().ValidateUrl.String())
5766 assert.Equal(t, "profile", p.Data().Scope)
5867}
5968
providers/myusa.go+5
@@ -32,6 +32,11 @@ func NewMyUsaProvider(p *ProviderData) *MyUsaProvider {
3232 Host: myUsaHost,
3333 Path: "/api/v1/profile"}
3434 }
35+ if p.ValidateUrl.String() == "" {
36+ p.ValidateUrl = &url.URL{Scheme: "https",
37+ Host: myUsaHost,
38+ Path: "/api/v1/tokeninfo"}
39+ }
3540 if p.Scope == "" {
3641 p.Scope = "profile.email"
3742 }
providers/myusa_test.go+10
@@ -21,11 +21,13 @@ func testMyUsaProvider(hostname string) *MyUsaProvider {
2121 LoginUrl: &url.URL{},
2222 RedeemUrl: &url.URL{},
2323 ProfileUrl: &url.URL{},
24+ ValidateUrl: &url.URL{},
2425 Scope: ""})
2526 if hostname != "" {
2627 updateUrl(p.Data().LoginUrl, hostname)
2728 updateUrl(p.Data().RedeemUrl, hostname)
2829 updateUrl(p.Data().ProfileUrl, hostname)
30+ updateUrl(p.Data().ValidateUrl, hostname)
2931 }
3032 return p
3133}
@@ -56,6 +58,8 @@ func TestMyUsaProviderDefaults(t *testing.T) {
5658 p.Data().RedeemUrl.String())
5759 assert.Equal(t, "https://alpha.my.usa.gov/api/v1/profile",
5860 p.Data().ProfileUrl.String())
61+ assert.Equal(t, "https://alpha.my.usa.gov/api/v1/tokeninfo",
62+ p.Data().ValidateUrl.String())
5963 assert.Equal(t, "profile.email", p.Data().Scope)
6064}
6165
@@ -74,6 +78,10 @@ func TestMyUsaProviderOverrides(t *testing.T) {
7478 Scheme: "https",
7579 Host: "example.com",
7680 Path: "/oauth/profile"},
81+ ValidateUrl: &url.URL{
82+ Scheme: "https",
83+ Host: "example.com",
84+ Path: "/oauth/tokeninfo"},
7785 Scope: "profile"})
7886 assert.NotEqual(t, nil, p)
7987 assert.Equal(t, "MyUSA", p.Data().ProviderName)
@@ -83,6 +91,8 @@ func TestMyUsaProviderOverrides(t *testing.T) {
8391 p.Data().RedeemUrl.String())
8492 assert.Equal(t, "https://example.com/oauth/profile",
8593 p.Data().ProfileUrl.String())
94+ assert.Equal(t, "https://example.com/oauth/tokeninfo",
95+ p.Data().ValidateUrl.String())
8696 assert.Equal(t, "profile", p.Data().Scope)
8797}
8898
providers/provider_data.go+1
@@ -9,6 +9,7 @@ type ProviderData struct {
99 LoginUrl *url.URL
1010 RedeemUrl *url.URL
1111 ProfileUrl *url.URL
12+ ValidateUrl *url.URL
1213 Scope string
1314}
1415