File fix-securecookie-value-too-long.patch of Package gangway
diff --git a/Gopkg.lock b/Gopkg.lock
index c36883f..af6de07 100644
--- a/Gopkg.lock
+++ b/Gopkg.lock
@@ -193,6 +193,7 @@
input-imports = [
"github.com/dgrijalva/jwt-go",
"github.com/ghodss/yaml",
+ "github.com/gorilla/securecookie",
"github.com/gorilla/sessions",
"github.com/justinas/alice",
"github.com/kelseyhightower/envconfig",
diff --git a/Makefile b/Makefile
index a467ef5..8f61f96 100644
--- a/Makefile
+++ b/Makefile
@@ -33,7 +33,7 @@ setup:
go get -u github.com/golang/dep/cmd/dep
go get -u github.com/mjibson/esc/...
-check: test vet gofmt staticcheck unused misspell
+check: test vet gofmt staticcheck misspell
deps:
dep ensure -v
@@ -49,11 +49,7 @@ test:
staticcheck:
@go get honnef.co/go/tools/cmd/staticcheck
- staticcheck $(PKGS)
-
-unused:
- @go get honnef.co/go/tools/cmd/unused
- unused -exported $(PKGS)
+ staticcheck -unused.whole-program $(PKGS)
misspell:
@go get github.com/client9/misspell/cmd/misspell
diff --git a/internal/session/session.go b/internal/session/session.go
index 93d3fc8..8c76260 100644
--- a/internal/session/session.go
+++ b/internal/session/session.go
@@ -16,23 +16,21 @@ package session
import (
"crypto/sha256"
- "net/http"
-
- "github.com/gorilla/sessions"
"golang.org/x/crypto/pbkdf2"
+ "net/http"
)
const salt = "MkmfuPNHnZBBivy0L0aW"
// Session defines a Gangway session
type Session struct {
- Session *sessions.CookieStore
+ Session *CustomCookieStore
}
// New inits a Session with CookieStore
func New(sessionSecurityKey string) *Session {
return &Session{
- Session: sessions.NewCookieStore(generateSessionKeys(sessionSecurityKey)),
+ Session: NewCustomCookieStore(generateSessionKeys(sessionSecurityKey)),
}
}
diff --git a/internal/session/store.go b/internal/session/store.go
new file mode 100644
index 0000000..9b930c0
--- /dev/null
+++ b/internal/session/store.go
@@ -0,0 +1,135 @@
+// Copyright © 2019 Heptio
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package session
+
+import (
+ "fmt"
+ "github.com/gorilla/securecookie"
+ "github.com/gorilla/sessions"
+ "net/http"
+)
+
+// The CustomCookieStore automatically splits cookies with length greater than maxCookieLength into multiple smaller cookies.
+// The motivation is the browsers' 4KB limit on cookies, which for instance causes problems for large id_tokens in azure.
+
+const (
+ // Cookies are limited to 4kb including the length of the cookie name,
+ // the cookie name can be up to 256 bytes
+ maxCookieLength = 3840
+)
+
+type CustomCookieStore struct {
+ *sessions.CookieStore
+}
+
+// Set secureCookie maxLength to an arbitrary (20x4kb) high value since we are no longer limited
+func NewCustomCookieStore(keyPairs ...[]byte) *CustomCookieStore {
+ cookieStore := sessions.NewCookieStore(keyPairs...)
+ for _, codec := range cookieStore.Codecs {
+ cookie := codec.(*securecookie.SecureCookie)
+ cookie.MaxLength(81920)
+ }
+ return &CustomCookieStore{cookieStore}
+}
+
+func (s *CustomCookieStore) Get(r *http.Request, name string) (*sessions.Session, error) {
+ return sessions.GetRegistry(r).Get(s, name)
+}
+
+// In contrast to default implementation, the session values can be partitioned into
+// multiple cookies.
+// The original cookie is split/joined in its encoded form
+func (s *CustomCookieStore) New(r *http.Request, name string) (*sessions.Session, error) {
+ session := sessions.NewSession(s, name)
+ opts := *s.Options
+ session.Options = &opts
+ session.IsNew = true
+ cookie := joinSectionCookies(r, name)
+ var err error
+ if len(cookie) > 0 {
+ err = securecookie.DecodeMulti(name, cookie, &session.Values, s.Codecs...)
+ if err == nil {
+ session.IsNew = false
+ }
+ }
+ return session, err
+}
+
+// If the cookie length is > maxCookieLength, its value is split into multiple cookies
+// fitting into the maxCookieLength limit.
+// The resulting section cookies get their index appended to the name.
+func (s *CustomCookieStore) Save(r *http.Request, w http.ResponseWriter,
+ session *sessions.Session) error {
+
+ cookie, err := securecookie.EncodeMulti(session.Name(), session.Values,
+ s.Codecs...)
+ if err != nil {
+ return err
+ }
+
+ sectionCookies := splitCookie(cookie)
+ // With a singular section the name is unchanged
+ if len(sectionCookies) == 1 {
+ cookieName := session.Name()
+ http.SetCookie(w, sessions.NewCookie(cookieName, sectionCookies[0], session.Options))
+ return nil
+ }
+
+ for i, value := range sectionCookies {
+ cookieName := buildSectionCookieName(session.Name(), i)
+ http.SetCookie(w, sessions.NewCookie(cookieName, value, session.Options))
+ }
+ return nil
+}
+
+// joinCookies concatenates the values of all matching cookies and returns the original, encoded cookievalue string.
+func joinSectionCookies(r *http.Request, name string) string {
+
+ // Exact match without index means only a single cookie exists
+ if c, err := r.Cookie(name); err == nil {
+ return c.Value
+ }
+
+ var joinedValue string
+ for i := 0; true; i++ {
+ cookieName := buildSectionCookieName(name, i)
+ if c, err := r.Cookie(cookieName); err == nil {
+ joinedValue += c.Value
+ } else {
+ break
+ }
+ }
+ return joinedValue
+}
+
+// splitCookie splits the original encoded cookie value into a slice of cookies which
+// fit within the 4kb cookie limit indexing the cookies from 0
+func splitCookie(cookieValue string) []string {
+ var sectionCookies []string
+ valueBytes := []byte(cookieValue)
+
+ for len(valueBytes) > 0 {
+ length := len(valueBytes)
+ if length > maxCookieLength {
+ length = maxCookieLength
+ }
+ sectionCookies = append(sectionCookies, string(valueBytes[:length]))
+ valueBytes = valueBytes[length:]
+ }
+ return sectionCookies
+}
+
+func buildSectionCookieName(name string, index int) string {
+ return fmt.Sprintf("%s_%d", name, index)
+}
diff --git a/internal/session/store_test.go b/internal/session/store_test.go
new file mode 100644
index 0000000..c010f8a
--- /dev/null
+++ b/internal/session/store_test.go
@@ -0,0 +1,154 @@
+// Copyright © 2019 Heptio
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package session
+
+import (
+ "fmt"
+ "github.com/gorilla/sessions"
+ log "github.com/sirupsen/logrus"
+ "math"
+ "math/rand"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+)
+
+func TestJoinSectionCookies(t *testing.T) {
+ var originalValue string
+ var value string
+ cookies := buildRandomCookies(2, 3800, "test_%d")
+ buildRequestWithCookies(cookies, func(cookies []*http.Cookie, r *http.Request) {
+ for _, c := range cookies {
+ originalValue += c.Value
+ }
+ value = joinSectionCookies(r, "test")
+ })
+ if value != originalValue {
+ t.Errorf("joinSectionCookies value incorrect: \n value: %s \n originalValue: %s", value, originalValue)
+ }
+}
+
+func TestJoinSectionCookiesSingle(t *testing.T) {
+ var originalValue string
+ var value string
+ cookies := buildRandomCookies(1, 2000, "test_%d")
+ buildRequestWithCookies(cookies, func(cookies []*http.Cookie, r *http.Request) {
+ for _, c := range cookies {
+ originalValue += c.Value
+ }
+ value = joinSectionCookies(r, "test")
+ })
+ if value != originalValue {
+ t.Errorf("joinSectionCookies value incorrect: \n value: %s \n originalValue: %s", value, originalValue)
+ }
+}
+
+func TestSplitCookie(t *testing.T) {
+ cookieLength := 8000
+ originalValue := randStringBytesRmndr(cookieLength)
+ sectionCookies := splitCookie(originalValue)
+ expectedCount := int(math.Ceil((float64(cookieLength) / maxCookieLength)))
+ if len(sectionCookies) != expectedCount {
+ t.Errorf("splitCookie count incorrect: \n count: %d \n expectedCount: %d", len(sectionCookies), expectedCount)
+ }
+ value := strings.Join(sectionCookies, "")
+ if value != originalValue {
+ t.Errorf("splitCookie value incorrect: \n value: %s \n originalValue: %s", value, originalValue)
+ }
+}
+
+func TestSplitCookieSingle(t *testing.T) {
+ cookieLength := 2000
+ originalValue := randStringBytesRmndr(cookieLength)
+ sectionCookies := splitCookie(originalValue)
+ expectedCount := int(math.Ceil((float64(cookieLength) / maxCookieLength)))
+ if len(sectionCookies) != expectedCount {
+ t.Errorf("splitCookie count incorrect: \n count: %d \n expectedCount: %d", len(sectionCookies), expectedCount)
+ }
+}
+
+func TestSplitCookieSize(t *testing.T) {
+ cookieLength := 10000
+ originalValue := randStringBytesRmndr(cookieLength)
+ sectionCookies := splitCookie(originalValue)
+ for _, s := range sectionCookies {
+ if len(s) > maxCookieLength {
+ t.Errorf("sectionCookie length over limit: \n length: %d", len(s))
+ }
+ }
+}
+
+func TestSplitAndJoin(t *testing.T) {
+ cookieLength := 10000
+ originalValue := randStringBytesRmndr(cookieLength)
+ sectionCookies := splitCookie(originalValue)
+ cookies := buildCookiesFromValues(sectionCookies, "test_%d")
+ var value string
+ buildRequestWithCookies(cookies, func(cookies []*http.Cookie, r *http.Request) {
+ value = joinSectionCookies(r, "test")
+ })
+ if value != originalValue {
+ t.Errorf("SplitAndJoin value incorrect: \n value: %s \n originalValue: %s", value, originalValue)
+ }
+}
+
+// Utility
+
+type handleReq func([]*http.Cookie, *http.Request)
+
+func buildRequestWithCookies(cookies []*http.Cookie, fn handleReq) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ for _, cookie := range cookies {
+ r.AddCookie(cookie)
+ }
+ fn(cookies, r)
+ }))
+ defer ts.Close()
+ _, err := http.Get(ts.URL)
+ if err != nil {
+ log.Fatal(err)
+ }
+}
+
+func buildRandomCookies(cookieCount int, cookieLength int, cookieName string) []*http.Cookie {
+ sessionOptions := &sessions.Options{}
+ var cookies []*http.Cookie
+ for i := 0; i < cookieCount; i++ {
+ value := randStringBytesRmndr(cookieLength)
+ cookie := sessions.NewCookie(fmt.Sprintf(cookieName, i), value, sessionOptions)
+ cookies = append(cookies, cookie)
+ }
+ return cookies
+}
+
+func buildCookiesFromValues(values []string, cookieName string) []*http.Cookie {
+ sessionOptions := &sessions.Options{}
+ var cookies []*http.Cookie
+ for i, value := range values {
+ cookie := sessions.NewCookie(fmt.Sprintf(cookieName, i), value, sessionOptions)
+ cookies = append(cookies, cookie)
+ }
+ return cookies
+}
+
+const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+
+func randStringBytesRmndr(n int) string {
+ b := make([]byte, n)
+ for i := range b {
+ b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))]
+ }
+ return string(b)
+}