2019-05-06 13:33:33 +00:00
|
|
|
package cookie
|
|
|
|
|
|
|
|
import (
|
2019-05-06 22:16:01 +00:00
|
|
|
"errors"
|
2019-05-06 13:33:33 +00:00
|
|
|
"fmt"
|
|
|
|
"net/http"
|
2019-05-06 23:20:36 +00:00
|
|
|
"regexp"
|
2019-05-06 22:16:01 +00:00
|
|
|
"strings"
|
|
|
|
"time"
|
2019-05-06 13:33:33 +00:00
|
|
|
|
2019-05-06 22:16:01 +00:00
|
|
|
"github.com/pusher/oauth2_proxy/cookie"
|
2019-05-06 13:33:33 +00:00
|
|
|
"github.com/pusher/oauth2_proxy/pkg/apis/options"
|
|
|
|
"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
|
2019-05-06 23:20:36 +00:00
|
|
|
"github.com/pusher/oauth2_proxy/pkg/cookies"
|
2019-05-06 22:16:01 +00:00
|
|
|
"github.com/pusher/oauth2_proxy/pkg/sessions/utils"
|
2019-05-06 13:33:33 +00:00
|
|
|
)
|
|
|
|
|
2019-05-07 11:18:23 +00:00
|
|
|
const (
|
|
|
|
// Cookies are limited to 4kb including the length of the cookie name,
|
|
|
|
// the cookie name can be up to 256 bytes
|
|
|
|
maxCookieLength = 3840
|
|
|
|
)
|
|
|
|
|
2019-05-06 13:33:33 +00:00
|
|
|
// 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
|
2019-05-06 22:16:01 +00:00
|
|
|
type SessionStore struct {
|
2019-05-06 23:20:36 +00:00
|
|
|
CookieCipher *cookie.Cipher
|
|
|
|
CookieDomain string
|
|
|
|
CookieExpire time.Duration
|
|
|
|
CookieHTTPOnly bool
|
|
|
|
CookieName string
|
|
|
|
CookiePath string
|
|
|
|
CookieSecret string
|
|
|
|
CookieSecure bool
|
2019-05-06 22:16:01 +00:00
|
|
|
}
|
2019-05-06 13:33:33 +00:00
|
|
|
|
2019-05-08 15:36:28 +00:00
|
|
|
// Save takes a sessions.SessionState and stores the information from it
|
2019-05-06 13:33:33 +00:00
|
|
|
// within Cookies set on the HTTP response writer
|
2019-05-08 15:36:28 +00:00
|
|
|
func (s *SessionStore) Save(rw http.ResponseWriter, req *http.Request, ss *sessions.SessionState) error {
|
2019-05-07 11:18:23 +00:00
|
|
|
value, err := utils.CookieForSession(ss, s.CookieCipher)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
s.setSessionCookie(rw, req, value)
|
|
|
|
return nil
|
2019-05-06 13:33:33 +00:00
|
|
|
}
|
|
|
|
|
2019-05-08 15:36:28 +00:00
|
|
|
// Load reads sessions.SessionState information from Cookies within the
|
2019-05-06 13:33:33 +00:00
|
|
|
// HTTP request object
|
2019-05-08 15:36:28 +00:00
|
|
|
func (s *SessionStore) Load(req *http.Request) (*sessions.SessionState, error) {
|
2019-05-06 22:16:01 +00:00
|
|
|
c, err := loadCookie(req, s.CookieName)
|
|
|
|
if err != nil {
|
|
|
|
// always http.ErrNoCookie
|
|
|
|
return nil, fmt.Errorf("Cookie %q not present", s.CookieName)
|
|
|
|
}
|
|
|
|
val, _, ok := cookie.Validate(c, s.CookieSecret, s.CookieExpire)
|
|
|
|
if !ok {
|
|
|
|
return nil, errors.New("Cookie Signature not valid")
|
|
|
|
}
|
|
|
|
|
|
|
|
session, err := utils.SessionFromCookie(val, s.CookieCipher)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return session, nil
|
2019-05-06 13:33:33 +00:00
|
|
|
}
|
|
|
|
|
2019-05-08 15:36:28 +00:00
|
|
|
// Clear clears any saved session information by writing a cookie to
|
2019-05-06 13:33:33 +00:00
|
|
|
// clear the session
|
2019-05-08 15:36:28 +00:00
|
|
|
func (s *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) error {
|
2019-05-06 23:20:36 +00:00
|
|
|
var cookies []*http.Cookie
|
|
|
|
|
|
|
|
// matches CookieName, CookieName_<number>
|
|
|
|
var cookieNameRegex = regexp.MustCompile(fmt.Sprintf("^%s(_\\d+)?$", s.CookieName))
|
|
|
|
|
|
|
|
for _, c := range req.Cookies() {
|
|
|
|
if cookieNameRegex.MatchString(c.Name) {
|
|
|
|
clearCookie := s.makeCookie(req, c.Name, "", time.Hour*-1)
|
|
|
|
|
|
|
|
http.SetCookie(rw, clearCookie)
|
|
|
|
cookies = append(cookies, clearCookie)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-05-07 11:18:23 +00:00
|
|
|
// 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}
|
|
|
|
}
|
|
|
|
|
2019-05-06 23:20:36 +00:00
|
|
|
func (s *SessionStore) makeCookie(req *http.Request, name string, value string, expiration time.Duration) *http.Cookie {
|
|
|
|
return cookies.MakeCookie(
|
|
|
|
req,
|
|
|
|
name,
|
|
|
|
value,
|
|
|
|
s.CookiePath,
|
|
|
|
s.CookieDomain,
|
|
|
|
s.CookieHTTPOnly,
|
|
|
|
s.CookieSecure,
|
|
|
|
expiration,
|
|
|
|
time.Now(),
|
|
|
|
)
|
2019-05-06 13:33:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewCookieSessionStore initialises a new instance of the SessionStore from
|
|
|
|
// the configuration given
|
|
|
|
func NewCookieSessionStore(opts options.CookieStoreOptions, cookieOpts *options.CookieOptions) (sessions.SessionStore, error) {
|
2019-05-06 22:16:01 +00:00
|
|
|
var cipher *cookie.Cipher
|
|
|
|
if len(cookieOpts.CookieSecret) > 0 {
|
|
|
|
var err error
|
|
|
|
cipher, err = cookie.NewCipher(utils.SecretBytes(cookieOpts.CookieSecret))
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("unable to create cipher: %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return &SessionStore{
|
2019-05-06 23:20:36 +00:00
|
|
|
CookieCipher: cipher,
|
|
|
|
CookieDomain: cookieOpts.CookieDomain,
|
|
|
|
CookieExpire: cookieOpts.CookieExpire,
|
|
|
|
CookieHTTPOnly: cookieOpts.CookieHTTPOnly,
|
|
|
|
CookieName: cookieOpts.CookieName,
|
|
|
|
CookiePath: cookieOpts.CookiePath,
|
|
|
|
CookieSecret: cookieOpts.CookieSecret,
|
|
|
|
CookieSecure: cookieOpts.CookieSecure,
|
2019-05-06 22:16:01 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2019-05-07 11:18:23 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2019-05-06 22:16:01 +00:00
|
|
|
// 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(fmt.Sprintf("%s_%d", 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,
|
|
|
|
}
|
2019-05-06 13:33:33 +00:00
|
|
|
}
|