2019-05-06 14:33:33 +01:00
package cookie
import (
2019-05-06 23:16:01 +01:00
"errors"
2019-05-06 14:33:33 +01:00
"fmt"
"net/http"
2019-05-07 00:20:36 +01:00
"regexp"
2019-05-06 23:16:01 +01:00
"strings"
"time"
2019-05-06 14:33:33 +01:00
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"
2020-07-03 20:31:12 -07:00
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"
2020-04-10 14:25:23 +01:00
"github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
2019-05-06 14:33:33 +01:00
)
2020-07-03 23:41:08 -07:00
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
)
2019-05-06 14:33:33 +01: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 23:16:01 +01:00
type SessionStore struct {
2020-05-25 12:43:24 +01:00
Cookie * options . Cookie
CookieCipher encryption . Cipher
2020-07-14 15:02:10 -07:00
Minimal bool
2019-05-06 23:16:01 +01:00
}
2019-05-06 14:33:33 +01:00
2019-05-08 16:36:28 +01:00
// Save takes a sessions.SessionState and stores the information from it
2019-05-06 14:33:33 +01:00
// within Cookies set on the HTTP response writer
2019-05-08 16:36:28 +01:00
func ( s * SessionStore ) Save ( rw http . ResponseWriter , req * http . Request , ss * sessions . SessionState ) error {
2020-05-30 08:53:38 +01:00
if ss . CreatedAt == nil || ss . CreatedAt . IsZero ( ) {
now := time . Now ( )
ss . CreatedAt = & now
2019-05-07 16:13:55 +01:00
}
2020-07-14 15:02:10 -07:00
value , err := s . cookieForSession ( ss )
2019-05-07 12:18:23 +01:00
if err != nil {
return err
}
2020-05-30 08:53:38 +01:00
s . setSessionCookie ( rw , req , value , * ss . CreatedAt )
2019-05-07 12:18:23 +01:00
return nil
2019-05-06 14:33:33 +01:00
}
2019-05-08 16:36:28 +01:00
// Load reads sessions.SessionState information from Cookies within the
2019-05-06 14:33:33 +01:00
// HTTP request object
2019-05-08 16:36:28 +01:00
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 )
2019-05-06 23:16:01 +01:00
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 )
2019-05-06 23:16:01 +01:00
}
2020-05-25 12:43:24 +01:00
val , _ , ok := encryption . Validate ( c , s . Cookie . Secret , s . Cookie . Expire )
2019-05-06 23:16:01 +01:00
if ! ok {
2020-04-14 17:36:44 +09:00
return nil , errors . New ( "cookie signature not valid" )
2019-05-06 23:16:01 +01:00
}
2020-07-13 12:56:05 -07:00
session , err := sessionFromCookie ( val , s . CookieCipher )
2019-05-06 23:16:01 +01:00
if err != nil {
return nil , err
}
return session , nil
2019-05-06 14:33:33 +01:00
}
2019-05-08 16:36:28 +01:00
// Clear clears any saved session information by writing a cookie to
2019-05-06 14:33:33 +01:00
// clear the session
2019-05-08 16:36:28 +01:00
func ( s * SessionStore ) Clear ( rw http . ResponseWriter , req * http . Request ) error {
2019-05-07 00:20:36 +01:00
// matches CookieName, CookieName_<number>
2020-05-25 12:43:24 +01:00
var cookieNameRegex = regexp . MustCompile ( fmt . Sprintf ( "^%s(_\\d+)?$" , s . Cookie . Name ) )
2019-05-07 00:20:36 +01:00
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 ( ) )
2019-05-07 00:20:36 +01:00
http . SetCookie ( rw , clearCookie )
}
}
return nil
}
2020-05-14 02:16:35 -07:00
// cookieForSession serializes a session state for storage in a cookie
2020-07-14 15:02:10 -07:00
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 )
2020-05-14 02:16:35 -07:00
}
// sessionFromCookie deserializes a session from a cookie value
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
2020-05-14 02:16:35 -07:00
}
2019-05-07 12:18:23 +01:00
// setSessionCookie adds the user's session cookie to the response
2020-07-13 12:56:05 -07:00
func ( s * SessionStore ) setSessionCookie ( rw http . ResponseWriter , req * http . Request , val [ ] byte , created time . Time ) {
2019-05-13 16:01:28 +01:00
for _ , c := range s . makeSessionCookie ( req , val , created ) {
2019-05-07 12:18:23 +01:00
http . SetCookie ( rw , c )
}
}
// makeSessionCookie creates an http.Cookie containing the authenticated user's
// authentication details
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 )
2019-05-07 12:18:23 +01:00
}
2020-07-13 12:56:05 -07:00
c := s . makeCookie ( req , s . Cookie . Name , strValue , s . Cookie . Expire , now )
2020-07-03 23:41:08 -07:00
if len ( c . String ( ) ) > maxCookieLength {
2019-05-07 12:18:23 +01:00
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 {
2020-07-03 20:31:12 -07:00
return pkgcookies . MakeCookieFromOptions (
2019-05-07 00:20:36 +01:00
req ,
name ,
value ,
2020-05-25 12:43:24 +01:00
s . Cookie ,
2019-05-07 00:20:36 +01:00
expiration ,
2019-05-07 15:32:46 +01:00
now ,
2019-05-07 00:20:36 +01:00
)
2019-05-06 14:33:33 +01:00
}
// 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 ) {
2020-07-13 12:56:05 -07:00
cipher , err := encryption . NewCFBCipher ( encryption . SecretBytes ( cookieOpts . Secret ) )
2020-06-28 12:44:12 +01:00
if err != nil {
return nil , fmt . Errorf ( "error initialising cipher: %v" , err )
}
2019-05-06 23:16:01 +01:00
return & SessionStore {
2020-05-25 12:43:24 +01:00
CookieCipher : cipher ,
Cookie : cookieOpts ,
2020-07-14 15:02:10 -07:00
Minimal : opts . Cookie . Minimal ,
2019-05-06 23:16:01 +01:00
} , nil
}
2019-05-07 12:18:23 +01: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 {
2020-07-03 23:41:08 -07:00
if len ( c . String ( ) ) < maxCookieLength {
2019-05-07 12:18:23 +01:00
return [ ] * http . Cookie { c }
}
2020-07-03 20:31:12 -07:00
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." )
2019-05-07 12:18:23 +01:00
cookies := [ ] * http . Cookie { }
valueBytes := [ ] byte ( c . Value )
count := 0
for len ( valueBytes ) > 0 {
2019-10-09 11:33:45 +03:00
newCookie := copyCookie ( c )
2020-07-03 20:31:12 -07:00
newCookie . Name = splitCookieName ( c . Name , count )
2019-05-07 12:18:23 +01:00
count ++
2020-07-03 20:31:12 -07:00
2020-07-03 23:41:08 -07:00
newCookie . Value = string ( valueBytes )
cookieLength := len ( newCookie . String ( ) )
if cookieLength <= maxCookieLength {
2019-05-07 12:18:23 +01:00
valueBytes = [ ] byte { }
} else {
2020-07-03 23:41:08 -07:00
overflow := cookieLength - maxCookieLength
valueSize := len ( valueBytes ) - overflow
newValue := valueBytes [ : valueSize ]
valueBytes = valueBytes [ valueSize : ]
2019-10-09 11:33:45 +03:00
newCookie . Value = string ( newValue )
2019-05-07 12:18:23 +01:00
}
2019-10-09 11:33:45 +03:00
cookies = append ( cookies , newCookie )
2019-05-07 12:18:23 +01:00
}
return cookies
}
2020-07-03 20:31:12 -07:00
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
}
2019-05-06 23:16:01 +01: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
2020-07-03 20:31:12 -07:00
c , err = req . Cookie ( splitCookieName ( cookieName , count ) )
2019-05-06 23:16:01 +01:00
if err == nil {
cookies = append ( cookies , c )
count ++
}
}
if len ( cookies ) == 0 {
2020-04-14 17:36:44 +09:00
return nil , fmt . Errorf ( "could not find cookie %s" , cookieName )
2019-05-06 23:16:01 +01:00
}
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 ,
2020-03-18 03:48:52 +09:00
SameSite : c . SameSite ,
2019-05-06 23:16:01 +01:00
}
2019-05-06 14:33:33 +01:00
}