From b965f25c1060c4ddd949dae8651e4ffc3140db9e Mon Sep 17 00:00:00 2001 From: Joel Speed Date: Tue, 7 May 2019 12:18:23 +0100 Subject: [PATCH] Implement SaveSession in Cookie SessionStore --- pkg/sessions/cookie/session_store.go | 60 +++++++++++++++++++++++++++- pkg/sessions/session_store_test.go | 26 ++++++++++-- 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/pkg/sessions/cookie/session_store.go b/pkg/sessions/cookie/session_store.go index 36c541e..b4db9ba 100644 --- a/pkg/sessions/cookie/session_store.go +++ b/pkg/sessions/cookie/session_store.go @@ -15,6 +15,12 @@ import ( "github.com/pusher/oauth2_proxy/pkg/sessions/utils" ) +const ( + // Cookies are limited to 4kb including the length of the cookie name, + // the cookie name can be up to 256 bytes + maxCookieLength = 3840 +) + // Ensure CookieSessionStore implements the interface var _ sessions.SessionStore = &SessionStore{} @@ -34,7 +40,12 @@ type SessionStore struct { // SaveSession takes a sessions.SessionState and stores the information from it // within Cookies set on the HTTP response writer func (s *SessionStore) SaveSession(rw http.ResponseWriter, req *http.Request, ss *sessions.SessionState) error { - return fmt.Errorf("method not implemented") + value, err := utils.CookieForSession(ss, s.CookieCipher) + if err != nil { + return err + } + s.setSessionCookie(rw, req, value) + return nil } // LoadSession reads sessions.SessionState information from Cookies within the @@ -77,6 +88,26 @@ func (s *SessionStore) ClearSession(rw http.ResponseWriter, req *http.Request) e return nil } +// setSessionCookie adds the user's session cookie to the response +func (s *SessionStore) setSessionCookie(rw http.ResponseWriter, req *http.Request, val string) { + for _, c := range s.makeSessionCookie(req, val, s.CookieExpire, time.Now()) { + http.SetCookie(rw, c) + } +} + +// makeSessionCookie creates an http.Cookie containing the authenticated user's +// authentication details +func (s *SessionStore) makeSessionCookie(req *http.Request, value string, expiration time.Duration, now time.Time) []*http.Cookie { + if value != "" { + value = cookie.SignedValue(s.CookieSecret, s.CookieName, value, now) + } + c := s.makeCookie(req, s.CookieName, value, expiration) + if len(c.Value) > 4096-len(s.CookieName) { + return splitCookie(c) + } + return []*http.Cookie{c} +} + func (s *SessionStore) makeCookie(req *http.Request, name string, value string, expiration time.Duration) *http.Cookie { return cookies.MakeCookie( req, @@ -115,6 +146,33 @@ func NewCookieSessionStore(opts options.CookieStoreOptions, cookieOpts *options. }, nil } +// splitCookie reads the full cookie generated to store the session and splits +// it into a slice of cookies which fit within the 4kb cookie limit indexing +// the cookies from 0 +func splitCookie(c *http.Cookie) []*http.Cookie { + if len(c.Value) < maxCookieLength { + return []*http.Cookie{c} + } + cookies := []*http.Cookie{} + valueBytes := []byte(c.Value) + count := 0 + for len(valueBytes) > 0 { + new := copyCookie(c) + new.Name = fmt.Sprintf("%s_%d", c.Name, count) + count++ + if len(valueBytes) < maxCookieLength { + new.Value = string(valueBytes) + valueBytes = []byte{} + } else { + newValue := valueBytes[:maxCookieLength] + valueBytes = valueBytes[maxCookieLength:] + new.Value = string(newValue) + } + cookies = append(cookies, new) + } + return cookies +} + // loadCookie retreieves the sessions state cookie from the http request. // If a single cookie is present this will be returned, otherwise it attempts // to reconstruct a cookie split up by splitCookie diff --git a/pkg/sessions/session_store_test.go b/pkg/sessions/session_store_test.go index 75ed0ae..0006ac4 100644 --- a/pkg/sessions/session_store_test.go +++ b/pkg/sessions/session_store_test.go @@ -89,7 +89,7 @@ var _ = Describe("NewSessionStore", func() { }) It("sets a `set-cookie` header in the response", func() { - Expect(response.Header().Get("Set-Cookie")).ToNot(BeEmpty()) + Expect(response.Header().Get("set-cookie")).ToNot(BeEmpty()) }) CheckCookieOptions() @@ -144,7 +144,7 @@ var _ = Describe("NewSessionStore", func() { }) It("sets a `set-cookie` header in the response", func() { - Expect(response.Header().Get("Set-Cookie")).ToNot(BeEmpty()) + Expect(response.Header().Get("set-cookie")).ToNot(BeEmpty()) }) CheckCookieOptions() @@ -152,12 +152,23 @@ var _ = Describe("NewSessionStore", func() { Context("when ClearSession is called", func() { BeforeEach(func() { + cookie := cookies.MakeCookie(request, + cookieOpts.CookieName, + "foo", + cookieOpts.CookiePath, + cookieOpts.CookieDomain, + cookieOpts.CookieHTTPOnly, + cookieOpts.CookieSecure, + cookieOpts.CookieExpire, + time.Now(), + ) + request.AddCookie(cookie) err := ss.ClearSession(response, request) Expect(err).ToNot(HaveOccurred()) }) It("sets a `set-cookie` header in the response", func() { - Expect(response.Header().Get("set-cookie")).ToNot(BeEmpty()) + Expect(response.Header().Get("Set-Cookie")).ToNot(BeEmpty()) }) CheckCookieOptions() @@ -178,6 +189,15 @@ var _ = Describe("NewSessionStore", func() { CookieHTTPOnly: true, } + session = &sessionsapi.SessionState{ + AccessToken: "AccessToken", + IDToken: "IDToken", + ExpiresOn: time.Now().Add(1 * time.Hour), + RefreshToken: "RefreshToken", + Email: "john.doe@example.com", + User: "john.doe", + } + request = httptest.NewRequest("GET", "http://example.com/", nil) response = httptest.NewRecorder() })