Skip to content
This repository has been archived by the owner on Jan 24, 2019. It is now read-only.

Cookie Refresh Improvements #115

Merged
merged 1 commit into from
Jun 23, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions contrib/oauth2_proxy.cfg.example
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,17 @@
# custom_templates_dir = ""

## Cookie Settings
## Name - the cookie name
## 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_token
## is set
## Domain - (optional) cookie domain to force cookies to (ie: .yourcompany.com)
## Expire - (duration) expire timeframe for cookie
## Refresh - (duration) refresh the cookie when less than this much time remains before
## expiration; should be less than cookie_expire; set to 0 to disable.
## Refresh revalidated the OAuth token to ensure it is still valid. ie: 24h
## Secure - secure cookies are only sent by the browser of a HTTPS connection (recommended)
## Name - the cookie name
## 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_token
## is set
## Domain - (optional) cookie domain to force cookies to (ie: .yourcompany.com)
## Expire - (duration) expire timeframe for cookie
## Refresh - (duration) refresh the cookie when duration has elapsed after cookie was initially set.
## Should be less than cookie_expire; set to 0 to disable.
## On refresh, OAuth token is re-validated.
## (ie: 1h means tokens are refreshed on request 1hr+ after it was set)
## Secure - secure cookies are only sent by the browser of a HTTPS connection (recommended)
## HttpOnly - httponly cookies are not readable by javascript (recommended)
# cookie_name = "_oauth2_proxy"
# cookie_secret = ""
Expand Down
24 changes: 17 additions & 7 deletions cookies.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,39 @@ import (
"time"
)

func validateCookie(cookie *http.Cookie, seed string) (string, time.Time, bool) {
func validateCookie(cookie *http.Cookie, seed string, expiration time.Duration) (value string, t time.Time, ok bool) {
// value, timestamp, sig
parts := strings.Split(cookie.Value, "|")
if len(parts) != 3 {
return "", time.Unix(0, 0), false
return
}
sig := cookieSignature(seed, cookie.Name, parts[0], parts[1])
if checkHmac(parts[2], sig) {
ts, err := strconv.Atoi(parts[1])
if err == nil && int64(ts) > time.Now().Add(time.Duration(24)*7*time.Hour*-1).Unix() {
if err != nil {
return
}
// The expiration timestamp set when the cookie was created
// isn't sent back by the browser. Hence, we check whether the
// creation timestamp stored in the cookie falls within the
// window defined by (Now()-expiration, Now()].
t = time.Unix(int64(ts), 0)
if t.After(time.Now().Add(expiration*-1)) && t.Before(time.Now().Add(time.Minute*5)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure of the utility of t.Before(time.Now().Add(time.Minute*5)). Seems like it will always be true, unless there are clock skew issues between different oauth2_proxy instances running on different machines with the same cookie secret.

// it's a valid cookie. now get the contents
rawValue, err := base64.URLEncoding.DecodeString(parts[0])
if err == nil {
return string(rawValue), time.Unix(int64(ts), 0), true
value = string(rawValue)
ok = true
return
}
}
}
return "", time.Unix(0, 0), false
return
}

func signedCookieValue(seed string, key string, value string) string {
func signedCookieValue(seed string, key string, value string, now time.Time) string {
encodedValue := base64.URLEncoding.EncodeToString([]byte(value))
timeStr := fmt.Sprintf("%d", time.Now().Unix())
timeStr := fmt.Sprintf("%d", now.Unix())
sig := cookieSignature(seed, key, encodedValue, timeStr)
cookieVal := fmt.Sprintf("%s|%s|%s", encodedValue, timeStr, sig)
return cookieVal
Expand Down
28 changes: 15 additions & 13 deletions oauthproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,12 @@ func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy {
if domain == "" {
domain = "<default>"
}
refresh := "disabled"
if opts.CookieRefresh != time.Duration(0) {
refresh = fmt.Sprintf("after %s", opts.CookieRefresh)
}

log.Printf("Cookie settings: name:%s secure(https):%v httponly:%v expiry:%s domain:%s", opts.CookieName, opts.CookieSecure, opts.CookieHttpOnly, opts.CookieExpire, domain)
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)

var aes_cipher cipher.Block
if opts.PassAccessToken || (opts.CookieRefresh != time.Duration(0)) {
Expand Down Expand Up @@ -191,7 +195,7 @@ func (p *OauthProxy) redeemCode(host, code string) (string, string, error) {
return access_token, email, nil
}

func (p *OauthProxy) MakeCookie(req *http.Request, value string, expiration time.Duration) *http.Cookie {
func (p *OauthProxy) MakeCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
domain := req.Host
if h, _, err := net.SplitHostPort(domain); err == nil {
domain = h
Expand All @@ -204,7 +208,7 @@ func (p *OauthProxy) MakeCookie(req *http.Request, value string, expiration time
}

if value != "" {
value = signedCookieValue(p.CookieSeed, p.CookieName, value)
value = signedCookieValue(p.CookieSeed, p.CookieName, value, now)
}

return &http.Cookie{
Expand All @@ -214,36 +218,34 @@ func (p *OauthProxy) MakeCookie(req *http.Request, value string, expiration time
Domain: domain,
HttpOnly: p.CookieHttpOnly,
Secure: p.CookieSecure,
Expires: time.Now().Add(expiration),
Expires: now.Add(expiration),
}
}

func (p *OauthProxy) ClearCookie(rw http.ResponseWriter, req *http.Request) {
http.SetCookie(rw, p.MakeCookie(req, "", time.Duration(1)*time.Hour*-1))
http.SetCookie(rw, p.MakeCookie(req, "", time.Hour*-1, time.Now()))
}

func (p *OauthProxy) SetCookie(rw http.ResponseWriter, req *http.Request, val string) {
http.SetCookie(rw, p.MakeCookie(req, val, p.CookieExpire))
http.SetCookie(rw, p.MakeCookie(req, val, p.CookieExpire, time.Now()))
}

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.CookieName)
if err == nil {
value, timestamp, ok = validateCookie(cookie, p.CookieSeed)
value, timestamp, ok = validateCookie(cookie, p.CookieSeed, p.CookieExpire)
if ok {
email, user, access_token, err = parseCookieValue(
value, p.AesCipher)
email, user, access_token, err = parseCookieValue(value, p.AesCipher)
}
}
if err != nil {
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() > expires.Unix() {
} else if ok && p.CookieRefresh != time.Duration(0) {
refresh := timestamp.Add(p.CookieRefresh)
if refresh.Before(time.Now()) {
ok = p.Validator(email) && p.provider.ValidateToken(access_token)
if ok {
p.SetCookie(rw, req, value)
Expand Down
70 changes: 52 additions & 18 deletions oauthproxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,13 +348,12 @@ func NewProcessCookieTest(opts ProcessCookieTestOpts) *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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

D'oh! Thanks for cleaning up my sloppiness. :-)

pc_test.opts.ClientSecret = "xyzzyplugh"
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.CookieRefresh = time.Hour
pc_test.opts.Validate()

pc_test.proxy = NewOauthProxy(pc_test.opts, func(email string) bool {
Expand All @@ -379,14 +378,13 @@ func NewProcessCookieTestWithDefaults() *ProcessCookieTest {
})
}

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) MakeCookie(value, access_token string, ref time.Time) *http.Cookie {
cookie_value, _ := buildCookieValue(value, p.proxy.AesCipher, access_token)
return p.proxy.MakeCookie(p.req, cookie_value, p.opts.CookieExpire, ref)
}

func (p *ProcessCookieTest) AddCookie(value, access_token string) {
p.req.AddCookie(p.MakeCookie(value, access_token))
p.req.AddCookie(p.MakeCookie(value, access_token, time.Now()))
}

func (p *ProcessCookieTest) ProcessCookie() (email, user, access_token string, ok bool) {
Expand Down Expand Up @@ -416,15 +414,16 @@ func TestProcessCookieFailIfParsingCookieValueFails(t *testing.T) {
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))
pc_test.opts.CookieExpire, time.Now()))
_, _, _, ok := pc_test.ProcessCookie()
assert.Equal(t, false, ok)
}

func TestProcessCookieRefreshNotSet(t *testing.T) {
pc_test := NewProcessCookieTestWithDefaults()
pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour
cookie := pc_test.MakeCookie("[email protected]", "")
reference := time.Now().Add(time.Duration(-2) * time.Hour)
cookie := pc_test.MakeCookie("[email protected]", "", reference)
pc_test.req.AddCookie(cookie)

_, _, _, ok := pc_test.ProcessCookie()
Expand All @@ -435,36 +434,70 @@ func TestProcessCookieRefreshNotSet(t *testing.T) {
func TestProcessCookieRefresh(t *testing.T) {
pc_test := NewProcessCookieTestWithDefaults()
pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour
cookie := pc_test.MakeCookie("[email protected]", "my_access_token")
reference := time.Now().Add(time.Duration(-2) * time.Hour)
cookie := pc_test.MakeCookie("[email protected]", "my_access_token", reference)
pc_test.req.AddCookie(cookie)

pc_test.proxy.CookieRefresh = time.Duration(24) * time.Hour
pc_test.proxy.CookieRefresh = time.Hour
_, _, _, ok := pc_test.ProcessCookie()
assert.Equal(t, true, ok)
assert.NotEqual(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"])
}

func TestProcessCookieRefreshThresholdNotCrossed(t *testing.T) {
pc_test := NewProcessCookieTestWithDefaults()
pc_test.proxy.CookieExpire = time.Duration(25) * time.Hour
cookie := pc_test.MakeCookie("[email protected]", "my_access_token")
pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour
reference := time.Now().Add(time.Duration(-30) * time.Minute)
cookie := pc_test.MakeCookie("[email protected]", "my_access_token", reference)
pc_test.req.AddCookie(cookie)

pc_test.proxy.CookieRefresh = time.Duration(24) * time.Hour
pc_test.proxy.CookieRefresh = time.Hour
_, _, _, ok := pc_test.ProcessCookie()
assert.Equal(t, true, ok)
assert.Equal(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"])
}

func TestProcessCookieFailIfCookieExpired(t *testing.T) {
pc_test := NewProcessCookieTestWithDefaults()
pc_test.proxy.CookieExpire = time.Duration(24) * time.Hour
reference := time.Now().Add(time.Duration(25) * time.Hour * -1)
cookie := pc_test.MakeCookie("[email protected]", "my_access_token", reference)
pc_test.req.AddCookie(cookie)

if _, _, _, ok := pc_test.ProcessCookie(); ok {
t.Error("ProcessCookie() should have failed")
}
if set_cookie := pc_test.rw.HeaderMap["Set-Cookie"]; set_cookie != nil {
t.Error("expected Set-Cookie to be nil, instead was: ", set_cookie)
}
}

func TestProcessCookieFailIfRefreshSetAndCookieExpired(t *testing.T) {
pc_test := NewProcessCookieTestWithDefaults()
pc_test.proxy.CookieExpire = time.Duration(24) * time.Hour
reference := time.Now().Add(time.Duration(25) * time.Hour * -1)
cookie := pc_test.MakeCookie("[email protected]", "my_access_token", reference)
pc_test.req.AddCookie(cookie)

pc_test.proxy.CookieRefresh = time.Hour
if _, _, _, ok := pc_test.ProcessCookie(); ok {
t.Error("ProcessCookie() should have failed")
}
if set_cookie := pc_test.rw.HeaderMap["Set-Cookie"]; set_cookie != nil {
t.Error("expected Set-Cookie to be nil, instead was: ", set_cookie)
}
}

func TestProcessCookieFailIfRefreshSetAndTokenNoLongerValid(t *testing.T) {
pc_test := NewProcessCookieTest(ProcessCookieTestOpts{
provider_validate_cookie_response: false,
})
pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour
cookie := pc_test.MakeCookie("[email protected]", "my_access_token")
reference := time.Now().Add(time.Duration(-24) * time.Hour)
cookie := pc_test.MakeCookie("[email protected]", "my_access_token", reference)
pc_test.req.AddCookie(cookie)

pc_test.proxy.CookieRefresh = time.Duration(24) * time.Hour
pc_test.proxy.CookieRefresh = time.Hour
_, _, _, ok := pc_test.ProcessCookie()
assert.Equal(t, false, ok)
assert.Equal(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"])
Expand All @@ -475,10 +508,11 @@ func TestProcessCookieFailIfRefreshSetAndUserNoLongerValid(t *testing.T) {
pc_test.validate_user = false

pc_test.proxy.CookieExpire = time.Duration(23) * time.Hour
cookie := pc_test.MakeCookie("[email protected]", "my_access_token")
reference := time.Now().Add(time.Duration(-2) * time.Hour)
cookie := pc_test.MakeCookie("[email protected]", "my_access_token", reference)
pc_test.req.AddCookie(cookie)

pc_test.proxy.CookieRefresh = time.Duration(24) * time.Hour
pc_test.proxy.CookieRefresh = time.Hour
_, _, _, ok := pc_test.ProcessCookie()
assert.Equal(t, false, ok)
assert.Equal(t, []string(nil), pc_test.rw.HeaderMap["Set-Cookie"])
Expand Down
23 changes: 17 additions & 6 deletions providers/internal_util.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,34 @@
package providers

import (
"github.com/bitly/oauth2_proxy/api"
"io/ioutil"
"log"
"net/http"
"net/url"

"github.com/bitly/oauth2_proxy/api"
)

func validateToken(p Provider, access_token string, header http.Header) bool {
if access_token == "" || p.Data().ValidateUrl == nil {
return false
}
url := p.Data().ValidateUrl.String()
endpoint := p.Data().ValidateUrl.String()
if len(header) == 0 {
url = url + "?access_token=" + access_token
params := url.Values{"access_token": {access_token}}
endpoint = endpoint + "?" + params.Encode()
}
if resp, err := api.RequestUnparsedResponse(url, header); err != nil {
resp, err := api.RequestUnparsedResponse(endpoint, header)
if err != nil {
log.Printf("token validation request failed: %s", err)
return false
} else {
return resp.StatusCode == 200
}

body, _ := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode == 200 {
return true
}
log.Printf("token validation request failed: status %d - %s", resp.StatusCode, body)
return false
}