git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge pull request #115 from jehiah/cookie_renew_115

Cookie Refresh Improvements

commitae2a1e55ab2da90ee2138e6515971d2d7c70b58c authorJehiah Czebotar <jehiah@gmail.com> dateTue 23 Jun 2015, 07:57 (11 years ago) parent66a0484e9b5631
5 files changed+112−54
contrib/oauth2_proxy.cfg.example+11−10 cookies.go+17−7 oauthproxy.go+15−13 oauthproxy_test.go+52−18 providers/internal_util.go+17−6
contrib/oauth2_proxy.cfg.example+11−10
@@ -54,16 +54,17 @@
5454# custom_templates_dir = ""
5555
5656## Cookie Settings
57−## Name - the cookie name
58−## Secret - the seed string for secure cookies; should be 16, 24, or 32 bytes
59−## for use with an AES cipher when cookie_refresh or pass_access_token
60−## is set
61−## Domain - (optional) cookie domain to force cookies to (ie: .yourcompany.com)
62−## Expire - (duration) expire timeframe for cookie
63−## Refresh - (duration) refresh the cookie when less than this much time remains before
64−## expiration; should be less than cookie_expire; set to 0 to disable.
65−## Refresh revalidated the OAuth token to ensure it is still valid. ie: 24h
66−## Secure - secure cookies are only sent by the browser of a HTTPS connection (recommended)
57+## Name - the cookie name
58+## Secret - the seed string for secure cookies; should be 16, 24, or 32 bytes
59+## for use with an AES cipher when cookie_refresh or pass_access_token
60+## is set
61+## Domain - (optional) cookie domain to force cookies to (ie: .yourcompany.com)
62+## Expire - (duration) expire timeframe for cookie
63+## Refresh - (duration) refresh the cookie when duration has elapsed after cookie was initially set.
64+## Should be less than cookie_expire; set to 0 to disable.
65+## On refresh, OAuth token is re-validated.
66+## (ie: 1h means tokens are refreshed on request 1hr+ after it was set)
67+## Secure - secure cookies are only sent by the browser of a HTTPS connection (recommended)
6768## HttpOnly - httponly cookies are not readable by javascript (recommended)
6869# cookie_name = "_oauth2_proxy"
6970# cookie_secret = ""
cookies.go+17−7
@@ -15,29 +15,39 @@ import (
1515 "time"
1616)
1717
18−func validateCookie(cookie *http.Cookie, seed string) (string, time.Time, bool) {
18+func validateCookie(cookie *http.Cookie, seed string, expiration time.Duration) (value string, t time.Time, ok bool) {
1919 // value, timestamp, sig
2020 parts := strings.Split(cookie.Value, "|")
2121 if len(parts) != 3 {
22− return "", time.Unix(0, 0), false
22+ return
2323 }
2424 sig := cookieSignature(seed, cookie.Name, parts[0], parts[1])
2525 if checkHmac(parts[2], sig) {
2626 ts, err := strconv.Atoi(parts[1])
27− if err == nil && int64(ts) > time.Now().Add(time.Duration(24)*7*time.Hour*-1).Unix() {
27+ if err != nil {
28+ return
29+ }
30+ // The expiration timestamp set when the cookie was created
31+ // isn't sent back by the browser. Hence, we check whether the
32+ // creation timestamp stored in the cookie falls within the
33+ // window defined by (Now()-expiration, Now()].
34+ t = time.Unix(int64(ts), 0)
35+ if t.After(time.Now().Add(expiration*-1)) && t.Before(time.Now().Add(time.Minute*5)) {
2836 // it's a valid cookie. now get the contents
2937 rawValue, err := base64.URLEncoding.DecodeString(parts[0])
3038 if err == nil {
31− return string(rawValue), time.Unix(int64(ts), 0), true
39+ value = string(rawValue)
40+ ok = true
41+ return
3242 }
3343 }
3444 }
35− return "", time.Unix(0, 0), false
45+ return
3646}
3747
38−func signedCookieValue(seed string, key string, value string) string {
48+func signedCookieValue(seed string, key string, value string, now time.Time) string {
3949 encodedValue := base64.URLEncoding.EncodeToString([]byte(value))
40− timeStr := fmt.Sprintf("%d", time.Now().Unix())
50+ timeStr := fmt.Sprintf("%d", now.Unix())
4151 sig := cookieSignature(seed, key, encodedValue, timeStr)
4252 cookieVal := fmt.Sprintf("%s|%s|%s", encodedValue, timeStr, sig)
4353 return cookieVal
oauthproxy.go+15−13
@@ -109,8 +109,12 @@ func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy {
109109 if domain == "" {
110110 domain = "<default>"
111111 }
112+ refresh := "disabled"
113+ if opts.CookieRefresh != time.Duration(0) {
114+ refresh = fmt.Sprintf("after %s", opts.CookieRefresh)
115+ }
112116
113− log.Printf("Cookie settings: name:%s secure(https):%v httponly:%v expiry:%s domain:%s", opts.CookieName, opts.CookieSecure, opts.CookieHttpOnly, opts.CookieExpire, domain)
117+ log.Printf("Cookie settings: name:%s secure(https):%v httponly:%v expiry:%s domain:%s refresh:%s", opts.CookieName, opts.CookieSecure, opts.CookieHttpOnly, opts.CookieExpire, domain, refresh)
114118
115119 var aes_cipher cipher.Block
116120 if opts.PassAccessToken || (opts.CookieRefresh != time.Duration(0)) {
@@ -191,7 +195,7 @@ func (p *OauthProxy) redeemCode(host, code string) (string, string, error) {
191195 return access_token, email, nil
192196}
193197
194−func (p *OauthProxy) MakeCookie(req *http.Request, value string, expiration time.Duration) *http.Cookie {
198+func (p *OauthProxy) MakeCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
195199 domain := req.Host
196200 if h, _, err := net.SplitHostPort(domain); err == nil {
197201 domain = h
@@ -204,7 +208,7 @@ func (p *OauthProxy) MakeCookie(req *http.Request, value string, expiration time
204208 }
205209
206210 if value != "" {
207− value = signedCookieValue(p.CookieSeed, p.CookieName, value)
211+ value = signedCookieValue(p.CookieSeed, p.CookieName, value, now)
208212 }
209213
210214 return &http.Cookie{
@@ -214,16 +218,16 @@ func (p *OauthProxy) MakeCookie(req *http.Request, value string, expiration time
214218 Domain: domain,
215219 HttpOnly: p.CookieHttpOnly,
216220 Secure: p.CookieSecure,
217− Expires: time.Now().Add(expiration),
221+ Expires: now.Add(expiration),
218222 }
219223}
220224
221225func (p *OauthProxy) ClearCookie(rw http.ResponseWriter, req *http.Request) {
222− http.SetCookie(rw, p.MakeCookie(req, "", time.Duration(1)*time.Hour*-1))
226+ http.SetCookie(rw, p.MakeCookie(req, "", time.Hour*-1, time.Now()))
223227}
224228
225229func (p *OauthProxy) SetCookie(rw http.ResponseWriter, req *http.Request, val string) {
226− http.SetCookie(rw, p.MakeCookie(req, val, p.CookieExpire))
230+ http.SetCookie(rw, p.MakeCookie(req, val, p.CookieExpire, time.Now()))
227231}
228232
229233func (p *OauthProxy) ProcessCookie(rw http.ResponseWriter, req *http.Request) (email, user, access_token string, ok bool) {
@@ -231,19 +235,17 @@ func (p *OauthProxy) ProcessCookie(rw http.ResponseWriter, req *http.Request) (e
231235 var timestamp time.Time
232236 cookie, err := req.Cookie(p.CookieName)
233237 if err == nil {
234− value, timestamp, ok = validateCookie(cookie, p.CookieSeed)
238+ value, timestamp, ok = validateCookie(cookie, p.CookieSeed, p.CookieExpire)
235239 if ok {
236− email, user, access_token, err = parseCookieValue(
237− value, p.AesCipher)
240+ email, user, access_token, err = parseCookieValue(value, p.AesCipher)
238241 }
239242 }
240243 if err != nil {
241244 log.Printf(err.Error())
242245 ok = false
243− } else if p.CookieRefresh != time.Duration(0) {
244− expires := timestamp.Add(p.CookieExpire)
245− refresh_threshold := time.Now().Add(p.CookieRefresh)
246− if refresh_threshold.Unix() > expires.Unix() {
246+ } else if ok && p.CookieRefresh != time.Duration(0) {
247+ refresh := timestamp.Add(p.CookieRefresh)
248+ if refresh.Before(time.Now()) {
247249 ok = p.Validator(email) && p.provider.ValidateToken(access_token)
248250 if ok {
249251 p.SetCookie(rw, req, value)
oauthproxy_test.go+52−18
@@ -348,13 +348,12 @@ func NewProcessCookieTest(opts ProcessCookieTestOpts) *ProcessCookieTest {
348348
349349 pc_test.opts = NewOptions()
350350 pc_test.opts.Upstreams = append(pc_test.opts.Upstreams, "unused")
351− pc_test.opts.CookieSecret = "foobar"
352351 pc_test.opts.ClientID = "bazquux"
353352 pc_test.opts.ClientSecret = "xyzzyplugh"
354353 pc_test.opts.CookieSecret = "0123456789abcdef"
355354 // First, set the CookieRefresh option so proxy.AesCipher is created,
356355 // needed to encrypt the access_token.
357− pc_test.opts.CookieRefresh = time.Duration(24) * time.Hour
356+ pc_test.opts.CookieRefresh = time.Hour
358357 pc_test.opts.Validate()
359358
360359 pc_test.proxy = NewOauthProxy(pc_test.opts, func(email string) bool {
@@ -379,14 +378,13 @@ func NewProcessCookieTestWithDefaults() *ProcessCookieTest {
379378 })
380379}
381380
382−func (p *ProcessCookieTest) MakeCookie(value, access_token string) *http.Cookie {
383− cookie_value, _ := buildCookieValue(
384− value, p.proxy.AesCipher, access_token)
385− return p.proxy.MakeCookie(p.req, cookie_value, p.opts.CookieExpire)
381+func (p *ProcessCookieTest) MakeCookie(value, access_token string, ref time.Time) *http.Cookie {
382+ cookie_value, _ := buildCookieValue(value, p.proxy.AesCipher, access_token)
383+ return p.proxy.MakeCookie(p.req, cookie_value, p.opts.CookieExpire, ref)
386384}
387385
388386func (p *ProcessCookieTest) AddCookie(value, access_token string) {
389− p.req.AddCookie(p.MakeCookie(value, access_token))
387+ p.req.AddCookie(p.MakeCookie(value, access_token, time.Now()))
390388}
391389
392390func (p *ProcessCookieTest) ProcessCookie() (email, user, access_token string, ok bool) {
@@ -416,7 +414,7 @@ func TestProcessCookieFailIfParsingCookieValueFails(t *testing.T) {
416414 pc_test.proxy.AesCipher, "my_access_token")
417415 pc_test.req.AddCookie(pc_test.proxy.MakeCookie(
418416 pc_test.req, value+"some bogus bytes",
419− pc_test.opts.CookieExpire))
417+ pc_test.opts.CookieExpire, time.Now()))
420418 _, _, _, ok := pc_test.ProcessCookie()
421419 assert.Equal(t, false, ok)
422420}
@@ -424,7 +422,8 @@ func TestProcessCookieFailIfParsingCookieValueFails(t *testing.T) {
424422func TestProcessCookieRefreshNotSet(t *testing.T) {
425423 pc_test := NewProcessCookieTestWithDefaults()
426424 pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour
427− cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "")
425+ reference := time.Now().Add(time.Duration(-2) * time.Hour)
426+ cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "", reference)
428427 pc_test.req.AddCookie(cookie)
429428
430429 _, _, _, ok := pc_test.ProcessCookie()
@@ -435,10 +434,11 @@ func TestProcessCookieRefreshNotSet(t *testing.T) {
435434func TestProcessCookieRefresh(t *testing.T) {
436435 pc_test := NewProcessCookieTestWithDefaults()
437436 pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour
438− cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token")
437+ reference := time.Now().Add(time.Duration(-2) * time.Hour)
438+ cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token", reference)
439439 pc_test.req.AddCookie(cookie)
440440
441− pc_test.proxy.CookieRefresh = time.Duration(24) * time.Hour
441+ pc_test.proxy.CookieRefresh = time.Hour
442442 _, _, _, ok := pc_test.ProcessCookie()
443443 assert.Equal(t, true, ok)
444444 assert.NotEqual(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"])
@@ -446,25 +446,58 @@ func TestProcessCookieRefresh(t *testing.T) {
446446
447447func TestProcessCookieRefreshThresholdNotCrossed(t *testing.T) {
448448 pc_test := NewProcessCookieTestWithDefaults()
449− pc_test.proxy.CookieExpire = time.Duration(25) * time.Hour
450− cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token")
449+ pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour
450+ reference := time.Now().Add(time.Duration(-30) * time.Minute)
451+ cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token", reference)
451452 pc_test.req.AddCookie(cookie)
452453
453− pc_test.proxy.CookieRefresh = time.Duration(24) * time.Hour
454+ pc_test.proxy.CookieRefresh = time.Hour
454455 _, _, _, ok := pc_test.ProcessCookie()
455456 assert.Equal(t, true, ok)
456457 assert.Equal(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"])
457458}
458459
460+func TestProcessCookieFailIfCookieExpired(t *testing.T) {
461+ pc_test := NewProcessCookieTestWithDefaults()
462+ pc_test.proxy.CookieExpire = time.Duration(24) * time.Hour
463+ reference := time.Now().Add(time.Duration(25) * time.Hour * -1)
464+ cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token", reference)
465+ pc_test.req.AddCookie(cookie)
466+
467+ if _, _, _, ok := pc_test.ProcessCookie(); ok {
468+ t.Error("ProcessCookie() should have failed")
469+ }
470+ if set_cookie := pc_test.rw.HeaderMap["Set-Cookie"]; set_cookie != nil {
471+ t.Error("expected Set-Cookie to be nil, instead was: ", set_cookie)
472+ }
473+}
474+
475+func TestProcessCookieFailIfRefreshSetAndCookieExpired(t *testing.T) {
476+ pc_test := NewProcessCookieTestWithDefaults()
477+ pc_test.proxy.CookieExpire = time.Duration(24) * time.Hour
478+ reference := time.Now().Add(time.Duration(25) * time.Hour * -1)
479+ cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token", reference)
480+ pc_test.req.AddCookie(cookie)
481+
482+ pc_test.proxy.CookieRefresh = time.Hour
483+ if _, _, _, ok := pc_test.ProcessCookie(); ok {
484+ t.Error("ProcessCookie() should have failed")
485+ }
486+ if set_cookie := pc_test.rw.HeaderMap["Set-Cookie"]; set_cookie != nil {
487+ t.Error("expected Set-Cookie to be nil, instead was: ", set_cookie)
488+ }
489+}
490+
459491func TestProcessCookieFailIfRefreshSetAndTokenNoLongerValid(t *testing.T) {
460492 pc_test := NewProcessCookieTest(ProcessCookieTestOpts{
461493 provider_validate_cookie_response: false,
462494 })
463495 pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour
464− cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token")
496+ reference := time.Now().Add(time.Duration(-24) * time.Hour)
497+ cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token", reference)
465498 pc_test.req.AddCookie(cookie)
466499
467− pc_test.proxy.CookieRefresh = time.Duration(24) * time.Hour
500+ pc_test.proxy.CookieRefresh = time.Hour
468501 _, _, _, ok := pc_test.ProcessCookie()
469502 assert.Equal(t, false, ok)
470503 assert.Equal(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"])
@@ -475,10 +508,11 @@ func TestProcessCookieFailIfRefreshSetAndUserNoLongerValid(t *testing.T) {
475508 pc_test.validate_user = false
476509
477510 pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour
478− cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token")
511+ reference := time.Now().Add(time.Duration(-2) * time.Hour)
512+ cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token", reference)
479513 pc_test.req.AddCookie(cookie)
480514
481− pc_test.proxy.CookieRefresh = time.Duration(24) * time.Hour
515+ pc_test.proxy.CookieRefresh = time.Hour
482516 _, _, _, ok := pc_test.ProcessCookie()
483517 assert.Equal(t, false, ok)
484518 assert.Equal(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"])
providers/internal_util.go+17−6
@@ -1,23 +1,34 @@
11package providers
22
33import (
4− "github.com/bitly/oauth2_proxy/api"
4+ "io/ioutil"
55 "log"
66 "net/http"
7+ "net/url"
8+
9+ "github.com/bitly/oauth2_proxy/api"
710)
811
912func validateToken(p Provider, access_token string, header http.Header) bool {
1013 if access_token == "" || p.Data().ValidateUrl == nil {
1114 return false
1215 }
13− url := p.Data().ValidateUrl.String()
16+ endpoint := p.Data().ValidateUrl.String()
1417 if len(header) == 0 {
15− url = url + "?access_token=" + access_token
18+ params := url.Values{"access_token": {access_token}}
19+ endpoint = endpoint + "?" + params.Encode()
1620 }
17− if resp, err := api.RequestUnparsedResponse(url, header); err != nil {
21+ resp, err := api.RequestUnparsedResponse(endpoint, header)
22+ if err != nil {
1823 log.Printf("token validation request failed: %s", err)
1924 return false
20− } else {
21− return resp.StatusCode == 200
2225 }
26+
27+ body, _ := ioutil.ReadAll(resp.Body)
28+ resp.Body.Close()
29+ if resp.StatusCode == 200 {
30+ return true
31+ }
32+ log.Printf("token validation request failed: status %d - %s", resp.StatusCode, body)
33+ return false
2334}