git.iko.soy/typeable/oauth2-proxy
on master help sign in
cookie refresh: validation fixes, interval changes
e9b5631Jehiah Czebotar
contrib/oauth2_proxy.cfg.example+11−10
@@ -54,16 +54,17 @@
54# custom_templates_dir = ""
54# custom_templates_dir = ""
55
55
56## Cookie Settings
56## Cookie Settings
57−## Name - the cookie name
57+## Name - the cookie name
58−## Secret - the seed string for secure cookies; should be 16, 24, or 32 bytes
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
59+## for use with an AES cipher when cookie_refresh or pass_access_token
60−## is set
60+## is set
61−## Domain - (optional) cookie domain to force cookies to (ie: .yourcompany.com)
61+## Domain - (optional) cookie domain to force cookies to (ie: .yourcompany.com)
62−## Expire - (duration) expire timeframe for cookie
62+## Expire - (duration) expire timeframe for cookie
63−## Refresh - (duration) refresh the cookie when less than this much time remains before
63+## Refresh - (duration) refresh the cookie when duration has elapsed after cookie was initially set.
64−## expiration; should be less than cookie_expire; set to 0 to disable.
64+## 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
65+## On refresh, OAuth token is re-validated.
66+## (ie: 1h means tokens are refreshed on request 1hr+ after it was set)
66−## Secure - secure cookies are only sent by the browser of a HTTPS connection (recommended)
67+## Secure - secure cookies are only sent by the browser of a HTTPS connection (recommended)
67## HttpOnly - httponly cookies are not readable by javascript (recommended)
68## HttpOnly - httponly cookies are not readable by javascript (recommended)
68# cookie_name = "_oauth2_proxy"
69# cookie_name = "_oauth2_proxy"
69# cookie_secret = ""
70# cookie_secret = ""
cookies.go+17−7
@@ -15,29 +15,39 @@ import (
15 "time"
15 "time"
16)
16)
17
17
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) {
19 // value, timestamp, sig
19 // value, timestamp, sig
20 parts := strings.Split(cookie.Value, "|")
20 parts := strings.Split(cookie.Value, "|")
21 if len(parts) != 3 {
21 if len(parts) != 3 {
22− return "", time.Unix(0, 0), false
22+ return
23 }
23 }
24 sig := cookieSignature(seed, cookie.Name, parts[0], parts[1])
24 sig := cookieSignature(seed, cookie.Name, parts[0], parts[1])
25 if checkHmac(parts[2], sig) {
25 if checkHmac(parts[2], sig) {
26 ts, err := strconv.Atoi(parts[1])
26 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)) {
28 // it's a valid cookie. now get the contents
36 // it's a valid cookie. now get the contents
29 rawValue, err := base64.URLEncoding.DecodeString(parts[0])
37 rawValue, err := base64.URLEncoding.DecodeString(parts[0])
30 if err == nil {
38 if err == nil {
31− return string(rawValue), time.Unix(int64(ts), 0), true
39+ value = string(rawValue)
40+ ok = true
41+ return
32 }
42 }
33 }
43 }
34 }
44 }
35− return "", time.Unix(0, 0), false
45+ return
36}
46}
37
47
38−func signedCookieValue(seed string, key string, value string) string {
48+func signedCookieValue(seed string, key string, value string, now time.Time) string {
39 encodedValue := base64.URLEncoding.EncodeToString([]byte(value))
49 encodedValue := base64.URLEncoding.EncodeToString([]byte(value))
40− timeStr := fmt.Sprintf("%d", time.Now().Unix())
50+ timeStr := fmt.Sprintf("%d", now.Unix())
41 sig := cookieSignature(seed, key, encodedValue, timeStr)
51 sig := cookieSignature(seed, key, encodedValue, timeStr)
42 cookieVal := fmt.Sprintf("%s|%s|%s", encodedValue, timeStr, sig)
52 cookieVal := fmt.Sprintf("%s|%s|%s", encodedValue, timeStr, sig)
43 return cookieVal
53 return cookieVal
oauthproxy.go+15−13
@@ -109,8 +109,12 @@ func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy {
109 if domain == "" {
109 if domain == "" {
110 domain = "<default>"
110 domain = "<default>"
111 }
111 }
112+ refresh := "disabled"
113+ if opts.CookieRefresh != time.Duration(0) {
114+ refresh = fmt.Sprintf("after %s", opts.CookieRefresh)
115+ }
112
116
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)
114
118
115 var aes_cipher cipher.Block
119 var aes_cipher cipher.Block
116 if opts.PassAccessToken || (opts.CookieRefresh != time.Duration(0)) {
120 if opts.PassAccessToken || (opts.CookieRefresh != time.Duration(0)) {
@@ -191,7 +195,7 @@ func (p *OauthProxy) redeemCode(host, code string) (string, string, error) {
191 return access_token, email, nil
195 return access_token, email, nil
192}
196}
193
197
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 {
195 domain := req.Host
199 domain := req.Host
196 if h, _, err := net.SplitHostPort(domain); err == nil {
200 if h, _, err := net.SplitHostPort(domain); err == nil {
197 domain = h
201 domain = h
@@ -204,7 +208,7 @@ func (p *OauthProxy) MakeCookie(req *http.Request, value string, expiration time
204 }
208 }
205
209
206 if value != "" {
210 if value != "" {
207− value = signedCookieValue(p.CookieSeed, p.CookieName, value)
211+ value = signedCookieValue(p.CookieSeed, p.CookieName, value, now)
208 }
212 }
209
213
210 return &http.Cookie{
214 return &http.Cookie{
@@ -214,16 +218,16 @@ func (p *OauthProxy) MakeCookie(req *http.Request, value string, expiration time
214 Domain: domain,
218 Domain: domain,
215 HttpOnly: p.CookieHttpOnly,
219 HttpOnly: p.CookieHttpOnly,
216 Secure: p.CookieSecure,
220 Secure: p.CookieSecure,
217− Expires: time.Now().Add(expiration),
221+ Expires: now.Add(expiration),
218 }
222 }
219}
223}
220
224
221func (p *OauthProxy) ClearCookie(rw http.ResponseWriter, req *http.Request) {
225func (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()))
223}
227}
224
228
225func (p *OauthProxy) SetCookie(rw http.ResponseWriter, req *http.Request, val string) {
229func (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()))
227}
231}
228
232
229func (p *OauthProxy) ProcessCookie(rw http.ResponseWriter, req *http.Request) (email, user, access_token string, ok bool) {
233func (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
231 var timestamp time.Time
235 var timestamp time.Time
232 cookie, err := req.Cookie(p.CookieName)
236 cookie, err := req.Cookie(p.CookieName)
233 if err == nil {
237 if err == nil {
234− value, timestamp, ok = validateCookie(cookie, p.CookieSeed)
238+ value, timestamp, ok = validateCookie(cookie, p.CookieSeed, p.CookieExpire)
235 if ok {
239 if ok {
236− email, user, access_token, err = parseCookieValue(
240+ email, user, access_token, err = parseCookieValue(value, p.AesCipher)
237− value, p.AesCipher)
238 }
241 }
239 }
242 }
240 if err != nil {
243 if err != nil {
241 log.Printf(err.Error())
244 log.Printf(err.Error())
242 ok = false
245 ok = false
243− } else if p.CookieRefresh != time.Duration(0) {
246+ } else if ok && p.CookieRefresh != time.Duration(0) {
244− expires := timestamp.Add(p.CookieExpire)
247+ refresh := timestamp.Add(p.CookieRefresh)
245− refresh_threshold := time.Now().Add(p.CookieRefresh)
248+ if refresh.Before(time.Now()) {
246− if refresh_threshold.Unix() > expires.Unix() {
247 ok = p.Validator(email) && p.provider.ValidateToken(access_token)
249 ok = p.Validator(email) && p.provider.ValidateToken(access_token)
248 if ok {
250 if ok {
249 p.SetCookie(rw, req, value)
251 p.SetCookie(rw, req, value)
oauthproxy_test.go+52−18
@@ -348,13 +348,12 @@ func NewProcessCookieTest(opts ProcessCookieTestOpts) *ProcessCookieTest {
348
348
349 pc_test.opts = NewOptions()
349 pc_test.opts = NewOptions()
350 pc_test.opts.Upstreams = append(pc_test.opts.Upstreams, "unused")
350 pc_test.opts.Upstreams = append(pc_test.opts.Upstreams, "unused")
351− pc_test.opts.CookieSecret = "foobar"
352 pc_test.opts.ClientID = "bazquux"
351 pc_test.opts.ClientID = "bazquux"
353 pc_test.opts.ClientSecret = "xyzzyplugh"
352 pc_test.opts.ClientSecret = "xyzzyplugh"
354 pc_test.opts.CookieSecret = "0123456789abcdef"
353 pc_test.opts.CookieSecret = "0123456789abcdef"
355 // First, set the CookieRefresh option so proxy.AesCipher is created,
354 // First, set the CookieRefresh option so proxy.AesCipher is created,
356 // needed to encrypt the access_token.
355 // needed to encrypt the access_token.
357− pc_test.opts.CookieRefresh = time.Duration(24) * time.Hour
356+ pc_test.opts.CookieRefresh = time.Hour
358 pc_test.opts.Validate()
357 pc_test.opts.Validate()
359
358
360 pc_test.proxy = NewOauthProxy(pc_test.opts, func(email string) bool {
359 pc_test.proxy = NewOauthProxy(pc_test.opts, func(email string) bool {
@@ -379,14 +378,13 @@ func NewProcessCookieTestWithDefaults() *ProcessCookieTest {
379 })
378 })
380}
379}
381
380
382−func (p *ProcessCookieTest) MakeCookie(value, access_token string) *http.Cookie {
381+func (p *ProcessCookieTest) MakeCookie(value, access_token string, ref time.Time) *http.Cookie {
383− cookie_value, _ := buildCookieValue(
384− value, p.proxy.AesCipher, access_token)
382+ cookie_value, _ := buildCookieValue(value, p.proxy.AesCipher, access_token)
385− return p.proxy.MakeCookie(p.req, cookie_value, p.opts.CookieExpire)
383+ return p.proxy.MakeCookie(p.req, cookie_value, p.opts.CookieExpire, ref)
386}
384}
387
385
388func (p *ProcessCookieTest) AddCookie(value, access_token string) {
386func (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()))
390}
388}
391
389
392func (p *ProcessCookieTest) ProcessCookie() (email, user, access_token string, ok bool) {
390func (p *ProcessCookieTest) ProcessCookie() (email, user, access_token string, ok bool) {
@@ -416,7 +414,7 @@ func TestProcessCookieFailIfParsingCookieValueFails(t *testing.T) {
416 pc_test.proxy.AesCipher, "my_access_token")
414 pc_test.proxy.AesCipher, "my_access_token")
417 pc_test.req.AddCookie(pc_test.proxy.MakeCookie(
415 pc_test.req.AddCookie(pc_test.proxy.MakeCookie(
418 pc_test.req, value+"some bogus bytes",
416 pc_test.req, value+"some bogus bytes",
419− pc_test.opts.CookieExpire))
417+ pc_test.opts.CookieExpire, time.Now()))
420 _, _, _, ok := pc_test.ProcessCookie()
418 _, _, _, ok := pc_test.ProcessCookie()
421 assert.Equal(t, false, ok)
419 assert.Equal(t, false, ok)
422}
420}
@@ -424,7 +422,8 @@ func TestProcessCookieFailIfParsingCookieValueFails(t *testing.T) {
424func TestProcessCookieRefreshNotSet(t *testing.T) {
422func TestProcessCookieRefreshNotSet(t *testing.T) {
425 pc_test := NewProcessCookieTestWithDefaults()
423 pc_test := NewProcessCookieTestWithDefaults()
426 pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour
424 pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour
425+ reference := time.Now().Add(time.Duration(-2) * time.Hour)
427− cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "")
426+ cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "", reference)
428 pc_test.req.AddCookie(cookie)
427 pc_test.req.AddCookie(cookie)
429
428
430 _, _, _, ok := pc_test.ProcessCookie()
429 _, _, _, ok := pc_test.ProcessCookie()
@@ -435,10 +434,11 @@ func TestProcessCookieRefreshNotSet(t *testing.T) {
435func TestProcessCookieRefresh(t *testing.T) {
434func TestProcessCookieRefresh(t *testing.T) {
436 pc_test := NewProcessCookieTestWithDefaults()
435 pc_test := NewProcessCookieTestWithDefaults()
437 pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour
436 pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour
437+ reference := time.Now().Add(time.Duration(-2) * time.Hour)
438− cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token")
438+ cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token", reference)
439 pc_test.req.AddCookie(cookie)
439 pc_test.req.AddCookie(cookie)
440
440
441− pc_test.proxy.CookieRefresh = time.Duration(24) * time.Hour
441+ pc_test.proxy.CookieRefresh = time.Hour
442 _, _, _, ok := pc_test.ProcessCookie()
442 _, _, _, ok := pc_test.ProcessCookie()
443 assert.Equal(t, true, ok)
443 assert.Equal(t, true, ok)
444 assert.NotEqual(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"])
444 assert.NotEqual(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"])
@@ -446,25 +446,58 @@ func TestProcessCookieRefresh(t *testing.T) {
446
446
447func TestProcessCookieRefreshThresholdNotCrossed(t *testing.T) {
447func TestProcessCookieRefreshThresholdNotCrossed(t *testing.T) {
448 pc_test := NewProcessCookieTestWithDefaults()
448 pc_test := NewProcessCookieTestWithDefaults()
449− pc_test.proxy.CookieExpire = time.Duration(25) * time.Hour
449+ pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour
450+ reference := time.Now().Add(time.Duration(-30) * time.Minute)
450− cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token")
451+ cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token", reference)
451 pc_test.req.AddCookie(cookie)
452 pc_test.req.AddCookie(cookie)
452
453
453− pc_test.proxy.CookieRefresh = time.Duration(24) * time.Hour
454+ pc_test.proxy.CookieRefresh = time.Hour
454 _, _, _, ok := pc_test.ProcessCookie()
455 _, _, _, ok := pc_test.ProcessCookie()
455 assert.Equal(t, true, ok)
456 assert.Equal(t, true, ok)
456 assert.Equal(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"])
457 assert.Equal(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"])
457}
458}
458
459
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+
459func TestProcessCookieFailIfRefreshSetAndTokenNoLongerValid(t *testing.T) {
491func TestProcessCookieFailIfRefreshSetAndTokenNoLongerValid(t *testing.T) {
460 pc_test := NewProcessCookieTest(ProcessCookieTestOpts{
492 pc_test := NewProcessCookieTest(ProcessCookieTestOpts{
461 provider_validate_cookie_response: false,
493 provider_validate_cookie_response: false,
462 })
494 })
463 pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour
495 pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour
496+ reference := time.Now().Add(time.Duration(-24) * time.Hour)
464− cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token")
497+ cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token", reference)
465 pc_test.req.AddCookie(cookie)
498 pc_test.req.AddCookie(cookie)
466
499
467− pc_test.proxy.CookieRefresh = time.Duration(24) * time.Hour
500+ pc_test.proxy.CookieRefresh = time.Hour
468 _, _, _, ok := pc_test.ProcessCookie()
501 _, _, _, ok := pc_test.ProcessCookie()
469 assert.Equal(t, false, ok)
502 assert.Equal(t, false, ok)
470 assert.Equal(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"])
503 assert.Equal(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"])
@@ -475,10 +508,11 @@ func TestProcessCookieFailIfRefreshSetAndUserNoLongerValid(t *testing.T) {
475 pc_test.validate_user = false
508 pc_test.validate_user = false
476
509
477 pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour
510 pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour
511+ reference := time.Now().Add(time.Duration(-2) * time.Hour)
478− cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token")
512+ cookie := pc_test.MakeCookie("michael.bland@gsa.gov", "my_access_token", reference)
479 pc_test.req.AddCookie(cookie)
513 pc_test.req.AddCookie(cookie)
480
514
481− pc_test.proxy.CookieRefresh = time.Duration(24) * time.Hour
515+ pc_test.proxy.CookieRefresh = time.Hour
482 _, _, _, ok := pc_test.ProcessCookie()
516 _, _, _, ok := pc_test.ProcessCookie()
483 assert.Equal(t, false, ok)
517 assert.Equal(t, false, ok)
484 assert.Equal(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"])
518 assert.Equal(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"])
providers/internal_util.go+17−6
@@ -1,23 +1,34 @@
1package providers
1package providers
2
2
3import (
3import (
4− "github.com/bitly/oauth2_proxy/api"
4+ "io/ioutil"
5 "log"
5 "log"
6 "net/http"
6 "net/http"
7+ "net/url"
8+
9+ "github.com/bitly/oauth2_proxy/api"
7)
10)
8
11
9func validateToken(p Provider, access_token string, header http.Header) bool {
12func validateToken(p Provider, access_token string, header http.Header) bool {
10 if access_token == "" || p.Data().ValidateUrl == nil {
13 if access_token == "" || p.Data().ValidateUrl == nil {
11 return false
14 return false
12 }
15 }
13− url := p.Data().ValidateUrl.String()
16+ endpoint := p.Data().ValidateUrl.String()
14 if len(header) == 0 {
17 if len(header) == 0 {
15− url = url + "?access_token=" + access_token
18+ params := url.Values{"access_token": {access_token}}
19+ endpoint = endpoint + "?" + params.Encode()
16 }
20 }
17− if resp, err := api.RequestUnparsedResponse(url, header); err != nil {
21+ resp, err := api.RequestUnparsedResponse(endpoint, header)
22+ if err != nil {
18 log.Printf("token validation request failed: %s", err)
23 log.Printf("token validation request failed: %s", err)
19 return false
24 return false
20− } else {
21− return resp.StatusCode == 200
22 }
25 }
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
23}
34}