git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge pull request #148 from pusher/proxy-session-store

Proxy session store

commit10e240c8bfce5311c56c272e47908b95be38c01a authorJoel Speed <Joel.speed@hotmail.co.uk> dateMon 20 May 2019, 12:55 (7 years ago) parent17e97abd40a616
15 files changed+226−275
CHANGELOG.md+1 oauthproxy.go+16−175 oauthproxy_test.go+67−45 options.go+23−1 pkg/apis/options/sessions.go+6−1 pkg/apis/sessions/session_state.go+20−1 pkg/apis/sessions/session_state_test.go+27−2 pkg/cookies/cookies.go+7 pkg/sessions/cookie/session_store.go+24−45 pkg/sessions/session_store.go+1−1 pkg/sessions/session_store_test.go+28−3 providers/google.go+1 providers/logingov.go+1 providers/oidc.go+2 providers/provider_default.go+2−1
CHANGELOG.md+1
@@ -10,6 +10,7 @@
1010
1111## Changes since v3.2.0
1212
13+- [#148](https://github.com/pusher/outh2_proxy/pull/148) Implement SessionStore interface within proxy (@JoelSpeed)
1314- [#147](https://github.com/pusher/outh2_proxy/pull/147) Add SessionStore interfaces and initial implementation (@JoelSpeed)
1415 - Allows for multiple different session storage implementations including client and server side
1516 - Adds tests suite for interface to ensure consistency across implementations
oauthproxy.go+16−175
@@ -16,7 +16,7 @@ import (
1616 "github.com/mbland/hmacauth"
1717 "github.com/pusher/oauth2_proxy/cookie"
1818 "github.com/pusher/oauth2_proxy/logger"
19− "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
19+ sessionsapi "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
2020 "github.com/pusher/oauth2_proxy/providers"
2121 "github.com/yhat/wsutil"
2222)
@@ -29,10 +29,6 @@ const (
2929 httpScheme = "http"
3030 httpsScheme = "https"
3131
32− // Cookies are limited to 4kb including the length of the cookie name,
33− // the cookie name can be up to 256 bytes
34− maxCookieLength = 3840
35−
3632 applicationJSON = "application/json"
3733)
3834
@@ -75,6 +71,7 @@ type OAuthProxy struct {
7571 redirectURL *url.URL // the url to receive requests at
7672 whitelistDomains []string
7773 provider providers.Provider
74+ sessionStore sessionsapi.SessionStore
7875 ProxyPrefix string
7976 SignInMessage string
8077 HtpasswdFile *HtpasswdFile
@@ -88,7 +85,6 @@ type OAuthProxy struct {
8885 PassAccessToken bool
8986 SetAuthorization bool
9087 PassAuthorization bool
91− CookieCipher *cookie.Cipher
9288 skipAuthRegex []string
9389 skipAuthPreflight bool
9490 compiledRegex []*regexp.Regexp
@@ -218,15 +214,6 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
218214
219215 logger.Printf("Cookie settings: name:%s secure(https):%v httponly:%v expiry:%s domain:%s path:%s refresh:%s", opts.CookieName, opts.CookieSecure, opts.CookieHTTPOnly, opts.CookieExpire, opts.CookieDomain, opts.CookiePath, refresh)
220216
221− var cipher *cookie.Cipher
222− if opts.PassAccessToken || opts.SetAuthorization || opts.PassAuthorization || (opts.CookieRefresh != time.Duration(0)) {
223− var err error
224− cipher, err = cookie.NewCipher(secretBytes(opts.CookieSecret))
225− if err != nil {
226− logger.Fatal("cookie-secret error: ", err)
227− }
228− }
229−
230217 return &OAuthProxy{
231218 CookieName: opts.CookieName,
232219 CSRFCookieName: fmt.Sprintf("%v_%v", opts.CookieName, "csrf"),
@@ -249,6 +236,7 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
249236
250237 ProxyPrefix: opts.ProxyPrefix,
251238 provider: opts.provider,
239+ sessionStore: opts.sessionStore,
252240 serveMux: serveMux,
253241 redirectURL: redirectURL,
254242 whitelistDomains: opts.WhitelistDomains,
@@ -263,7 +251,6 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
263251 SetAuthorization: opts.SetAuthorization,
264252 PassAuthorization: opts.PassAuthorization,
265253 SkipProviderButton: opts.SkipProviderButton,
266− CookieCipher: cipher,
267254 templates: loadTemplates(opts.CustomTemplatesDir),
268255 Footer: opts.Footer,
269256 }
@@ -293,7 +280,7 @@ func (p *OAuthProxy) displayCustomLoginForm() bool {
293280 return p.HtpasswdFile != nil && p.DisplayHtpasswdForm
294281}
295282
296−func (p *OAuthProxy) redeemCode(host, code string) (s *sessions.SessionState, err error) {
283+func (p *OAuthProxy) redeemCode(host, code string) (s *sessionsapi.SessionState, err error) {
297284 if code == "" {
298285 return nil, errors.New("missing code")
299286 }
@@ -316,104 +303,6 @@ func (p *OAuthProxy) redeemCode(host, code string) (s *sessions.SessionState, er
316303 return
317304}
318305
319−// MakeSessionCookie creates an http.Cookie containing the authenticated user's
320−// authentication details
321−func (p *OAuthProxy) MakeSessionCookie(req *http.Request, value string, expiration time.Duration, now time.Time) []*http.Cookie {
322− if value != "" {
323− value = cookie.SignedValue(p.CookieSeed, p.CookieName, value, now)
324− }
325− c := p.makeCookie(req, p.CookieName, value, expiration, now)
326− if len(c.Value) > 4096-len(p.CookieName) {
327− return splitCookie(c)
328− }
329− return []*http.Cookie{c}
330−}
331−
332−func copyCookie(c *http.Cookie) *http.Cookie {
333− return &http.Cookie{
334− Name: c.Name,
335− Value: c.Value,
336− Path: c.Path,
337− Domain: c.Domain,
338− Expires: c.Expires,
339− RawExpires: c.RawExpires,
340− MaxAge: c.MaxAge,
341− Secure: c.Secure,
342− HttpOnly: c.HttpOnly,
343− Raw: c.Raw,
344− Unparsed: c.Unparsed,
345− }
346−}
347−
348−// splitCookie reads the full cookie generated to store the session and splits
349−// it into a slice of cookies which fit within the 4kb cookie limit indexing
350−// the cookies from 0
351−func splitCookie(c *http.Cookie) []*http.Cookie {
352− if len(c.Value) < maxCookieLength {
353− return []*http.Cookie{c}
354− }
355− cookies := []*http.Cookie{}
356− valueBytes := []byte(c.Value)
357− count := 0
358− for len(valueBytes) > 0 {
359− new := copyCookie(c)
360− new.Name = fmt.Sprintf("%s_%d", c.Name, count)
361− count++
362− if len(valueBytes) < maxCookieLength {
363− new.Value = string(valueBytes)
364− valueBytes = []byte{}
365− } else {
366− newValue := valueBytes[:maxCookieLength]
367− valueBytes = valueBytes[maxCookieLength:]
368− new.Value = string(newValue)
369− }
370− cookies = append(cookies, new)
371− }
372− return cookies
373−}
374−
375−// joinCookies takes a slice of cookies from the request and reconstructs the
376−// full session cookie
377−func joinCookies(cookies []*http.Cookie) (*http.Cookie, error) {
378− if len(cookies) == 0 {
379− return nil, fmt.Errorf("list of cookies must be > 0")
380− }
381− if len(cookies) == 1 {
382− return cookies[0], nil
383− }
384− c := copyCookie(cookies[0])
385− for i := 1; i < len(cookies); i++ {
386− c.Value += cookies[i].Value
387− }
388− c.Name = strings.TrimRight(c.Name, "_0")
389− return c, nil
390−}
391−
392−// loadCookie retreieves the sessions state cookie from the http request.
393−// If a single cookie is present this will be returned, otherwise it attempts
394−// to reconstruct a cookie split up by splitCookie
395−func loadCookie(req *http.Request, cookieName string) (*http.Cookie, error) {
396− c, err := req.Cookie(cookieName)
397− if err == nil {
398− return c, nil
399− }
400− cookies := []*http.Cookie{}
401− err = nil
402− count := 0
403− for err == nil {
404− var c *http.Cookie
405− c, err = req.Cookie(fmt.Sprintf("%s_%d", cookieName, count))
406− if err == nil {
407− cookies = append(cookies, c)
408− count++
409− }
410− }
411− if len(cookies) == 0 {
412− return nil, fmt.Errorf("Could not find cookie %s", cookieName)
413− }
414− return joinCookies(cookies)
415−}
416−
417306// MakeCSRFCookie creates a cookie for CSRF
418307func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
419308 return p.makeCookie(req, p.CSRFCookieName, value, expiration, now)
@@ -454,66 +343,18 @@ func (p *OAuthProxy) SetCSRFCookie(rw http.ResponseWriter, req *http.Request, va
454343
455344// ClearSessionCookie creates a cookie to unset the user's authentication cookie
456345// stored in the user's session
457−func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) {
458− var cookies []*http.Cookie
459−
460− // matches CookieName, CookieName_<number>
461− var cookieNameRegex = regexp.MustCompile(fmt.Sprintf("^%s(_\\d+)?$", p.CookieName))
462−
463− for _, c := range req.Cookies() {
464− if cookieNameRegex.MatchString(c.Name) {
465− clearCookie := p.makeCookie(req, c.Name, "", time.Hour*-1, time.Now())
466−
467− http.SetCookie(rw, clearCookie)
468− cookies = append(cookies, clearCookie)
469− }
470− }
471−
472− // ugly hack because default domain changed
473− if p.CookieDomain == "" && len(cookies) > 0 {
474− clr2 := *cookies[0]
475− clr2.Domain = req.Host
476− http.SetCookie(rw, &clr2)
477− }
478−}
479−
480−// SetSessionCookie adds the user's session cookie to the response
481−func (p *OAuthProxy) SetSessionCookie(rw http.ResponseWriter, req *http.Request, val string) {
482− for _, c := range p.MakeSessionCookie(req, val, p.CookieExpire, time.Now()) {
483− http.SetCookie(rw, c)
484− }
346+func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) error {
347+ return p.sessionStore.Clear(rw, req)
485348}
486349
487350// LoadCookiedSession reads the user's authentication details from the request
488−func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*sessions.SessionState, time.Duration, error) {
489− var age time.Duration
490− c, err := loadCookie(req, p.CookieName)
491− if err != nil {
492− // always http.ErrNoCookie
493− return nil, age, fmt.Errorf("Cookie %q not present", p.CookieName)
494− }
495− val, timestamp, ok := cookie.Validate(c, p.CookieSeed, p.CookieExpire)
496− if !ok {
497− return nil, age, errors.New("Cookie Signature not valid")
498− }
499−
500− session, err := p.provider.SessionFromCookie(val, p.CookieCipher)
501− if err != nil {
502− return nil, age, err
503− }
504−
505− age = time.Now().Truncate(time.Second).Sub(timestamp)
506− return session, age, nil
351+func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*sessionsapi.SessionState, error) {
352+ return p.sessionStore.Load(req)
507353}
508354
509355// SaveSession creates a new session cookie value and sets this on the response
510−func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *sessions.SessionState) error {
511− value, err := p.provider.CookieForSession(s, p.CookieCipher)
512− if err != nil {
513− return err
514− }
515− p.SetSessionCookie(rw, req, value)
516− return nil
356+func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *sessionsapi.SessionState) error {
357+ return p.sessionStore.Save(rw, req, s)
517358}
518359
519360// RobotsTxt disallows scraping pages from the OAuthProxy
@@ -694,7 +535,7 @@ func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
694535
695536 user, ok := p.ManualSignIn(rw, req)
696537 if ok {
697− session := &sessions.SessionState{User: user}
538+ session := &sessionsapi.SessionState{User: user}
698539 p.SaveSession(rw, req, session)
699540 http.Redirect(rw, req, redirect, 302)
700541 } else {
@@ -833,12 +674,12 @@ func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int
833674 var saveSession, clearSession, revalidated bool
834675 remoteAddr := getRemoteAddr(req)
835676
836− session, sessionAge, err := p.LoadCookiedSession(req)
677+ session, err := p.LoadCookiedSession(req)
837678 if err != nil {
838679 logger.Printf("Error loading cookied session: %s", err)
839680 }
840− if session != nil && sessionAge > p.CookieRefresh && p.CookieRefresh != time.Duration(0) {
841− logger.Printf("Refreshing %s old session cookie for %s (refresh after %s)", sessionAge, session, p.CookieRefresh)
681+ if session != nil && session.Age() > p.CookieRefresh && p.CookieRefresh != time.Duration(0) {
682+ logger.Printf("Refreshing %s old session cookie for %s (refresh after %s)", session.Age(), session, p.CookieRefresh)
842683 saveSession = true
843684 }
844685
@@ -945,7 +786,7 @@ func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int
945786
946787// CheckBasicAuth checks the requests Authorization header for basic auth
947788// credentials and authenticates these against the proxies HtpasswdFile
948−func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*sessions.SessionState, error) {
789+func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*sessionsapi.SessionState, error) {
949790 if p.HtpasswdFile == nil {
950791 return nil, nil
951792 }
@@ -967,7 +808,7 @@ func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*sessions.SessionState,
967808 }
968809 if p.HtpasswdFile.Validate(pair[0], pair[1]) {
969810 logger.PrintAuthf(pair[0], req, logger.AuthSuccess, "Authenticated via basic auth and HTpasswd File")
970− return &sessions.SessionState{User: pair[0]}, nil
811+ return &sessionsapi.SessionState{User: pair[0]}, nil
971812 }
972813 logger.PrintAuthf(pair[0], req, logger.AuthFailure, "Invalid authentication via basic auth: not in Htpasswd File")
973814 return nil, nil
oauthproxy_test.go+67−45
@@ -17,6 +17,7 @@ import (
1717 "github.com/mbland/hmacauth"
1818 "github.com/pusher/oauth2_proxy/logger"
1919 "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
20+ "github.com/pusher/oauth2_proxy/pkg/sessions/cookie"
2021 "github.com/pusher/oauth2_proxy/providers"
2122 "github.com/stretchr/testify/assert"
2223 "github.com/stretchr/testify/require"
@@ -600,10 +601,15 @@ type ProcessCookieTestOpts struct {
600601 providerValidateCookieResponse bool
601602}
602603
603−func NewProcessCookieTest(opts ProcessCookieTestOpts) *ProcessCookieTest {
604+type OptionsModifier func(*Options)
605+
606+func NewProcessCookieTest(opts ProcessCookieTestOpts, modifiers ...OptionsModifier) *ProcessCookieTest {
604607 var pcTest ProcessCookieTest
605608
606609 pcTest.opts = NewOptions()
610+ for _, modifier := range modifiers {
611+ modifier(pcTest.opts)
612+ }
607613 pcTest.opts.ClientID = "bazquux"
608614 pcTest.opts.ClientSecret = "xyzzyplugh"
609615 pcTest.opts.CookieSecret = "0123456789abcdefabcd"
@@ -634,32 +640,34 @@ func NewProcessCookieTestWithDefaults() *ProcessCookieTest {
634640 })
635641}
636642
637−func (p *ProcessCookieTest) MakeCookie(value string, ref time.Time) []*http.Cookie {
638− return p.proxy.MakeSessionCookie(p.req, value, p.opts.CookieExpire, ref)
643+func NewProcessCookieTestWithOptionsModifiers(modifiers ...OptionsModifier) *ProcessCookieTest {
644+ return NewProcessCookieTest(ProcessCookieTestOpts{
645+ providerValidateCookieResponse: true,
646+ }, modifiers...)
639647}
640648
641−func (p *ProcessCookieTest) SaveSession(s *sessions.SessionState, ref time.Time) error {
642− value, err := p.proxy.provider.CookieForSession(s, p.proxy.CookieCipher)
649+func (p *ProcessCookieTest) SaveSession(s *sessions.SessionState) error {
650+ err := p.proxy.SaveSession(p.rw, p.req, s)
643651 if err != nil {
644652 return err
645653 }
646− for _, c := range p.proxy.MakeSessionCookie(p.req, value, p.proxy.CookieExpire, ref) {
647− p.req.AddCookie(c)
654+ for _, cookie := range p.rw.Result().Cookies() {
655+ p.req.AddCookie(cookie)
648656 }
649657 return nil
650658}
651659
652−func (p *ProcessCookieTest) LoadCookiedSession() (*sessions.SessionState, time.Duration, error) {
660+func (p *ProcessCookieTest) LoadCookiedSession() (*sessions.SessionState, error) {
653661 return p.proxy.LoadCookiedSession(p.req)
654662}
655663
656664func TestLoadCookiedSession(t *testing.T) {
657665 pcTest := NewProcessCookieTestWithDefaults()
658666
659− startSession := &sessions.SessionState{Email: "john.doe@example.com", AccessToken: "my_access_token"}
660− pcTest.SaveSession(startSession, time.Now())
667+ startSession := &sessions.SessionState{Email: "john.doe@example.com", AccessToken: "my_access_token", CreatedAt: time.Now()}
668+ pcTest.SaveSession(startSession)
661669
662− session, _, err := pcTest.LoadCookiedSession()
670+ session, err := pcTest.LoadCookiedSession()
663671 assert.Equal(t, nil, err)
664672 assert.Equal(t, startSession.Email, session.Email)
665673 assert.Equal(t, "john.doe@example.com", session.User)
@@ -669,7 +677,7 @@ func TestLoadCookiedSession(t *testing.T) {
669677func TestProcessCookieNoCookieError(t *testing.T) {
670678 pcTest := NewProcessCookieTestWithDefaults()
671679
672− session, _, err := pcTest.LoadCookiedSession()
680+ session, err := pcTest.LoadCookiedSession()
673681 assert.Equal(t, "Cookie \"_oauth2_proxy\" not present", err.Error())
674682 if session != nil {
675683 t.Errorf("expected nil session. got %#v", session)
@@ -677,29 +685,31 @@ func TestProcessCookieNoCookieError(t *testing.T) {
677685}
678686
679687func TestProcessCookieRefreshNotSet(t *testing.T) {
680− pcTest := NewProcessCookieTestWithDefaults()
681− pcTest.proxy.CookieExpire = time.Duration(23) * time.Hour
688+ pcTest := NewProcessCookieTestWithOptionsModifiers(func(opts *Options) {
689+ opts.CookieExpire = time.Duration(23) * time.Hour
690+ })
682691 reference := time.Now().Add(time.Duration(-2) * time.Hour)
683692
684− startSession := &sessions.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"}
685− pcTest.SaveSession(startSession, reference)
693+ startSession := &sessions.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: reference}
694+ pcTest.SaveSession(startSession)
686695
687− session, age, err := pcTest.LoadCookiedSession()
696+ session, err := pcTest.LoadCookiedSession()
688697 assert.Equal(t, nil, err)
689− if age < time.Duration(-2)*time.Hour {
690− t.Errorf("cookie too young %v", age)
698+ if session.Age() < time.Duration(-2)*time.Hour {
699+ t.Errorf("cookie too young %v", session.Age())
691700 }
692701 assert.Equal(t, startSession.Email, session.Email)
693702}
694703
695704func TestProcessCookieFailIfCookieExpired(t *testing.T) {
696− pcTest := NewProcessCookieTestWithDefaults()
697− pcTest.proxy.CookieExpire = time.Duration(24) * time.Hour
705+ pcTest := NewProcessCookieTestWithOptionsModifiers(func(opts *Options) {
706+ opts.CookieExpire = time.Duration(24) * time.Hour
707+ })
698708 reference := time.Now().Add(time.Duration(25) * time.Hour * -1)
699− startSession := &sessions.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"}
700− pcTest.SaveSession(startSession, reference)
709+ startSession := &sessions.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: reference}
710+ pcTest.SaveSession(startSession)
701711
702− session, _, err := pcTest.LoadCookiedSession()
712+ session, err := pcTest.LoadCookiedSession()
703713 assert.NotEqual(t, nil, err)
704714 if session != nil {
705715 t.Errorf("expected nil session %#v", session)
@@ -707,22 +717,23 @@ func TestProcessCookieFailIfCookieExpired(t *testing.T) {
707717}
708718
709719func TestProcessCookieFailIfRefreshSetAndCookieExpired(t *testing.T) {
710− pcTest := NewProcessCookieTestWithDefaults()
711− pcTest.proxy.CookieExpire = time.Duration(24) * time.Hour
720+ pcTest := NewProcessCookieTestWithOptionsModifiers(func(opts *Options) {
721+ opts.CookieExpire = time.Duration(24) * time.Hour
722+ })
712723 reference := time.Now().Add(time.Duration(25) * time.Hour * -1)
713− startSession := &sessions.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"}
714− pcTest.SaveSession(startSession, reference)
724+ startSession := &sessions.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: reference}
725+ pcTest.SaveSession(startSession)
715726
716727 pcTest.proxy.CookieRefresh = time.Hour
717− session, _, err := pcTest.LoadCookiedSession()
728+ session, err := pcTest.LoadCookiedSession()
718729 assert.NotEqual(t, nil, err)
719730 if session != nil {
720731 t.Errorf("expected nil session %#v", session)
721732 }
722733}
723734
724−func NewAuthOnlyEndpointTest() *ProcessCookieTest {
725− pcTest := NewProcessCookieTestWithDefaults()
735+func NewAuthOnlyEndpointTest(modifiers ...OptionsModifier) *ProcessCookieTest {
736+ pcTest := NewProcessCookieTestWithOptionsModifiers(modifiers...)
726737 pcTest.req, _ = http.NewRequest("GET",
727738 pcTest.opts.ProxyPrefix+"/auth", nil)
728739 return pcTest
@@ -731,8 +742,8 @@ func NewAuthOnlyEndpointTest() *ProcessCookieTest {
731742func TestAuthOnlyEndpointAccepted(t *testing.T) {
732743 test := NewAuthOnlyEndpointTest()
733744 startSession := &sessions.SessionState{
734− Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"}
735− test.SaveSession(startSession, time.Now())
745+ Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: time.Now()}
746+ test.SaveSession(startSession)
736747
737748 test.proxy.ServeHTTP(test.rw, test.req)
738749 assert.Equal(t, http.StatusAccepted, test.rw.Code)
@@ -750,12 +761,13 @@ func TestAuthOnlyEndpointUnauthorizedOnNoCookieSetError(t *testing.T) {
750761}
751762
752763func TestAuthOnlyEndpointUnauthorizedOnExpiration(t *testing.T) {
753− test := NewAuthOnlyEndpointTest()
754− test.proxy.CookieExpire = time.Duration(24) * time.Hour
764+ test := NewAuthOnlyEndpointTest(func(opts *Options) {
765+ opts.CookieExpire = time.Duration(24) * time.Hour
766+ })
755767 reference := time.Now().Add(time.Duration(25) * time.Hour * -1)
756768 startSession := &sessions.SessionState{
757− Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"}
758− test.SaveSession(startSession, reference)
769+ Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: reference}
770+ test.SaveSession(startSession)
759771
760772 test.proxy.ServeHTTP(test.rw, test.req)
761773 assert.Equal(t, http.StatusUnauthorized, test.rw.Code)
@@ -766,8 +778,8 @@ func TestAuthOnlyEndpointUnauthorizedOnExpiration(t *testing.T) {
766778func TestAuthOnlyEndpointUnauthorizedOnEmailValidationFailure(t *testing.T) {
767779 test := NewAuthOnlyEndpointTest()
768780 startSession := &sessions.SessionState{
769− Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"}
770− test.SaveSession(startSession, time.Now())
781+ Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: time.Now()}
782+ test.SaveSession(startSession)
771783 test.validateUser = false
772784
773785 test.proxy.ServeHTTP(test.rw, test.req)
@@ -797,8 +809,8 @@ func TestAuthOnlyEndpointSetXAuthRequestHeaders(t *testing.T) {
797809 pcTest.opts.ProxyPrefix+"/auth", nil)
798810
799811 startSession := &sessions.SessionState{
800− User: "oauth_user", Email: "oauth_user@example.com", AccessToken: "oauth_token"}
801− pcTest.SaveSession(startSession, time.Now())
812+ User: "oauth_user", Email: "oauth_user@example.com", AccessToken: "oauth_token", CreatedAt: time.Now()}
813+ pcTest.SaveSession(startSession)
802814
803815 pcTest.proxy.ServeHTTP(pcTest.rw, pcTest.req)
804816 assert.Equal(t, http.StatusAccepted, pcTest.rw.Code)
@@ -930,11 +942,11 @@ func (st *SignatureTest) MakeRequestWithExpectedKey(method, body, key string) {
930942
931943 state := &sessions.SessionState{
932944 Email: "mbland@acm.org", AccessToken: "my_access_token"}
933− value, err := proxy.provider.CookieForSession(state, proxy.CookieCipher)
945+ err = proxy.SaveSession(st.rw, req, state)
934946 if err != nil {
935947 panic(err)
936948 }
937− for _, c := range proxy.MakeSessionCookie(req, value, proxy.CookieExpire, time.Now()) {
949+ for _, c := range st.rw.Result().Cookies() {
938950 req.AddCookie(c)
939951 }
940952 // This is used by the upstream to validate the signature.
@@ -1068,7 +1080,12 @@ func TestAjaxForbiddendRequest(t *testing.T) {
10681080}
10691081
10701082func TestClearSplitCookie(t *testing.T) {
1071− p := OAuthProxy{CookieName: "oauth2", CookieDomain: "abc"}
1083+ opts := NewOptions()
1084+ opts.CookieName = "oauth2"
1085+ opts.CookieDomain = "abc"
1086+ store, err := cookie.NewCookieSessionStore(&opts.SessionOptions, &opts.CookieOptions)
1087+ assert.Equal(t, err, nil)
1088+ p := OAuthProxy{CookieName: opts.CookieName, CookieDomain: opts.CookieDomain, sessionStore: store}
10721089 var rw = httptest.NewRecorder()
10731090 req := httptest.NewRequest("get", "/", nil)
10741091
@@ -1092,7 +1109,12 @@ func TestClearSplitCookie(t *testing.T) {
10921109}
10931110
10941111func TestClearSingleCookie(t *testing.T) {
1095− p := OAuthProxy{CookieName: "oauth2", CookieDomain: "abc"}
1112+ opts := NewOptions()
1113+ opts.CookieName = "oauth2"
1114+ opts.CookieDomain = "abc"
1115+ store, err := cookie.NewCookieSessionStore(&opts.SessionOptions, &opts.CookieOptions)
1116+ assert.Equal(t, err, nil)
1117+ p := OAuthProxy{CookieName: opts.CookieName, CookieDomain: opts.CookieDomain, sessionStore: store}
10961118 var rw = httptest.NewRecorder()
10971119 req := httptest.NewRequest("get", "/", nil)
10981120
options.go+23−1
@@ -17,8 +17,11 @@ import (
1717 oidc "github.com/coreos/go-oidc"
1818 "github.com/dgrijalva/jwt-go"
1919 "github.com/mbland/hmacauth"
20+ "github.com/pusher/oauth2_proxy/cookie"
2021 "github.com/pusher/oauth2_proxy/logger"
2122 "github.com/pusher/oauth2_proxy/pkg/apis/options"
23+ sessionsapi "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
24+ "github.com/pusher/oauth2_proxy/pkg/sessions"
2225 "github.com/pusher/oauth2_proxy/providers"
2326 "gopkg.in/natefinch/lumberjack.v2"
2427)
@@ -111,6 +114,7 @@ type Options struct {
111114 proxyURLs []*url.URL
112115 CompiledRegex []*regexp.Regexp
113116 provider providers.Provider
117+ sessionStore sessionsapi.SessionStore
114118 signatureData *SignatureData
115119 oidcVerifier *oidc.IDTokenVerifier
116120}
@@ -136,6 +140,9 @@ func NewOptions() *Options {
136140 CookieExpire: time.Duration(168) * time.Hour,
137141 CookieRefresh: time.Duration(0),
138142 },
143+ SessionOptions: options.SessionOptions{
144+ Type: "cookie",
145+ },
139146 SetXAuthRequest: false,
140147 SkipAuthPreflight: false,
141148 PassBasicAuth: true,
@@ -261,7 +268,8 @@ func (o *Options) Validate() error {
261268 }
262269 msgs = parseProviderInfo(o, msgs)
263270
264− if o.PassAccessToken || (o.CookieRefresh != time.Duration(0)) {
271+ var cipher *cookie.Cipher
272+ if o.PassAccessToken || o.SetAuthorization || o.PassAuthorization || (o.CookieRefresh != time.Duration(0)) {
265273 validCookieSecretSize := false
266274 for _, i := range []int{16, 24, 32} {
267275 if len(secretBytes(o.CookieSecret)) == i {
@@ -283,9 +291,23 @@ func (o *Options) Validate() error {
283291 "pass_access_token == true or "+
284292 "cookie_refresh != 0, but is %d bytes.%s",
285293 len(secretBytes(o.CookieSecret)), suffix))
294+ } else {
295+ var err error
296+ cipher, err = cookie.NewCipher(secretBytes(o.CookieSecret))
297+ if err != nil {
298+ msgs = append(msgs, fmt.Sprintf("cookie-secret error: %v", err))
299+ }
286300 }
287301 }
288302
303+ o.SessionOptions.Cipher = cipher
304+ sessionStore, err := sessions.NewSessionStore(&o.SessionOptions, &o.CookieOptions)
305+ if err != nil {
306+ msgs = append(msgs, fmt.Sprintf("error initialising session storage: %v", err))
307+ } else {
308+ o.sessionStore = sessionStore
309+ }
310+
289311 if o.CookieRefresh >= o.CookieExpire {
290312 msgs = append(msgs, fmt.Sprintf(
291313 "cookie_refresh (%s) must be less than "+
pkg/apis/options/sessions.go+6−1
@@ -1,8 +1,13 @@
11package options
22
3+import (
4+ "github.com/pusher/oauth2_proxy/cookie"
5+)
6+
37// SessionOptions contains configuration options for the SessionStore providers.
48type SessionOptions struct {
5− Type string `flag:"session-store-type" cfg:"session_store_type" env:"OAUTH2_PROXY_SESSION_STORE_TYPE"`
9+ Type string `flag:"session-store-type" cfg:"session_store_type" env:"OAUTH2_PROXY_SESSION_STORE_TYPE"`
10+ Cipher *cookie.Cipher
611 CookieStoreOptions
712}
813
pkg/apis/sessions/session_state.go+20−1
@@ -14,6 +14,7 @@ import (
1414type SessionState struct {
1515 AccessToken string `json:",omitempty"`
1616 IDToken string `json:",omitempty"`
17+ CreatedAt time.Time `json:"-"`
1718 ExpiresOn time.Time `json:"-"`
1819 RefreshToken string `json:",omitempty"`
1920 Email string `json:",omitempty"`
@@ -23,6 +24,7 @@ type SessionState struct {
2324// SessionStateJSON is used to encode SessionState into JSON without exposing time.Time zero value
2425type SessionStateJSON struct {
2526 *SessionState
27+ CreatedAt *time.Time `json:",omitempty"`
2628 ExpiresOn *time.Time `json:",omitempty"`
2729}
2830
@@ -34,6 +36,14 @@ func (s *SessionState) IsExpired() bool {
3436 return false
3537}
3638
39+// Age returns the age of a session
40+func (s *SessionState) Age() time.Duration {
41+ if !s.CreatedAt.IsZero() {
42+ return time.Now().Truncate(time.Second).Sub(s.CreatedAt)
43+ }
44+ return 0
45+}
46+
3747// String constructs a summary of the session state
3848func (s *SessionState) String() string {
3949 o := fmt.Sprintf("Session{email:%s user:%s", s.Email, s.User)
@@ -43,6 +53,9 @@ func (s *SessionState) String() string {
4353 if s.IDToken != "" {
4454 o += " id_token:true"
4555 }
56+ if !s.CreatedAt.IsZero() {
57+ o += fmt.Sprintf(" created:%s", s.CreatedAt)
58+ }
4659 if !s.ExpiresOn.IsZero() {
4760 o += fmt.Sprintf(" expires:%s", s.ExpiresOn)
4861 }
@@ -95,6 +108,9 @@ func (s *SessionState) EncodeSessionState(c *cookie.Cipher) (string, error) {
95108 }
96109 // Embed SessionState and ExpiresOn pointer into SessionStateJSON
97110 ssj := &SessionStateJSON{SessionState: &ss}
111+ if !ss.CreatedAt.IsZero() {
112+ ssj.CreatedAt = &ss.CreatedAt
113+ }
98114 if !ss.ExpiresOn.IsZero() {
99115 ssj.ExpiresOn = &ss.ExpiresOn
100116 }
@@ -165,8 +181,11 @@ func DecodeSessionState(v string, c *cookie.Cipher) (*SessionState, error) {
165181 var ss *SessionState
166182 err := json.Unmarshal([]byte(v), &ssj)
167183 if err == nil && ssj.SessionState != nil {
168− // Extract SessionState and ExpiresOn value from SessionStateJSON
184+ // Extract SessionState and CreatedAt,ExpiresOn value from SessionStateJSON
169185 ss = ssj.SessionState
186+ if ssj.CreatedAt != nil {
187+ ss.CreatedAt = *ssj.CreatedAt
188+ }
170189 if ssj.ExpiresOn != nil {
171190 ss.ExpiresOn = *ssj.ExpiresOn
172191 }
pkg/apis/sessions/session_state_test.go+27−2
@@ -22,6 +22,7 @@ func TestSessionStateSerialization(t *testing.T) {
2222 Email: "user@domain.com",
2323 AccessToken: "token1234",
2424 IDToken: "rawtoken1234",
25+ CreatedAt: time.Now(),
2526 ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
2627 RefreshToken: "refresh4321",
2728 }
@@ -35,6 +36,7 @@ func TestSessionStateSerialization(t *testing.T) {
3536 assert.Equal(t, s.Email, ss.Email)
3637 assert.Equal(t, s.AccessToken, ss.AccessToken)
3738 assert.Equal(t, s.IDToken, ss.IDToken)
39+ assert.Equal(t, s.CreatedAt.Unix(), ss.CreatedAt.Unix())
3840 assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())
3941 assert.Equal(t, s.RefreshToken, ss.RefreshToken)
4042
@@ -44,6 +46,7 @@ func TestSessionStateSerialization(t *testing.T) {
4446 assert.Equal(t, nil, err)
4547 assert.NotEqual(t, "user@domain.com", ss.User)
4648 assert.NotEqual(t, s.Email, ss.Email)
49+ assert.Equal(t, s.CreatedAt.Unix(), ss.CreatedAt.Unix())
4750 assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())
4851 assert.NotEqual(t, s.AccessToken, ss.AccessToken)
4952 assert.NotEqual(t, s.IDToken, ss.IDToken)
@@ -59,6 +62,7 @@ func TestSessionStateSerializationWithUser(t *testing.T) {
5962 User: "just-user",
6063 Email: "user@domain.com",
6164 AccessToken: "token1234",
65+ CreatedAt: time.Now(),
6266 ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
6367 RefreshToken: "refresh4321",
6468 }
@@ -71,6 +75,7 @@ func TestSessionStateSerializationWithUser(t *testing.T) {
7175 assert.Equal(t, s.User, ss.User)
7276 assert.Equal(t, s.Email, ss.Email)
7377 assert.Equal(t, s.AccessToken, ss.AccessToken)
78+ assert.Equal(t, s.CreatedAt.Unix(), ss.CreatedAt.Unix())
7479 assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())
7580 assert.Equal(t, s.RefreshToken, ss.RefreshToken)
7681
@@ -80,6 +85,7 @@ func TestSessionStateSerializationWithUser(t *testing.T) {
8085 assert.Equal(t, nil, err)
8186 assert.NotEqual(t, s.User, ss.User)
8287 assert.NotEqual(t, s.Email, ss.Email)
88+ assert.Equal(t, s.CreatedAt.Unix(), ss.CreatedAt.Unix())
8389 assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())
8490 assert.NotEqual(t, s.AccessToken, ss.AccessToken)
8591 assert.NotEqual(t, s.RefreshToken, ss.RefreshToken)
@@ -89,6 +95,7 @@ func TestSessionStateSerializationNoCipher(t *testing.T) {
8995 s := &sessions.SessionState{
9096 Email: "user@domain.com",
9197 AccessToken: "token1234",
98+ CreatedAt: time.Now(),
9299 ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
93100 RefreshToken: "refresh4321",
94101 }
@@ -109,6 +116,7 @@ func TestSessionStateSerializationNoCipherWithUser(t *testing.T) {
109116 User: "just-user",
110117 Email: "user@domain.com",
111118 AccessToken: "token1234",
119+ CreatedAt: time.Now(),
112120 ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
113121 RefreshToken: "refresh4321",
114122 }
@@ -147,6 +155,7 @@ type testCase struct {
147155// Currently only tests without cipher here because we have no way to mock
148156// the random generator used in EncodeSessionState.
149157func TestEncodeSessionState(t *testing.T) {
158+ c := time.Now()
150159 e := time.Now().Add(time.Duration(1) * time.Hour)
151160
152161 testCases := []testCase{
@@ -163,6 +172,7 @@ func TestEncodeSessionState(t *testing.T) {
163172 User: "just-user",
164173 AccessToken: "token1234",
165174 IDToken: "rawtoken1234",
175+ CreatedAt: c,
166176 ExpiresOn: e,
167177 RefreshToken: "refresh4321",
168178 },
@@ -185,6 +195,9 @@ func TestEncodeSessionState(t *testing.T) {
185195
186196// TestDecodeSessionState testssessions.DecodeSessionState with the test vector
187197func TestDecodeSessionState(t *testing.T) {
198+ created := time.Now()
199+ createdJSON, _ := created.MarshalJSON()
200+ createdString := string(createdJSON)
188201 e := time.Now().Add(time.Duration(1) * time.Hour)
189202 eJSON, _ := e.MarshalJSON()
190203 eString := string(eJSON)
@@ -219,7 +232,7 @@ func TestDecodeSessionState(t *testing.T) {
219232 Email: "user@domain.com",
220233 User: "just-user",
221234 },
222− Encoded: fmt.Sprintf(`{"Email":"user@domain.com","User":"just-user","AccessToken":"I6s+ml+/MldBMgHIiC35BTKTh57skGX24w==","IDToken":"xojNdyyjB1HgYWh6XMtXY/Ph5eCVxa1cNsklJw==","RefreshToken":"qEX0x6RmASxo4dhlBG6YuRs9Syn/e9sHu/+K","ExpiresOn":%s}`, eString),
235+ Encoded: fmt.Sprintf(`{"Email":"user@domain.com","User":"just-user","AccessToken":"I6s+ml+/MldBMgHIiC35BTKTh57skGX24w==","IDToken":"xojNdyyjB1HgYWh6XMtXY/Ph5eCVxa1cNsklJw==","RefreshToken":"qEX0x6RmASxo4dhlBG6YuRs9Syn/e9sHu/+K","CreatedAt":%s,"ExpiresOn":%s}`, createdString, eString),
223236 },
224237 {
225238 SessionState: sessions.SessionState{
@@ -227,10 +240,11 @@ func TestDecodeSessionState(t *testing.T) {
227240 User: "just-user",
228241 AccessToken: "token1234",
229242 IDToken: "rawtoken1234",
243+ CreatedAt: created,
230244 ExpiresOn: e,
231245 RefreshToken: "refresh4321",
232246 },
233− Encoded: fmt.Sprintf(`{"Email":"FsKKYrTWZWrxSOAqA/fTNAUZS5QWCqOBjuAbBlbVOw==","User":"rT6JP3dxQhxUhkWrrd7yt6c1mDVyQCVVxw==","AccessToken":"I6s+ml+/MldBMgHIiC35BTKTh57skGX24w==","IDToken":"xojNdyyjB1HgYWh6XMtXY/Ph5eCVxa1cNsklJw==","RefreshToken":"qEX0x6RmASxo4dhlBG6YuRs9Syn/e9sHu/+K","ExpiresOn":%s}`, eString),
247+ Encoded: fmt.Sprintf(`{"Email":"FsKKYrTWZWrxSOAqA/fTNAUZS5QWCqOBjuAbBlbVOw==","User":"rT6JP3dxQhxUhkWrrd7yt6c1mDVyQCVVxw==","AccessToken":"I6s+ml+/MldBMgHIiC35BTKTh57skGX24w==","IDToken":"xojNdyyjB1HgYWh6XMtXY/Ph5eCVxa1cNsklJw==","RefreshToken":"qEX0x6RmASxo4dhlBG6YuRs9Syn/e9sHu/+K","CreatedAt":%s,"ExpiresOn":%s}`, createdString, eString),
234248 Cipher: c,
235249 },
236250 {
@@ -316,3 +330,14 @@ func TestDecodeSessionState(t *testing.T) {
316330 }
317331 }
318332}
333+
334+func TestSessionStateAge(t *testing.T) {
335+ ss := &sessions.SessionState{}
336+
337+ // Created at unset so should be 0
338+ assert.Equal(t, time.Duration(0), ss.Age())
339+
340+ // Set CreatedAt to 1 hour ago
341+ ss.CreatedAt = time.Now().Add(-1 * time.Hour)
342+ assert.Equal(t, time.Hour, ss.Age().Round(time.Minute))
343+}
pkg/cookies/cookies.go+7
@@ -7,6 +7,7 @@ import (
77 "time"
88
99 "github.com/pusher/oauth2_proxy/logger"
10+ "github.com/pusher/oauth2_proxy/pkg/apis/options"
1011)
1112
1213// MakeCookie constructs a cookie from the given parameters,
@@ -32,3 +33,9 @@ func MakeCookie(req *http.Request, name string, value string, path string, domai
3233 Expires: now.Add(expiration),
3334 }
3435}
36+
37+// MakeCookieFromOptions constructs a cookie based on the givemn *options.CookieOptions,
38+// value and creation time
39+func MakeCookieFromOptions(req *http.Request, name string, value string, opts *options.CookieOptions, expiration time.Duration, now time.Time) *http.Cookie {
40+ return MakeCookie(req, name, value, opts.CookiePath, opts.CookieDomain, opts.CookieHTTPOnly, opts.CookieSecure, expiration, now)
41+}
pkg/sessions/cookie/session_store.go+24−45
@@ -27,36 +27,33 @@ var _ sessions.SessionStore = &SessionStore{}
2727// SessionStore is an implementation of the sessions.SessionStore
2828// interface that stores sessions in client side cookies
2929type SessionStore struct {
30− CookieCipher *cookie.Cipher
31− CookieDomain string
32− CookieExpire time.Duration
33− CookieHTTPOnly bool
34− CookieName string
35− CookiePath string
36− CookieSecret string
37− CookieSecure bool
30+ CookieOptions *options.CookieOptions
31+ CookieCipher *cookie.Cipher
3832}
3933
4034// Save takes a sessions.SessionState and stores the information from it
4135// within Cookies set on the HTTP response writer
4236func (s *SessionStore) Save(rw http.ResponseWriter, req *http.Request, ss *sessions.SessionState) error {
37+ if ss.CreatedAt.IsZero() {
38+ ss.CreatedAt = time.Now()
39+ }
4340 value, err := utils.CookieForSession(ss, s.CookieCipher)
4441 if err != nil {
4542 return err
4643 }
47− s.setSessionCookie(rw, req, value)
44+ s.setSessionCookie(rw, req, value, ss.CreatedAt)
4845 return nil
4946}
5047
5148// Load reads sessions.SessionState information from Cookies within the
5249// HTTP request object
5350func (s *SessionStore) Load(req *http.Request) (*sessions.SessionState, error) {
54− c, err := loadCookie(req, s.CookieName)
51+ c, err := loadCookie(req, s.CookieOptions.CookieName)
5552 if err != nil {
5653 // always http.ErrNoCookie
57− return nil, fmt.Errorf("Cookie %q not present", s.CookieName)
54+ return nil, fmt.Errorf("Cookie %q not present", s.CookieOptions.CookieName)
5855 }
59− val, _, ok := cookie.Validate(c, s.CookieSecret, s.CookieExpire)
56+ val, _, ok := cookie.Validate(c, s.CookieOptions.CookieSecret, s.CookieOptions.CookieExpire)
6057 if !ok {
6158 return nil, errors.New("Cookie Signature not valid")
6259 }
@@ -74,11 +71,11 @@ func (s *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) error {
7471 var cookies []*http.Cookie
7572
7673 // matches CookieName, CookieName_<number>
77− var cookieNameRegex = regexp.MustCompile(fmt.Sprintf("^%s(_\\d+)?$", s.CookieName))
74+ var cookieNameRegex = regexp.MustCompile(fmt.Sprintf("^%s(_\\d+)?$", s.CookieOptions.CookieName))
7875
7976 for _, c := range req.Cookies() {
8077 if cookieNameRegex.MatchString(c.Name) {
81− clearCookie := s.makeCookie(req, c.Name, "", time.Hour*-1)
78+ clearCookie := s.makeCookie(req, c.Name, "", time.Hour*-1, time.Now())
8279
8380 http.SetCookie(rw, clearCookie)
8481 cookies = append(cookies, clearCookie)
@@ -89,60 +86,42 @@ func (s *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) error {
8986}
9087
9188// setSessionCookie adds the user's session cookie to the response
92−func (s *SessionStore) setSessionCookie(rw http.ResponseWriter, req *http.Request, val string) {
93− for _, c := range s.makeSessionCookie(req, val, s.CookieExpire, time.Now()) {
89+func (s *SessionStore) setSessionCookie(rw http.ResponseWriter, req *http.Request, val string, created time.Time) {
90+ for _, c := range s.makeSessionCookie(req, val, created) {
9491 http.SetCookie(rw, c)
9592 }
9693}
9794
9895// makeSessionCookie creates an http.Cookie containing the authenticated user's
9996// authentication details
100−func (s *SessionStore) makeSessionCookie(req *http.Request, value string, expiration time.Duration, now time.Time) []*http.Cookie {
97+func (s *SessionStore) makeSessionCookie(req *http.Request, value string, now time.Time) []*http.Cookie {
10198 if value != "" {
102− value = cookie.SignedValue(s.CookieSecret, s.CookieName, value, now)
99+ value = cookie.SignedValue(s.CookieOptions.CookieSecret, s.CookieOptions.CookieName, value, now)
103100 }
104− c := s.makeCookie(req, s.CookieName, value, expiration)
105− if len(c.Value) > 4096-len(s.CookieName) {
101+ c := s.makeCookie(req, s.CookieOptions.CookieName, value, s.CookieOptions.CookieExpire, now)
102+ if len(c.Value) > 4096-len(s.CookieOptions.CookieName) {
106103 return splitCookie(c)
107104 }
108105 return []*http.Cookie{c}
109106}
110107
111−func (s *SessionStore) makeCookie(req *http.Request, name string, value string, expiration time.Duration) *http.Cookie {
112− return cookies.MakeCookie(
108+func (s *SessionStore) makeCookie(req *http.Request, name string, value string, expiration time.Duration, now time.Time) *http.Cookie {
109+ return cookies.MakeCookieFromOptions(
113110 req,
114111 name,
115112 value,
116− s.CookiePath,
117− s.CookieDomain,
118− s.CookieHTTPOnly,
119− s.CookieSecure,
113+ s.CookieOptions,
120114 expiration,
121− time.Now(),
115+ now,
122116 )
123117}
124118
125119// NewCookieSessionStore initialises a new instance of the SessionStore from
126120// the configuration given
127−func NewCookieSessionStore(opts options.CookieStoreOptions, cookieOpts *options.CookieOptions) (sessions.SessionStore, error) {
128− var cipher *cookie.Cipher
129− if len(cookieOpts.CookieSecret) > 0 {
130− var err error
131− cipher, err = cookie.NewCipher(utils.SecretBytes(cookieOpts.CookieSecret))
132− if err != nil {
133− return nil, fmt.Errorf("unable to create cipher: %v", err)
134− }
135− }
136−
121+func NewCookieSessionStore(opts *options.SessionOptions, cookieOpts *options.CookieOptions) (sessions.SessionStore, error) {
137122 return &SessionStore{
138− CookieCipher: cipher,
139− CookieDomain: cookieOpts.CookieDomain,
140− CookieExpire: cookieOpts.CookieExpire,
141− CookieHTTPOnly: cookieOpts.CookieHTTPOnly,
142− CookieName: cookieOpts.CookieName,
143− CookiePath: cookieOpts.CookiePath,
144− CookieSecret: cookieOpts.CookieSecret,
145− CookieSecure: cookieOpts.CookieSecure,
123+ CookieCipher: opts.Cipher,
124+ CookieOptions: cookieOpts,
146125 }, nil
147126}
148127
pkg/sessions/session_store.go+1−1
@@ -12,7 +12,7 @@ import (
1212func NewSessionStore(opts *options.SessionOptions, cookieOpts *options.CookieOptions) (sessions.SessionStore, error) {
1313 switch opts.Type {
1414 case options.CookieSessionStoreType:
15− return cookie.NewCookieSessionStore(opts.CookieStoreOptions, cookieOpts)
15+ return cookie.NewCookieSessionStore(opts, cookieOpts)
1616 default:
1717 return nil, fmt.Errorf("unknown session store type '%s'", opts.Type)
1818 }
pkg/sessions/session_store_test.go+28−3
@@ -5,16 +5,20 @@ import (
55 "encoding/base64"
66 "net/http"
77 "net/http/httptest"
8+ "strconv"
9+ "strings"
810 "testing"
911 "time"
1012
1113 . "github.com/onsi/ginkgo"
1214 . "github.com/onsi/gomega"
15+ "github.com/pusher/oauth2_proxy/cookie"
1316 "github.com/pusher/oauth2_proxy/pkg/apis/options"
1417 sessionsapi "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
1518 "github.com/pusher/oauth2_proxy/pkg/cookies"
1619 "github.com/pusher/oauth2_proxy/pkg/sessions"
17− "github.com/pusher/oauth2_proxy/pkg/sessions/cookie"
20+ sessionscookie "github.com/pusher/oauth2_proxy/pkg/sessions/cookie"
21+ "github.com/pusher/oauth2_proxy/pkg/sessions/utils"
1822)
1923
2024func TestSessionStore(t *testing.T) {
@@ -72,6 +76,16 @@ var _ = Describe("NewSessionStore", func() {
7276 }
7377 })
7478
79+ It("have a signature timestamp matching session.CreatedAt", func() {
80+ for _, cookie := range cookies {
81+ if cookie.Value != "" {
82+ parts := strings.Split(cookie.Value, "|")
83+ Expect(parts).To(HaveLen(3))
84+ Expect(parts[1]).To(Equal(strconv.Itoa(int(session.CreatedAt.Unix()))))
85+ }
86+ }
87+ })
88+
7589 })
7690 }
7791
@@ -86,6 +100,10 @@ var _ = Describe("NewSessionStore", func() {
86100 Expect(response.Header().Get("set-cookie")).ToNot(BeEmpty())
87101 })
88102
103+ It("Ensures the session CreatedAt is not zero", func() {
104+ Expect(session.CreatedAt.IsZero()).To(BeFalse())
105+ })
106+
89107 CheckCookieOptions()
90108 })
91109
@@ -138,12 +156,15 @@ var _ = Describe("NewSessionStore", func() {
138156
139157 // Can't compare time.Time using Equal() so remove ExpiresOn from sessions
140158 l := *loadedSession
159+ l.CreatedAt = time.Time{}
141160 l.ExpiresOn = time.Time{}
142161 s := *session
162+ s.CreatedAt = time.Time{}
143163 s.ExpiresOn = time.Time{}
144164 Expect(l).To(Equal(s))
145165
146166 // Compare time.Time separately
167+ Expect(loadedSession.CreatedAt.Equal(session.CreatedAt)).To(BeTrue())
147168 Expect(loadedSession.ExpiresOn.Equal(session.ExpiresOn)).To(BeTrue())
148169 }
149170 })
@@ -181,12 +202,16 @@ var _ = Describe("NewSessionStore", func() {
181202 SessionStoreInterfaceTests()
182203 })
183204
184− Context("with a cookie-secret set", func() {
205+ Context("with a cipher", func() {
185206 BeforeEach(func() {
186207 secret := make([]byte, 32)
187208 _, err := rand.Read(secret)
188209 Expect(err).ToNot(HaveOccurred())
189210 cookieOpts.CookieSecret = base64.URLEncoding.EncodeToString(secret)
211+ cipher, err := cookie.NewCipher(utils.SecretBytes(cookieOpts.CookieSecret))
212+ Expect(err).ToNot(HaveOccurred())
213+ Expect(cipher).ToNot(BeNil())
214+ opts.Cipher = cipher
190215
191216 ss, err = sessions.NewSessionStore(opts, cookieOpts)
192217 Expect(err).ToNot(HaveOccurred())
@@ -231,7 +256,7 @@ var _ = Describe("NewSessionStore", func() {
231256 It("creates a cookie.SessionStore", func() {
232257 ss, err := sessions.NewSessionStore(opts, cookieOpts)
233258 Expect(err).NotTo(HaveOccurred())
234− Expect(ss).To(BeAssignableToTypeOf(&cookie.SessionStore{}))
259+ Expect(ss).To(BeAssignableToTypeOf(&sessionscookie.SessionStore{}))
235260 })
236261
237262 Context("the cookie.SessionStore", func() {
providers/google.go+1
@@ -149,6 +149,7 @@ func (p *GoogleProvider) Redeem(redirectURL, code string) (s *sessions.SessionSt
149149 s = &sessions.SessionState{
150150 AccessToken: jsonResponse.AccessToken,
151151 IDToken: jsonResponse.IDToken,
152+ CreatedAt: time.Now(),
152153 ExpiresOn: time.Now().Add(time.Duration(jsonResponse.ExpiresIn) * time.Second).Truncate(time.Second),
153154 RefreshToken: jsonResponse.RefreshToken,
154155 Email: c.Email,
providers/logingov.go+1
@@ -252,6 +252,7 @@ func (p *LoginGovProvider) Redeem(redirectURL, code string) (s *sessions.Session
252252 s = &sessions.SessionState{
253253 AccessToken: jsonResponse.AccessToken,
254254 IDToken: jsonResponse.IDToken,
255+ CreatedAt: time.Now(),
255256 ExpiresOn: time.Now().Add(time.Duration(jsonResponse.ExpiresIn) * time.Second).Truncate(time.Second),
256257 Email: email,
257258 }
providers/oidc.go+2
@@ -87,6 +87,7 @@ func (p *OIDCProvider) redeemRefreshToken(s *sessions.SessionState) (err error)
8787 s.AccessToken = newSession.AccessToken
8888 s.IDToken = newSession.IDToken
8989 s.RefreshToken = newSession.RefreshToken
90+ s.CreatedAt = newSession.CreatedAt
9091 s.ExpiresOn = newSession.ExpiresOn
9192 s.Email = newSession.Email
9293 return
@@ -126,6 +127,7 @@ func (p *OIDCProvider) createSessionState(ctx context.Context, token *oauth2.Tok
126127 AccessToken: token.AccessToken,
127128 IDToken: rawIDToken,
128129 RefreshToken: token.RefreshToken,
130+ CreatedAt: time.Now(),
129131 ExpiresOn: token.Expiry,
130132 Email: claims.Email,
131133 User: claims.Subject,
providers/provider_default.go+2−1
@@ -8,6 +8,7 @@ import (
88 "io/ioutil"
99 "net/http"
1010 "net/url"
11+ "time"
1112
1213 "github.com/pusher/oauth2_proxy/cookie"
1314 "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
@@ -72,7 +73,7 @@ func (p *ProviderData) Redeem(redirectURL, code string) (s *sessions.SessionStat
7273 return
7374 }
7475 if a := v.Get("access_token"); a != "" {
75− s = &sessions.SessionState{AccessToken: a}
76+ s = &sessions.SessionState{AccessToken: a, CreatedAt: time.Now()}
7677 } else {
7778 err = fmt.Errorf("no access token found %s", body)
7879 }