1
0
mirror of https://github.com/oauth2-proxy/oauth2-proxy.git synced 2025-01-28 05:36:20 +02:00
oauth2-proxy/pkg/sessions/cookie/session_store.go

265 lines
7.8 KiB
Go
Raw Normal View History

package cookie
import (
"errors"
"fmt"
"net/http"
"regexp"
"strings"
"time"
2020-03-29 14:54:36 +01:00
"github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
"github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
pkgcookies "github.com/oauth2-proxy/oauth2-proxy/pkg/cookies"
2020-03-29 14:54:36 +01:00
"github.com/oauth2-proxy/oauth2-proxy/pkg/encryption"
"github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
)
const (
// Cookies are limited to 4kb for all parts
// including the cookie name, value, attributes; IE (http.cookie).String()
// Most browsers' max is 4096 -- but we give ourselves some leeway
maxCookieLength = 4000
)
// Ensure CookieSessionStore implements the interface
var _ sessions.SessionStore = &SessionStore{}
// SessionStore is an implementation of the sessions.SessionStore
// interface that stores sessions in client side cookies
type SessionStore struct {
2020-05-25 12:43:24 +01:00
Cookie *options.Cookie
CookieCipher encryption.Cipher
Minimal bool
}
// Save takes a sessions.SessionState and stores the information from it
// within Cookies set on the HTTP response writer
func (s *SessionStore) Save(rw http.ResponseWriter, req *http.Request, ss *sessions.SessionState) error {
if ss.CreatedAt == nil || ss.CreatedAt.IsZero() {
now := time.Now()
ss.CreatedAt = &now
2019-05-07 16:13:55 +01:00
}
value, err := s.cookieForSession(ss)
if err != nil {
return err
}
s.setSessionCookie(rw, req, value, *ss.CreatedAt)
return nil
}
// Load reads sessions.SessionState information from Cookies within the
// HTTP request object
func (s *SessionStore) Load(req *http.Request) (*sessions.SessionState, error) {
2020-05-25 12:43:24 +01:00
c, err := loadCookie(req, s.Cookie.Name)
if err != nil {
// always http.ErrNoCookie
2020-05-25 12:43:24 +01:00
return nil, fmt.Errorf("cookie %q not present", s.Cookie.Name)
}
2020-05-25 12:43:24 +01:00
val, _, ok := encryption.Validate(c, s.Cookie.Secret, s.Cookie.Expire)
if !ok {
return nil, errors.New("cookie signature not valid")
}
Reduce SessionState size better with MessagePack + LZ4 (#632) * Encode sessions with MsgPack + LZ4 Assumes ciphers are now mandatory per #414. Cookie & Redis sessions can fallback to V5 style JSON in error cases. TODO: session_state.go unit tests & new unit tests for Legacy fallback scenarios. * Only compress encoded sessions with Cookie Store * Cleanup msgpack + lz4 error handling * Change NewBase64Cipher to take in an existing Cipher * Add msgpack & lz4 session state tests * Add required options for oauthproxy tests More aggressively assert.NoError on all validation.Validate(opts) calls to enforce legal options in all our tests. Add additional NoError checks wherever error return values were ignored. * Remove support for uncompressed session state fields * Improve error verbosity & add session state tests * Ensure all marshalled sessions are valid Invalid CFB decryptions can result in garbage data that 1/100 times might cause message pack unmarshal to not fail and instead return an empty session. This adds more rigor to make sure legacy sessions cause appropriate errors. * Add tests for legacy V5 session decoding Refactor common legacy JSON test cases to a legacy helpers area under session store tests. * Make ValidateSession a struct method & add CHANGELOG entry * Improve SessionState error & comments verbosity * Move legacy session test helpers to sessions pkg Placing these helpers under the sessions pkg removed all the circular import uses in housing it under the session store area. * Improve SignatureAuthenticator test helper formatting * Make redis.legacyV5DecodeSession internal * Make LegacyV5TestCase test table public for linter
2020-07-13 12:56:05 -07:00
session, err := sessionFromCookie(val, s.CookieCipher)
if err != nil {
return nil, err
}
return session, nil
}
// Clear clears any saved session information by writing a cookie to
// clear the session
func (s *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) error {
// matches CookieName, CookieName_<number>
2020-05-25 12:43:24 +01:00
var cookieNameRegex = regexp.MustCompile(fmt.Sprintf("^%s(_\\d+)?$", s.Cookie.Name))
for _, c := range req.Cookies() {
if cookieNameRegex.MatchString(c.Name) {
2019-05-07 15:32:46 +01:00
clearCookie := s.makeCookie(req, c.Name, "", time.Hour*-1, time.Now())
http.SetCookie(rw, clearCookie)
}
}
return nil
}
// cookieForSession serializes a session state for storage in a cookie
func (s *SessionStore) cookieForSession(ss *sessions.SessionState) ([]byte, error) {
if s.Minimal && (ss.AccessToken != "" || ss.IDToken != "" || ss.RefreshToken != "") {
minimal := *ss
minimal.AccessToken = ""
minimal.IDToken = ""
minimal.RefreshToken = ""
return minimal.EncodeSessionState(s.CookieCipher, true)
}
return ss.EncodeSessionState(s.CookieCipher, true)
}
// sessionFromCookie deserializes a session from a cookie value
Reduce SessionState size better with MessagePack + LZ4 (#632) * Encode sessions with MsgPack + LZ4 Assumes ciphers are now mandatory per #414. Cookie & Redis sessions can fallback to V5 style JSON in error cases. TODO: session_state.go unit tests & new unit tests for Legacy fallback scenarios. * Only compress encoded sessions with Cookie Store * Cleanup msgpack + lz4 error handling * Change NewBase64Cipher to take in an existing Cipher * Add msgpack & lz4 session state tests * Add required options for oauthproxy tests More aggressively assert.NoError on all validation.Validate(opts) calls to enforce legal options in all our tests. Add additional NoError checks wherever error return values were ignored. * Remove support for uncompressed session state fields * Improve error verbosity & add session state tests * Ensure all marshalled sessions are valid Invalid CFB decryptions can result in garbage data that 1/100 times might cause message pack unmarshal to not fail and instead return an empty session. This adds more rigor to make sure legacy sessions cause appropriate errors. * Add tests for legacy V5 session decoding Refactor common legacy JSON test cases to a legacy helpers area under session store tests. * Make ValidateSession a struct method & add CHANGELOG entry * Improve SessionState error & comments verbosity * Move legacy session test helpers to sessions pkg Placing these helpers under the sessions pkg removed all the circular import uses in housing it under the session store area. * Improve SignatureAuthenticator test helper formatting * Make redis.legacyV5DecodeSession internal * Make LegacyV5TestCase test table public for linter
2020-07-13 12:56:05 -07:00
func sessionFromCookie(v []byte, c encryption.Cipher) (s *sessions.SessionState, err error) {
ss, err := sessions.DecodeSessionState(v, c, true)
// If anything fails (Decrypt, LZ4, MessagePack), try legacy JSON decode
// LZ4 will likely fail for wrong header after AES-CFB spits out garbage
// data from trying to decrypt JSON it things is ciphertext
if err != nil {
// Legacy used Base64 + AES CFB
legacyCipher := encryption.NewBase64Cipher(c)
return sessions.LegacyV5DecodeSessionState(string(v), legacyCipher)
}
return ss, nil
}
// setSessionCookie adds the user's session cookie to the response
Reduce SessionState size better with MessagePack + LZ4 (#632) * Encode sessions with MsgPack + LZ4 Assumes ciphers are now mandatory per #414. Cookie & Redis sessions can fallback to V5 style JSON in error cases. TODO: session_state.go unit tests & new unit tests for Legacy fallback scenarios. * Only compress encoded sessions with Cookie Store * Cleanup msgpack + lz4 error handling * Change NewBase64Cipher to take in an existing Cipher * Add msgpack & lz4 session state tests * Add required options for oauthproxy tests More aggressively assert.NoError on all validation.Validate(opts) calls to enforce legal options in all our tests. Add additional NoError checks wherever error return values were ignored. * Remove support for uncompressed session state fields * Improve error verbosity & add session state tests * Ensure all marshalled sessions are valid Invalid CFB decryptions can result in garbage data that 1/100 times might cause message pack unmarshal to not fail and instead return an empty session. This adds more rigor to make sure legacy sessions cause appropriate errors. * Add tests for legacy V5 session decoding Refactor common legacy JSON test cases to a legacy helpers area under session store tests. * Make ValidateSession a struct method & add CHANGELOG entry * Improve SessionState error & comments verbosity * Move legacy session test helpers to sessions pkg Placing these helpers under the sessions pkg removed all the circular import uses in housing it under the session store area. * Improve SignatureAuthenticator test helper formatting * Make redis.legacyV5DecodeSession internal * Make LegacyV5TestCase test table public for linter
2020-07-13 12:56:05 -07:00
func (s *SessionStore) setSessionCookie(rw http.ResponseWriter, req *http.Request, val []byte, created time.Time) {
for _, c := range s.makeSessionCookie(req, val, created) {
http.SetCookie(rw, c)
}
}
// makeSessionCookie creates an http.Cookie containing the authenticated user's
// authentication details
Reduce SessionState size better with MessagePack + LZ4 (#632) * Encode sessions with MsgPack + LZ4 Assumes ciphers are now mandatory per #414. Cookie & Redis sessions can fallback to V5 style JSON in error cases. TODO: session_state.go unit tests & new unit tests for Legacy fallback scenarios. * Only compress encoded sessions with Cookie Store * Cleanup msgpack + lz4 error handling * Change NewBase64Cipher to take in an existing Cipher * Add msgpack & lz4 session state tests * Add required options for oauthproxy tests More aggressively assert.NoError on all validation.Validate(opts) calls to enforce legal options in all our tests. Add additional NoError checks wherever error return values were ignored. * Remove support for uncompressed session state fields * Improve error verbosity & add session state tests * Ensure all marshalled sessions are valid Invalid CFB decryptions can result in garbage data that 1/100 times might cause message pack unmarshal to not fail and instead return an empty session. This adds more rigor to make sure legacy sessions cause appropriate errors. * Add tests for legacy V5 session decoding Refactor common legacy JSON test cases to a legacy helpers area under session store tests. * Make ValidateSession a struct method & add CHANGELOG entry * Improve SessionState error & comments verbosity * Move legacy session test helpers to sessions pkg Placing these helpers under the sessions pkg removed all the circular import uses in housing it under the session store area. * Improve SignatureAuthenticator test helper formatting * Make redis.legacyV5DecodeSession internal * Make LegacyV5TestCase test table public for linter
2020-07-13 12:56:05 -07:00
func (s *SessionStore) makeSessionCookie(req *http.Request, value []byte, now time.Time) []*http.Cookie {
strValue := string(value)
if strValue != "" {
strValue = encryption.SignedValue(s.Cookie.Secret, s.Cookie.Name, value, now)
}
Reduce SessionState size better with MessagePack + LZ4 (#632) * Encode sessions with MsgPack + LZ4 Assumes ciphers are now mandatory per #414. Cookie & Redis sessions can fallback to V5 style JSON in error cases. TODO: session_state.go unit tests & new unit tests for Legacy fallback scenarios. * Only compress encoded sessions with Cookie Store * Cleanup msgpack + lz4 error handling * Change NewBase64Cipher to take in an existing Cipher * Add msgpack & lz4 session state tests * Add required options for oauthproxy tests More aggressively assert.NoError on all validation.Validate(opts) calls to enforce legal options in all our tests. Add additional NoError checks wherever error return values were ignored. * Remove support for uncompressed session state fields * Improve error verbosity & add session state tests * Ensure all marshalled sessions are valid Invalid CFB decryptions can result in garbage data that 1/100 times might cause message pack unmarshal to not fail and instead return an empty session. This adds more rigor to make sure legacy sessions cause appropriate errors. * Add tests for legacy V5 session decoding Refactor common legacy JSON test cases to a legacy helpers area under session store tests. * Make ValidateSession a struct method & add CHANGELOG entry * Improve SessionState error & comments verbosity * Move legacy session test helpers to sessions pkg Placing these helpers under the sessions pkg removed all the circular import uses in housing it under the session store area. * Improve SignatureAuthenticator test helper formatting * Make redis.legacyV5DecodeSession internal * Make LegacyV5TestCase test table public for linter
2020-07-13 12:56:05 -07:00
c := s.makeCookie(req, s.Cookie.Name, strValue, s.Cookie.Expire, now)
if len(c.String()) > maxCookieLength {
return splitCookie(c)
}
return []*http.Cookie{c}
}
2019-05-07 15:32:46 +01:00
func (s *SessionStore) makeCookie(req *http.Request, name string, value string, expiration time.Duration, now time.Time) *http.Cookie {
return pkgcookies.MakeCookieFromOptions(
req,
name,
value,
2020-05-25 12:43:24 +01:00
s.Cookie,
expiration,
2019-05-07 15:32:46 +01:00
now,
)
}
// NewCookieSessionStore initialises a new instance of the SessionStore from
// the configuration given
2020-05-25 12:43:24 +01:00
func NewCookieSessionStore(opts *options.SessionOptions, cookieOpts *options.Cookie) (sessions.SessionStore, error) {
Reduce SessionState size better with MessagePack + LZ4 (#632) * Encode sessions with MsgPack + LZ4 Assumes ciphers are now mandatory per #414. Cookie & Redis sessions can fallback to V5 style JSON in error cases. TODO: session_state.go unit tests & new unit tests for Legacy fallback scenarios. * Only compress encoded sessions with Cookie Store * Cleanup msgpack + lz4 error handling * Change NewBase64Cipher to take in an existing Cipher * Add msgpack & lz4 session state tests * Add required options for oauthproxy tests More aggressively assert.NoError on all validation.Validate(opts) calls to enforce legal options in all our tests. Add additional NoError checks wherever error return values were ignored. * Remove support for uncompressed session state fields * Improve error verbosity & add session state tests * Ensure all marshalled sessions are valid Invalid CFB decryptions can result in garbage data that 1/100 times might cause message pack unmarshal to not fail and instead return an empty session. This adds more rigor to make sure legacy sessions cause appropriate errors. * Add tests for legacy V5 session decoding Refactor common legacy JSON test cases to a legacy helpers area under session store tests. * Make ValidateSession a struct method & add CHANGELOG entry * Improve SessionState error & comments verbosity * Move legacy session test helpers to sessions pkg Placing these helpers under the sessions pkg removed all the circular import uses in housing it under the session store area. * Improve SignatureAuthenticator test helper formatting * Make redis.legacyV5DecodeSession internal * Make LegacyV5TestCase test table public for linter
2020-07-13 12:56:05 -07:00
cipher, err := encryption.NewCFBCipher(encryption.SecretBytes(cookieOpts.Secret))
if err != nil {
return nil, fmt.Errorf("error initialising cipher: %v", err)
}
return &SessionStore{
2020-05-25 12:43:24 +01:00
CookieCipher: cipher,
Cookie: cookieOpts,
Minimal: opts.Cookie.Minimal,
}, 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.String()) < maxCookieLength {
return []*http.Cookie{c}
}
logger.Printf("WARNING: Multiple cookies are required for this session as it exceeds the 4kb cookie limit. Please use server side session storage (eg. Redis) instead.")
cookies := []*http.Cookie{}
valueBytes := []byte(c.Value)
count := 0
for len(valueBytes) > 0 {
newCookie := copyCookie(c)
newCookie.Name = splitCookieName(c.Name, count)
count++
newCookie.Value = string(valueBytes)
cookieLength := len(newCookie.String())
if cookieLength <= maxCookieLength {
valueBytes = []byte{}
} else {
overflow := cookieLength - maxCookieLength
valueSize := len(valueBytes) - overflow
newValue := valueBytes[:valueSize]
valueBytes = valueBytes[valueSize:]
newCookie.Value = string(newValue)
}
cookies = append(cookies, newCookie)
}
return cookies
}
func splitCookieName(name string, count int) string {
splitName := fmt.Sprintf("%s_%d", name, count)
overflow := len(splitName) - 256
if overflow > 0 {
splitName = fmt.Sprintf("%s_%d", name[:len(name)-overflow], count)
}
return splitName
}
// 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
func loadCookie(req *http.Request, cookieName string) (*http.Cookie, error) {
c, err := req.Cookie(cookieName)
if err == nil {
return c, nil
}
cookies := []*http.Cookie{}
err = nil
count := 0
for err == nil {
var c *http.Cookie
c, err = req.Cookie(splitCookieName(cookieName, count))
if err == nil {
cookies = append(cookies, c)
count++
}
}
if len(cookies) == 0 {
return nil, fmt.Errorf("could not find cookie %s", cookieName)
}
return joinCookies(cookies)
}
// joinCookies takes a slice of cookies from the request and reconstructs the
// full session cookie
func joinCookies(cookies []*http.Cookie) (*http.Cookie, error) {
if len(cookies) == 0 {
return nil, fmt.Errorf("list of cookies must be > 0")
}
if len(cookies) == 1 {
return cookies[0], nil
}
c := copyCookie(cookies[0])
for i := 1; i < len(cookies); i++ {
c.Value += cookies[i].Value
}
c.Name = strings.TrimRight(c.Name, "_0")
return c, nil
}
func copyCookie(c *http.Cookie) *http.Cookie {
return &http.Cookie{
Name: c.Name,
Value: c.Value,
Path: c.Path,
Domain: c.Domain,
Expires: c.Expires,
RawExpires: c.RawExpires,
MaxAge: c.MaxAge,
Secure: c.Secure,
HttpOnly: c.HttpOnly,
Raw: c.Raw,
Unparsed: c.Unparsed,
SameSite: c.SameSite,
}
}