Merge branch 'master' into bumpoidc
This commit is contained in:
		
						commit
						78feaec6fa
					
				
							
								
								
									
										10
									
								
								CHANGELOG.md
								
								
								
								
							
							
						
						
									
										10
									
								
								CHANGELOG.md
								
								
								
								
							|  | @ -16,6 +16,16 @@ | ||||||
| 
 | 
 | ||||||
| - [#175](https://github.com/pusher/outh2_proxy/pull/175) Bump go-oidc to v2.0.0 (@aeijdenberg). | - [#175](https://github.com/pusher/outh2_proxy/pull/175) Bump go-oidc to v2.0.0 (@aeijdenberg). | ||||||
|   - Includes fix for potential signature checking issue when OIDC discovery is skipped. |   - Includes fix for potential signature checking issue when OIDC discovery is skipped. | ||||||
|  | - [#155](https://github.com/pusher/outh2_proxy/pull/155) Add RedisSessionStore implementation (@brianv0, @JoelSpeed) | ||||||
|  |   - Implement flags to configure the redis session store | ||||||
|  |     - `-session-store-type=redis` Sets the store type to redis | ||||||
|  |     - `-redis-connection-url` Sets the Redis connection URL | ||||||
|  |     - `-redis-use-sentinel=true` Enables Redis Sentinel support | ||||||
|  |     - `-redis-sentinel-master-name` Sets the Sentinel master name, if sentinel is enabled | ||||||
|  |     - `-redis-sentinel-connection-urls` Defines the Redis Sentinel Connection URLs, if sentinel is enabled | ||||||
|  |   - Introduces the concept of a session ticket. Tickets are composed of the cookie name, a session ID, and a secret. | ||||||
|  |   - Redis Sessions are stored encrypted with a per-session secret  | ||||||
|  |   - Added tests for server based session stores | ||||||
| - [#168](https://github.com/pusher/outh2_proxy/pull/168) Drop Go 1.11 support in Travis (@JoelSpeed) | - [#168](https://github.com/pusher/outh2_proxy/pull/168) Drop Go 1.11 support in Travis (@JoelSpeed) | ||||||
| - [#169](https://github.com/pusher/outh2_proxy/pull/169) Update Alpine to 3.9 (@kskewes) | - [#169](https://github.com/pusher/outh2_proxy/pull/169) Update Alpine to 3.9 (@kskewes) | ||||||
| - [#148](https://github.com/pusher/outh2_proxy/pull/148) Implement SessionStore interface within proxy (@JoelSpeed) | - [#148](https://github.com/pusher/outh2_proxy/pull/148) Implement SessionStore interface within proxy (@JoelSpeed) | ||||||
|  |  | ||||||
|  | @ -17,6 +17,25 @@ | ||||||
|   revision = "b26d9c308763d68093482582cea63d69be07a0f0" |   revision = "b26d9c308763d68093482582cea63d69be07a0f0" | ||||||
|   version = "v0.3.0" |   version = "v0.3.0" | ||||||
| 
 | 
 | ||||||
|  | [[projects]] | ||||||
|  |   branch = "master" | ||||||
|  |   digest = "1:3cce78d5d0090e3f1162945fba60ba74e72e8422e8e41bb9c701afb67237bb65" | ||||||
|  |   name = "github.com/alicebob/gopher-json" | ||||||
|  |   packages = ["."] | ||||||
|  |   pruneopts = "" | ||||||
|  |   revision = "5a6b3ba71ee69b77cf64febf8b5a7526ca5eaef0" | ||||||
|  | 
 | ||||||
|  | [[projects]] | ||||||
|  |   digest = "1:18a07506ddaa87b1612bfd69eef03f510faf122398df3da774d46dcfe751a060" | ||||||
|  |   name = "github.com/alicebob/miniredis" | ||||||
|  |   packages = [ | ||||||
|  |     ".", | ||||||
|  |     "server", | ||||||
|  |   ] | ||||||
|  |   pruneopts = "" | ||||||
|  |   revision = "3d7aa1333af56ab862d446678d93aaa6803e0938" | ||||||
|  |   version = "v2.7.0" | ||||||
|  | 
 | ||||||
| [[projects]] | [[projects]] | ||||||
|   digest = "1:512883404c2a99156e410e9880e3bb35ecccc0c07c1159eb204b5f3ef3c431b3" |   digest = "1:512883404c2a99156e410e9880e3bb35ecccc0c07c1159eb204b5f3ef3c431b3" | ||||||
|   name = "github.com/bitly/go-simplejson" |   name = "github.com/bitly/go-simplejson" | ||||||
|  | @ -49,6 +68,22 @@ | ||||||
|   revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e" |   revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e" | ||||||
|   version = "v3.2.0" |   version = "v3.2.0" | ||||||
| 
 | 
 | ||||||
|  | [[projects]] | ||||||
|  |   digest = "1:8c7410dae63c74bd92db09bf33af7e0698b635ab6a397fd8e9e10dfcce3138ac" | ||||||
|  |   name = "github.com/go-redis/redis" | ||||||
|  |   packages = [ | ||||||
|  |     ".", | ||||||
|  |     "internal", | ||||||
|  |     "internal/consistenthash", | ||||||
|  |     "internal/hashtag", | ||||||
|  |     "internal/pool", | ||||||
|  |     "internal/proto", | ||||||
|  |     "internal/util", | ||||||
|  |   ] | ||||||
|  |   pruneopts = "" | ||||||
|  |   revision = "d22fde8721cc915a55aeb6b00944a76a92bfeb6e" | ||||||
|  |   version = "v6.15.2" | ||||||
|  | 
 | ||||||
| [[projects]] | [[projects]] | ||||||
|   branch = "master" |   branch = "master" | ||||||
|   digest = "1:3b760d3b93f994df8eb1d9ebfad17d3e9e37edcb7f7efaa15b427c0d7a64f4e4" |   digest = "1:3b760d3b93f994df8eb1d9ebfad17d3e9e37edcb7f7efaa15b427c0d7a64f4e4" | ||||||
|  | @ -57,6 +92,17 @@ | ||||||
|   pruneopts = "" |   pruneopts = "" | ||||||
|   revision = "1e59b77b52bf8e4b449a57e6f79f21226d571845" |   revision = "1e59b77b52bf8e4b449a57e6f79f21226d571845" | ||||||
| 
 | 
 | ||||||
|  | [[projects]] | ||||||
|  |   digest = "1:dcf8316121302735c0ac84e05f4686e3b34e284444435e9a206da48d8be18cb1" | ||||||
|  |   name = "github.com/gomodule/redigo" | ||||||
|  |   packages = [ | ||||||
|  |     "internal", | ||||||
|  |     "redis", | ||||||
|  |   ] | ||||||
|  |   pruneopts = "" | ||||||
|  |   revision = "9c11da706d9b7902c6da69c592f75637793fe121" | ||||||
|  |   version = "v2.0.0" | ||||||
|  | 
 | ||||||
| [[projects]] | [[projects]] | ||||||
|   digest = "1:b3c5b95e56c06f5aa72cb2500e6ee5f44fcd122872d4fec2023a488e561218bc" |   digest = "1:b3c5b95e56c06f5aa72cb2500e6ee5f44fcd122872d4fec2023a488e561218bc" | ||||||
|   name = "github.com/hpcloud/tail" |   name = "github.com/hpcloud/tail" | ||||||
|  | @ -173,6 +219,19 @@ | ||||||
|   pruneopts = "" |   pruneopts = "" | ||||||
|   revision = "1d66fa95c997864ba4d8479f56609620fe542928" |   revision = "1d66fa95c997864ba4d8479f56609620fe542928" | ||||||
| 
 | 
 | ||||||
|  | [[projects]] | ||||||
|  |   branch = "master" | ||||||
|  |   digest = "1:378d29a839ff770e9d9150580b4c01ff0a513a296b0487558a7af7c18adab98e" | ||||||
|  |   name = "github.com/yuin/gopher-lua" | ||||||
|  |   packages = [ | ||||||
|  |     ".", | ||||||
|  |     "ast", | ||||||
|  |     "parse", | ||||||
|  |     "pm", | ||||||
|  |   ] | ||||||
|  |   pruneopts = "" | ||||||
|  |   revision = "8bfc7677f583b35a5663a9dd934c08f3b5774bbb" | ||||||
|  | 
 | ||||||
| [[projects]] | [[projects]] | ||||||
|   branch = "master" |   branch = "master" | ||||||
|   digest = "1:f6a006d27619a4d93bf9b66fe1999b8c8d1fa62bdc63af14f10fbe6fcaa2aa1a" |   digest = "1:f6a006d27619a4d93bf9b66fe1999b8c8d1fa62bdc63af14f10fbe6fcaa2aa1a" | ||||||
|  | @ -341,9 +400,11 @@ | ||||||
|   analyzer-version = 1 |   analyzer-version = 1 | ||||||
|   input-imports = [ |   input-imports = [ | ||||||
|     "github.com/BurntSushi/toml", |     "github.com/BurntSushi/toml", | ||||||
|  |     "github.com/alicebob/miniredis", | ||||||
|     "github.com/bitly/go-simplejson", |     "github.com/bitly/go-simplejson", | ||||||
|     "github.com/coreos/go-oidc", |     "github.com/coreos/go-oidc", | ||||||
|     "github.com/dgrijalva/jwt-go", |     "github.com/dgrijalva/jwt-go", | ||||||
|  |     "github.com/go-redis/redis", | ||||||
|     "github.com/mbland/hmacauth", |     "github.com/mbland/hmacauth", | ||||||
|     "github.com/mreiferson/go-options", |     "github.com/mreiferson/go-options", | ||||||
|     "github.com/onsi/ginkgo", |     "github.com/onsi/ginkgo", | ||||||
|  |  | ||||||
|  | @ -46,3 +46,11 @@ | ||||||
| [[constraint]] | [[constraint]] | ||||||
|   name = "gopkg.in/natefinch/lumberjack.v2" |   name = "gopkg.in/natefinch/lumberjack.v2" | ||||||
|   version = "2.1.0" |   version = "2.1.0" | ||||||
|  | 
 | ||||||
|  | [[constraint]] | ||||||
|  |   name = "github.com/go-redis/redis" | ||||||
|  |   version = "v6.15.2" | ||||||
|  | 
 | ||||||
|  | [[constraint]] | ||||||
|  |   name = "github.com/alicebob/miniredis" | ||||||
|  |   version = "2.7.0" | ||||||
|  |  | ||||||
|  | @ -75,6 +75,10 @@ Usage of oauth2_proxy: | ||||||
|   -pubjwk-url string: JWK pubkey access endpoint: required by login.gov |   -pubjwk-url string: JWK pubkey access endpoint: required by login.gov | ||||||
|   -redeem-url string: Token redemption endpoint |   -redeem-url string: Token redemption endpoint | ||||||
|   -redirect-url string: the OAuth Redirect URL. ie: "https://internalapp.yourcompany.com/oauth2/callback" |   -redirect-url string: the OAuth Redirect URL. ie: "https://internalapp.yourcompany.com/oauth2/callback" | ||||||
|  |   -redis-connection-url string: URL of redis server for redis session storage (eg: redis://HOST[:PORT]) | ||||||
|  |   -redis-sentinel-master-name string: Redis sentinel master name. Used in conjuction with --redis-use-sentinel | ||||||
|  |   -redis-sentinel-connection-urls: List of Redis sentinel conneciton URLs (eg redis://HOST[:PORT]). Used in conjuction with --redis-use-sentinel | ||||||
|  |   -redis-use-sentinel: Connect to redis via sentinels. Must set --redis-sentinel-master-name and --redis-sentinel-connection-urls to use this feature (default: false) | ||||||
|   -request-logging: Log requests to stdout (default true) |   -request-logging: Log requests to stdout (default true) | ||||||
|   -request-logging-format: Template for request log lines (see "Logging Configuration" paragraph below) |   -request-logging-format: Template for request log lines (see "Logging Configuration" paragraph below) | ||||||
|   -resource string: The resource that is protected (Azure AD only) |   -resource string: The resource that is protected (Azure AD only) | ||||||
|  |  | ||||||
|  | @ -16,6 +16,7 @@ data in one of the available session storage backends. | ||||||
| 
 | 
 | ||||||
| At present the available backends are (as passed to `--session-store-type`): | At present the available backends are (as passed to `--session-store-type`): | ||||||
| - [cookie](cookie-storage) (default) | - [cookie](cookie-storage) (default) | ||||||
|  | - [redis](redis-storage) | ||||||
| 
 | 
 | ||||||
| ### Cookie Storage | ### Cookie Storage | ||||||
| 
 | 
 | ||||||
|  | @ -32,3 +33,35 @@ The following should be known when using this implementation: | ||||||
| - Since multiple requests can be made concurrently to the OAuth2 Proxy, this session implementation | - Since multiple requests can be made concurrently to the OAuth2 Proxy, this session implementation | ||||||
| cannot lock sessions and while updating and refreshing sessions, there can be conflicts which force | cannot lock sessions and while updating and refreshing sessions, there can be conflicts which force | ||||||
| users to re-authenticate | users to re-authenticate | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ### Redis Storage | ||||||
|  | 
 | ||||||
|  | The Redis Storage backend stores sessions, encrypted, in redis. Instead sending all the information | ||||||
|  | back the the client for storage, as in the [Cookie storage](cookie-storage), a ticket is sent back | ||||||
|  | to the user as the cookie value instead. | ||||||
|  | 
 | ||||||
|  | A ticket is composed as the following: | ||||||
|  | 
 | ||||||
|  | `{CookieName}-{ticketID}.{secret}` | ||||||
|  | 
 | ||||||
|  | Where: | ||||||
|  | 
 | ||||||
|  | - The `CookieName` is the OAuth2 cookie name (_oauth2_proxy by default) | ||||||
|  | - The `ticketID` is a 128 bit random number, hex-encoded | ||||||
|  | - The `secret` is a 128 bit random number, base64url encoded (no padding). The secret is unique for every session. | ||||||
|  | - The pair of `{CookieName}-{ticketID}` comprises a ticket handle, and thus, the redis key | ||||||
|  | to which the session is stored. The encoded session is encrypted with the secret and stored | ||||||
|  | in redis via the `SETEX` command. | ||||||
|  | 
 | ||||||
|  | Encrypting every session uniquely protects the refresh/access/id tokens stored in the session from | ||||||
|  | disclosure. | ||||||
|  | 
 | ||||||
|  | #### Usage | ||||||
|  | 
 | ||||||
|  | When using the redis store, specify `--session-store-type=redis` as well as the Redis connection URL, via | ||||||
|  | `--redis-connection-url=redis://host[:port][/db-number]`. | ||||||
|  | 
 | ||||||
|  | You may also configure the store for Redis Sentinel. In this case, you will want to use the  | ||||||
|  | `--redis-use-sentinel=true` flag, as well as configure the flags `--redis-sentinel-master-name`  | ||||||
|  | and `--redis-sentinel-connection-urls` appropriately. | ||||||
|  |  | ||||||
							
								
								
									
										5
									
								
								main.go
								
								
								
								
							
							
						
						
									
										5
									
								
								main.go
								
								
								
								
							|  | @ -24,6 +24,7 @@ func main() { | ||||||
| 	upstreams := StringArray{} | 	upstreams := StringArray{} | ||||||
| 	skipAuthRegex := StringArray{} | 	skipAuthRegex := StringArray{} | ||||||
| 	googleGroups := StringArray{} | 	googleGroups := StringArray{} | ||||||
|  | 	redisSentinelConnectionURLs := StringArray{} | ||||||
| 
 | 
 | ||||||
| 	config := flagSet.String("config", "", "path to config file") | 	config := flagSet.String("config", "", "path to config file") | ||||||
| 	showVersion := flagSet.Bool("version", false, "print version string") | 	showVersion := flagSet.Bool("version", false, "print version string") | ||||||
|  | @ -76,6 +77,10 @@ func main() { | ||||||
| 	flagSet.Bool("cookie-httponly", true, "set HttpOnly cookie flag") | 	flagSet.Bool("cookie-httponly", true, "set HttpOnly cookie flag") | ||||||
| 
 | 
 | ||||||
| 	flagSet.String("session-store-type", "cookie", "the session storage provider to use") | 	flagSet.String("session-store-type", "cookie", "the session storage provider to use") | ||||||
|  | 	flagSet.String("redis-connection-url", "", "URL of redis server for redis session storage (eg: redis://HOST[:PORT])") | ||||||
|  | 	flagSet.Bool("redis-use-sentinel", false, "Connect to redis via sentinels. Must set --redis-sentinel-master-name and --redis-sentinel-connection-urls to use this feature") | ||||||
|  | 	flagSet.String("redis-sentinel-master-name", "", "Redis sentinel master name. Used in conjuction with --redis-use-sentinel") | ||||||
|  | 	flagSet.Var(&redisSentinelConnectionURLs, "redis-sentinel-connection-urls", "List of Redis sentinel connection URLs (eg redis://HOST[:PORT]). Used in conjuction with --redis-use-sentinel") | ||||||
| 
 | 
 | ||||||
| 	flagSet.String("logging-filename", "", "File to log requests to, empty for stdout") | 	flagSet.String("logging-filename", "", "File to log requests to, empty for stdout") | ||||||
| 	flagSet.Int("logging-max-size", 100, "Maximum size in megabytes of the log file before rotation") | 	flagSet.Int("logging-max-size", 100, "Maximum size in megabytes of the log file before rotation") | ||||||
|  |  | ||||||
|  | @ -9,6 +9,7 @@ type SessionOptions struct { | ||||||
| 	Type   string `flag:"session-store-type" cfg:"session_store_type" env:"OAUTH2_PROXY_SESSION_STORE_TYPE"` | 	Type   string `flag:"session-store-type" cfg:"session_store_type" env:"OAUTH2_PROXY_SESSION_STORE_TYPE"` | ||||||
| 	Cipher *cookie.Cipher | 	Cipher *cookie.Cipher | ||||||
| 	CookieStoreOptions | 	CookieStoreOptions | ||||||
|  | 	RedisStoreOptions | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // CookieSessionStoreType is used to indicate the CookieSessionStore should be
 | // CookieSessionStoreType is used to indicate the CookieSessionStore should be
 | ||||||
|  | @ -17,3 +18,15 @@ var CookieSessionStoreType = "cookie" | ||||||
| 
 | 
 | ||||||
| // CookieStoreOptions contains configuration options for the CookieSessionStore.
 | // CookieStoreOptions contains configuration options for the CookieSessionStore.
 | ||||||
| type CookieStoreOptions struct{} | type CookieStoreOptions struct{} | ||||||
|  | 
 | ||||||
|  | // RedisSessionStoreType is used to indicate the RedisSessionStore should be
 | ||||||
|  | // used for storing sessions.
 | ||||||
|  | var RedisSessionStoreType = "redis" | ||||||
|  | 
 | ||||||
|  | // RedisStoreOptions contains configuration options for the RedisSessionStore.
 | ||||||
|  | type RedisStoreOptions struct { | ||||||
|  | 	RedisConnectionURL     string   `flag:"redis-connection-url" cfg:"redis_connection_url" env:"OAUTH2_PROXY_REDIS_CONNECTION_URL"` | ||||||
|  | 	UseSentinel            bool     `flag:"redis-use-sentinel" cfg:"redis_use_sentinel" env:"OAUTH2_PROXY_REDIS_USE_SENTINEL"` | ||||||
|  | 	SentinelMasterName     string   `flag:"redis-sentinel-master-name" cfg:"redis_sentinel_master_name" env:"OAUTH2_PROXY_REDIS_SENTINEL_MASTER_NAME"` | ||||||
|  | 	SentinelConnectionURLs []string `flag:"redis-sentinel-connection-urls" cfg:"redis_sentinel_connection_urls" env:"OAUTH2_PROXY_REDIS_SENTINEL_CONNECTION_URLS"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -203,14 +203,14 @@ func DecodeSessionState(v string, c *cookie.Cipher) (*SessionState, error) { | ||||||
| 			User:  ss.User, | 			User:  ss.User, | ||||||
| 		} | 		} | ||||||
| 	} else { | 	} else { | ||||||
| 		// Backward compatibility with using unecrypted Email
 | 		// Backward compatibility with using unencrypted Email
 | ||||||
| 		if ss.Email != "" { | 		if ss.Email != "" { | ||||||
| 			decryptedEmail, errEmail := c.Decrypt(ss.Email) | 			decryptedEmail, errEmail := c.Decrypt(ss.Email) | ||||||
| 			if errEmail == nil { | 			if errEmail == nil { | ||||||
| 				ss.Email = decryptedEmail | 				ss.Email = decryptedEmail | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		// Backward compatibility with using unecrypted User
 | 		// Backward compatibility with using unencrypted User
 | ||||||
| 		if ss.User != "" { | 		if ss.User != "" { | ||||||
| 			decryptedUser, errUser := c.Decrypt(ss.User) | 			decryptedUser, errUser := c.Decrypt(ss.User) | ||||||
| 			if errUser == nil { | 			if errUser == nil { | ||||||
|  |  | ||||||
|  | @ -0,0 +1,305 @@ | ||||||
|  | package redis | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"crypto/aes" | ||||||
|  | 	"crypto/cipher" | ||||||
|  | 	"crypto/rand" | ||||||
|  | 	"encoding/base64" | ||||||
|  | 	"encoding/hex" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"net/http" | ||||||
|  | 	"strings" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"github.com/go-redis/redis" | ||||||
|  | 	"github.com/pusher/oauth2_proxy/cookie" | ||||||
|  | 	"github.com/pusher/oauth2_proxy/pkg/apis/options" | ||||||
|  | 	"github.com/pusher/oauth2_proxy/pkg/apis/sessions" | ||||||
|  | 	"github.com/pusher/oauth2_proxy/pkg/cookies" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // TicketData is a structure representing the ticket used in server session storage
 | ||||||
|  | type TicketData struct { | ||||||
|  | 	TicketID string | ||||||
|  | 	Secret   []byte | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // SessionStore is an implementation of the sessions.SessionStore
 | ||||||
|  | // interface that stores sessions in redis
 | ||||||
|  | type SessionStore struct { | ||||||
|  | 	CookieCipher  *cookie.Cipher | ||||||
|  | 	CookieOptions *options.CookieOptions | ||||||
|  | 	Client        *redis.Client | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewRedisSessionStore initialises a new instance of the SessionStore from
 | ||||||
|  | // the configuration given
 | ||||||
|  | func NewRedisSessionStore(opts *options.SessionOptions, cookieOpts *options.CookieOptions) (sessions.SessionStore, error) { | ||||||
|  | 	client, err := newRedisClient(opts.RedisStoreOptions) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("error constructing redis client: %v", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	rs := &SessionStore{ | ||||||
|  | 		Client:        client, | ||||||
|  | 		CookieCipher:  opts.Cipher, | ||||||
|  | 		CookieOptions: cookieOpts, | ||||||
|  | 	} | ||||||
|  | 	return rs, nil | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func newRedisClient(opts options.RedisStoreOptions) (*redis.Client, error) { | ||||||
|  | 	if opts.UseSentinel { | ||||||
|  | 		client := redis.NewFailoverClient(&redis.FailoverOptions{ | ||||||
|  | 			MasterName:    opts.SentinelMasterName, | ||||||
|  | 			SentinelAddrs: opts.SentinelConnectionURLs, | ||||||
|  | 		}) | ||||||
|  | 		return client, nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	opt, err := redis.ParseURL(opts.RedisConnectionURL) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("unable to parse redis url: %s", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	client := redis.NewClient(opt) | ||||||
|  | 	return client, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Save takes a sessions.SessionState and stores the information from it
 | ||||||
|  | // to redies, and adds a new ticket cookie on the HTTP response writer
 | ||||||
|  | func (store *SessionStore) Save(rw http.ResponseWriter, req *http.Request, s *sessions.SessionState) error { | ||||||
|  | 	if s.CreatedAt.IsZero() { | ||||||
|  | 		s.CreatedAt = time.Now() | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Old sessions that we are refreshing would have a request cookie
 | ||||||
|  | 	// New sessions don't, so we ignore the error. storeValue will check requestCookie
 | ||||||
|  | 	requestCookie, _ := req.Cookie(store.CookieOptions.CookieName) | ||||||
|  | 	value, err := s.EncodeSessionState(store.CookieCipher) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	ticketString, err := store.storeValue(value, store.CookieOptions.CookieExpire, requestCookie) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	ticketCookie := store.makeCookie( | ||||||
|  | 		req, | ||||||
|  | 		ticketString, | ||||||
|  | 		store.CookieOptions.CookieExpire, | ||||||
|  | 		s.CreatedAt, | ||||||
|  | 	) | ||||||
|  | 
 | ||||||
|  | 	http.SetCookie(rw, ticketCookie) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Load reads sessions.SessionState information from a ticket
 | ||||||
|  | // cookie within the HTTP request object
 | ||||||
|  | func (store *SessionStore) Load(req *http.Request) (*sessions.SessionState, error) { | ||||||
|  | 	requestCookie, err := req.Cookie(store.CookieOptions.CookieName) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("error loading session: %s", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	val, _, ok := cookie.Validate(requestCookie, store.CookieOptions.CookieSecret, store.CookieOptions.CookieExpire) | ||||||
|  | 	if !ok { | ||||||
|  | 		return nil, fmt.Errorf("Cookie Signature not valid") | ||||||
|  | 	} | ||||||
|  | 	session, err := store.loadSessionFromString(val) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("error loading session: %s", err) | ||||||
|  | 	} | ||||||
|  | 	return session, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // loadSessionFromString loads the session based on the ticket value
 | ||||||
|  | func (store *SessionStore) loadSessionFromString(value string) (*sessions.SessionState, error) { | ||||||
|  | 	ticket, err := decodeTicket(store.CookieOptions.CookieName, value) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	result, err := store.Client.Get(ticket.asHandle(store.CookieOptions.CookieName)).Result() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	resultBytes := []byte(result) | ||||||
|  | 	block, err := aes.NewCipher(ticket.Secret) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	// Use secret as the IV too, because each entry has it's own key
 | ||||||
|  | 	stream := cipher.NewCFBDecrypter(block, ticket.Secret) | ||||||
|  | 	stream.XORKeyStream(resultBytes, resultBytes) | ||||||
|  | 
 | ||||||
|  | 	session, err := sessions.DecodeSessionState(string(resultBytes), store.CookieCipher) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return session, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Clear clears any saved session information for a given ticket cookie
 | ||||||
|  | // from redis, and then clears the session
 | ||||||
|  | func (store *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) error { | ||||||
|  | 	// We go ahead and clear the cookie first, always.
 | ||||||
|  | 	clearCookie := store.makeCookie( | ||||||
|  | 		req, | ||||||
|  | 		"", | ||||||
|  | 		time.Hour*-1, | ||||||
|  | 		time.Now(), | ||||||
|  | 	) | ||||||
|  | 	http.SetCookie(rw, clearCookie) | ||||||
|  | 
 | ||||||
|  | 	// If there was an existing cookie we should clear the session in redis
 | ||||||
|  | 	requestCookie, err := req.Cookie(store.CookieOptions.CookieName) | ||||||
|  | 	if err != nil && err == http.ErrNoCookie { | ||||||
|  | 		// No existing cookie so can't clear redis
 | ||||||
|  | 		return nil | ||||||
|  | 	} else if err != nil { | ||||||
|  | 		return fmt.Errorf("error retrieving cookie: %v", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	val, _, ok := cookie.Validate(requestCookie, store.CookieOptions.CookieSecret, store.CookieOptions.CookieExpire) | ||||||
|  | 	if !ok { | ||||||
|  | 		return fmt.Errorf("Cookie Signature not valid") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// We only return an error if we had an issue with redis
 | ||||||
|  | 	// If there's an issue decoding the ticket, ignore it
 | ||||||
|  | 	ticket, _ := decodeTicket(store.CookieOptions.CookieName, val) | ||||||
|  | 	if ticket != nil { | ||||||
|  | 		_, err := store.Client.Del(ticket.asHandle(store.CookieOptions.CookieName)).Result() | ||||||
|  | 		if err != nil { | ||||||
|  | 			return fmt.Errorf("error clearing cookie from redis: %s", err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // makeCookie makes a cookie, signing the value if present
 | ||||||
|  | func (store *SessionStore) makeCookie(req *http.Request, value string, expires time.Duration, now time.Time) *http.Cookie { | ||||||
|  | 	if value != "" { | ||||||
|  | 		value = cookie.SignedValue(store.CookieOptions.CookieSecret, store.CookieOptions.CookieName, value, now) | ||||||
|  | 	} | ||||||
|  | 	return cookies.MakeCookieFromOptions( | ||||||
|  | 		req, | ||||||
|  | 		store.CookieOptions.CookieName, | ||||||
|  | 		value, | ||||||
|  | 		store.CookieOptions, | ||||||
|  | 		expires, | ||||||
|  | 		now, | ||||||
|  | 	) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (store *SessionStore) storeValue(value string, expiration time.Duration, requestCookie *http.Cookie) (string, error) { | ||||||
|  | 	ticket, err := store.getTicket(requestCookie) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", fmt.Errorf("error getting ticket: %v", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	ciphertext := make([]byte, len(value)) | ||||||
|  | 	block, err := aes.NewCipher(ticket.Secret) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", fmt.Errorf("error initiating cipher block %s", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Use secret as the Initialization Vector too, because each entry has it's own key
 | ||||||
|  | 	stream := cipher.NewCFBEncrypter(block, ticket.Secret) | ||||||
|  | 	stream.XORKeyStream(ciphertext, []byte(value)) | ||||||
|  | 
 | ||||||
|  | 	handle := ticket.asHandle(store.CookieOptions.CookieName) | ||||||
|  | 	err = store.Client.Set(handle, ciphertext, expiration).Err() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	return ticket.encodeTicket(store.CookieOptions.CookieName), nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // getTicket retrieves an existing ticket from the cookie if present,
 | ||||||
|  | // or creates a new ticket
 | ||||||
|  | func (store *SessionStore) getTicket(requestCookie *http.Cookie) (*TicketData, error) { | ||||||
|  | 	if requestCookie == nil { | ||||||
|  | 		return newTicket() | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// An existing cookie exists, try to retrieve the ticket
 | ||||||
|  | 	val, _, ok := cookie.Validate(requestCookie, store.CookieOptions.CookieSecret, store.CookieOptions.CookieExpire) | ||||||
|  | 	if !ok { | ||||||
|  | 		// Cookie is invalid, create a new ticket
 | ||||||
|  | 		return newTicket() | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Valid cookie, decode the ticket
 | ||||||
|  | 	ticket, err := decodeTicket(store.CookieOptions.CookieName, val) | ||||||
|  | 	if err != nil { | ||||||
|  | 		// If we can't decode the ticket we have to create a new one
 | ||||||
|  | 		return newTicket() | ||||||
|  | 	} | ||||||
|  | 	return ticket, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func newTicket() (*TicketData, error) { | ||||||
|  | 	rawID := make([]byte, 16) | ||||||
|  | 	if _, err := io.ReadFull(rand.Reader, rawID); err != nil { | ||||||
|  | 		return nil, fmt.Errorf("failed to create new ticket ID %s", err) | ||||||
|  | 	} | ||||||
|  | 	// ticketID is hex encoded
 | ||||||
|  | 	ticketID := fmt.Sprintf("%x", rawID) | ||||||
|  | 
 | ||||||
|  | 	secret := make([]byte, aes.BlockSize) | ||||||
|  | 	if _, err := io.ReadFull(rand.Reader, secret); err != nil { | ||||||
|  | 		return nil, fmt.Errorf("failed to create initialization vector %s", err) | ||||||
|  | 	} | ||||||
|  | 	ticket := &TicketData{ | ||||||
|  | 		TicketID: ticketID, | ||||||
|  | 		Secret:   secret, | ||||||
|  | 	} | ||||||
|  | 	return ticket, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (ticket *TicketData) asHandle(prefix string) string { | ||||||
|  | 	return fmt.Sprintf("%s-%s", prefix, ticket.TicketID) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func decodeTicket(cookieName string, ticketString string) (*TicketData, error) { | ||||||
|  | 	prefix := cookieName + "-" | ||||||
|  | 	if !strings.HasPrefix(ticketString, prefix) { | ||||||
|  | 		return nil, fmt.Errorf("failed to decode ticket handle") | ||||||
|  | 	} | ||||||
|  | 	trimmedTicket := strings.TrimPrefix(ticketString, prefix) | ||||||
|  | 
 | ||||||
|  | 	ticketParts := strings.Split(trimmedTicket, ".") | ||||||
|  | 	if len(ticketParts) != 2 { | ||||||
|  | 		return nil, fmt.Errorf("failed to decode ticket") | ||||||
|  | 	} | ||||||
|  | 	ticketID, secretBase64 := ticketParts[0], ticketParts[1] | ||||||
|  | 
 | ||||||
|  | 	// ticketID must be a hexadecimal string
 | ||||||
|  | 	_, err := hex.DecodeString(ticketID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("server ticket failed sanity checks") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	secret, err := base64.RawURLEncoding.DecodeString(secretBase64) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("failed to decode initialization vector %s", err) | ||||||
|  | 	} | ||||||
|  | 	ticketData := &TicketData{ | ||||||
|  | 		TicketID: ticketID, | ||||||
|  | 		Secret:   secret, | ||||||
|  | 	} | ||||||
|  | 	return ticketData, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (ticket *TicketData) encodeTicket(prefix string) string { | ||||||
|  | 	handle := ticket.asHandle(prefix) | ||||||
|  | 	ticketString := handle + "." + base64.RawURLEncoding.EncodeToString(ticket.Secret) | ||||||
|  | 	return ticketString | ||||||
|  | } | ||||||
|  | @ -6,6 +6,7 @@ import ( | ||||||
| 	"github.com/pusher/oauth2_proxy/pkg/apis/options" | 	"github.com/pusher/oauth2_proxy/pkg/apis/options" | ||||||
| 	"github.com/pusher/oauth2_proxy/pkg/apis/sessions" | 	"github.com/pusher/oauth2_proxy/pkg/apis/sessions" | ||||||
| 	"github.com/pusher/oauth2_proxy/pkg/sessions/cookie" | 	"github.com/pusher/oauth2_proxy/pkg/sessions/cookie" | ||||||
|  | 	"github.com/pusher/oauth2_proxy/pkg/sessions/redis" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // NewSessionStore creates a SessionStore from the provided configuration
 | // NewSessionStore creates a SessionStore from the provided configuration
 | ||||||
|  | @ -13,6 +14,8 @@ func NewSessionStore(opts *options.SessionOptions, cookieOpts *options.CookieOpt | ||||||
| 	switch opts.Type { | 	switch opts.Type { | ||||||
| 	case options.CookieSessionStoreType: | 	case options.CookieSessionStoreType: | ||||||
| 		return cookie.NewCookieSessionStore(opts, cookieOpts) | 		return cookie.NewCookieSessionStore(opts, cookieOpts) | ||||||
|  | 	case options.RedisSessionStoreType: | ||||||
|  | 		return redis.NewRedisSessionStore(opts, cookieOpts) | ||||||
| 	default: | 	default: | ||||||
| 		return nil, fmt.Errorf("unknown session store type '%s'", opts.Type) | 		return nil, fmt.Errorf("unknown session store type '%s'", opts.Type) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -10,6 +10,7 @@ import ( | ||||||
| 	"testing" | 	"testing" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
|  | 	"github.com/alicebob/miniredis" | ||||||
| 	. "github.com/onsi/ginkgo" | 	. "github.com/onsi/ginkgo" | ||||||
| 	. "github.com/onsi/gomega" | 	. "github.com/onsi/gomega" | ||||||
| 	"github.com/pusher/oauth2_proxy/cookie" | 	"github.com/pusher/oauth2_proxy/cookie" | ||||||
|  | @ -18,6 +19,7 @@ import ( | ||||||
| 	"github.com/pusher/oauth2_proxy/pkg/cookies" | 	"github.com/pusher/oauth2_proxy/pkg/cookies" | ||||||
| 	"github.com/pusher/oauth2_proxy/pkg/sessions" | 	"github.com/pusher/oauth2_proxy/pkg/sessions" | ||||||
| 	sessionscookie "github.com/pusher/oauth2_proxy/pkg/sessions/cookie" | 	sessionscookie "github.com/pusher/oauth2_proxy/pkg/sessions/cookie" | ||||||
|  | 	"github.com/pusher/oauth2_proxy/pkg/sessions/redis" | ||||||
| 	"github.com/pusher/oauth2_proxy/pkg/sessions/utils" | 	"github.com/pusher/oauth2_proxy/pkg/sessions/utils" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -34,6 +36,7 @@ var _ = Describe("NewSessionStore", func() { | ||||||
| 	var response *httptest.ResponseRecorder | 	var response *httptest.ResponseRecorder | ||||||
| 	var session *sessionsapi.SessionState | 	var session *sessionsapi.SessionState | ||||||
| 	var ss sessionsapi.SessionStore | 	var ss sessionsapi.SessionStore | ||||||
|  | 	var mr *miniredis.Miniredis | ||||||
| 
 | 
 | ||||||
| 	CheckCookieOptions := func() { | 	CheckCookieOptions := func() { | ||||||
| 		Context("the cookies returned", func() { | 		Context("the cookies returned", func() { | ||||||
|  | @ -89,8 +92,54 @@ var _ = Describe("NewSessionStore", func() { | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	SessionStoreInterfaceTests := func() { | 	// The following should only be for server stores
 | ||||||
|  | 	PersistentSessionStoreTests := func() { | ||||||
|  | 		Context("when Clear is called on a persistent store", func() { | ||||||
|  | 			var resultCookies []*http.Cookie | ||||||
|  | 
 | ||||||
|  | 			BeforeEach(func() { | ||||||
|  | 				req := httptest.NewRequest("GET", "http://example.com/", nil) | ||||||
|  | 				saveResp := httptest.NewRecorder() | ||||||
|  | 				err := ss.Save(saveResp, req, session) | ||||||
|  | 				Expect(err).ToNot(HaveOccurred()) | ||||||
|  | 
 | ||||||
|  | 				resultCookies = saveResp.Result().Cookies() | ||||||
|  | 				for _, c := range resultCookies { | ||||||
|  | 					request.AddCookie(c) | ||||||
|  | 				} | ||||||
|  | 				err = ss.Clear(response, request) | ||||||
|  | 				Expect(err).ToNot(HaveOccurred()) | ||||||
|  | 			}) | ||||||
|  | 
 | ||||||
|  | 			Context("attempting to Load", func() { | ||||||
|  | 				var loadedAfterClear *sessionsapi.SessionState | ||||||
|  | 				var loadErr error | ||||||
|  | 
 | ||||||
|  | 				BeforeEach(func() { | ||||||
|  | 					loadReq := httptest.NewRequest("GET", "http://example.com/", nil) | ||||||
|  | 					for _, c := range resultCookies { | ||||||
|  | 						loadReq.AddCookie(c) | ||||||
|  | 					} | ||||||
|  | 
 | ||||||
|  | 					loadedAfterClear, loadErr = ss.Load(loadReq) | ||||||
|  | 				}) | ||||||
|  | 
 | ||||||
|  | 				It("returns an empty session", func() { | ||||||
|  | 					Expect(loadedAfterClear).To(BeNil()) | ||||||
|  | 				}) | ||||||
|  | 
 | ||||||
|  | 				It("returns an error", func() { | ||||||
|  | 					Expect(loadErr).To(HaveOccurred()) | ||||||
|  | 				}) | ||||||
|  | 			}) | ||||||
|  | 
 | ||||||
|  | 			CheckCookieOptions() | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	SessionStoreInterfaceTests := func(persistent bool) { | ||||||
| 		Context("when Save is called", func() { | 		Context("when Save is called", func() { | ||||||
|  | 			Context("with no existing session", func() { | ||||||
| 				BeforeEach(func() { | 				BeforeEach(func() { | ||||||
| 					err := ss.Save(response, request, session) | 					err := ss.Save(response, request, session) | ||||||
| 					Expect(err).ToNot(HaveOccurred()) | 					Expect(err).ToNot(HaveOccurred()) | ||||||
|  | @ -103,24 +152,69 @@ var _ = Describe("NewSessionStore", func() { | ||||||
| 				It("Ensures the session CreatedAt is not zero", func() { | 				It("Ensures the session CreatedAt is not zero", func() { | ||||||
| 					Expect(session.CreatedAt.IsZero()).To(BeFalse()) | 					Expect(session.CreatedAt.IsZero()).To(BeFalse()) | ||||||
| 				}) | 				}) | ||||||
|  | 			}) | ||||||
|  | 
 | ||||||
|  | 			Context("with a broken session", func() { | ||||||
|  | 				BeforeEach(func() { | ||||||
|  | 					By("Using a valid cookie with a different providers session encoding") | ||||||
|  | 					broken := "BrokenSessionFromADifferentSessionImplementation" | ||||||
|  | 					value := cookie.SignedValue(cookieOpts.CookieSecret, cookieOpts.CookieName, broken, time.Now()) | ||||||
|  | 					cookie := cookies.MakeCookieFromOptions(request, cookieOpts.CookieName, value, cookieOpts, cookieOpts.CookieExpire, time.Now()) | ||||||
|  | 					request.AddCookie(cookie) | ||||||
|  | 
 | ||||||
|  | 					err := ss.Save(response, request, session) | ||||||
|  | 					Expect(err).ToNot(HaveOccurred()) | ||||||
|  | 				}) | ||||||
|  | 
 | ||||||
|  | 				It("sets a `set-cookie` header in the response", func() { | ||||||
|  | 					Expect(response.Header().Get("set-cookie")).ToNot(BeEmpty()) | ||||||
|  | 				}) | ||||||
|  | 
 | ||||||
|  | 				It("Ensures the session CreatedAt is not zero", func() { | ||||||
|  | 					Expect(session.CreatedAt.IsZero()).To(BeFalse()) | ||||||
|  | 				}) | ||||||
|  | 			}) | ||||||
|  | 
 | ||||||
|  | 			Context("with an expired saved session", func() { | ||||||
|  | 				var err error | ||||||
|  | 				BeforeEach(func() { | ||||||
|  | 					By("saving a session") | ||||||
|  | 					req := httptest.NewRequest("GET", "http://example.com/", nil) | ||||||
|  | 					saveResp := httptest.NewRecorder() | ||||||
|  | 					err = ss.Save(saveResp, req, session) | ||||||
|  | 					Expect(err).ToNot(HaveOccurred()) | ||||||
|  | 
 | ||||||
|  | 					By("and clearing the session") | ||||||
|  | 					for _, c := range saveResp.Result().Cookies() { | ||||||
|  | 						request.AddCookie(c) | ||||||
|  | 					} | ||||||
|  | 					clearResp := httptest.NewRecorder() | ||||||
|  | 					err = ss.Clear(clearResp, request) | ||||||
|  | 					Expect(err).ToNot(HaveOccurred()) | ||||||
|  | 
 | ||||||
|  | 					By("then saving a request with the cleared session") | ||||||
|  | 					err = ss.Save(response, request, session) | ||||||
|  | 				}) | ||||||
|  | 
 | ||||||
|  | 				It("no error should occur", func() { | ||||||
|  | 					Expect(err).ToNot(HaveOccurred()) | ||||||
|  | 				}) | ||||||
|  | 			}) | ||||||
| 
 | 
 | ||||||
| 			CheckCookieOptions() | 			CheckCookieOptions() | ||||||
| 		}) | 		}) | ||||||
| 
 | 
 | ||||||
| 		Context("when Clear is called", func() { | 		Context("when Clear is called", func() { | ||||||
| 			BeforeEach(func() { | 			BeforeEach(func() { | ||||||
| 				cookie := cookies.MakeCookie(request, | 				req := httptest.NewRequest("GET", "http://example.com/", nil) | ||||||
| 					cookieOpts.CookieName, | 				saveResp := httptest.NewRecorder() | ||||||
| 					"foo", | 				err := ss.Save(saveResp, req, session) | ||||||
| 					cookieOpts.CookiePath, | 				Expect(err).ToNot(HaveOccurred()) | ||||||
| 					cookieOpts.CookieDomain, | 
 | ||||||
| 					cookieOpts.CookieHTTPOnly, | 				for _, c := range saveResp.Result().Cookies() { | ||||||
| 					cookieOpts.CookieSecure, | 					request.AddCookie(c) | ||||||
| 					cookieOpts.CookieExpire, | 				} | ||||||
| 					time.Now(), | 				err = ss.Clear(response, request) | ||||||
| 				) |  | ||||||
| 				request.AddCookie(cookie) |  | ||||||
| 				err := ss.Clear(response, request) |  | ||||||
| 				Expect(err).ToNot(HaveOccurred()) | 				Expect(err).ToNot(HaveOccurred()) | ||||||
| 			}) | 			}) | ||||||
| 
 | 
 | ||||||
|  | @ -132,16 +226,10 @@ var _ = Describe("NewSessionStore", func() { | ||||||
| 		}) | 		}) | ||||||
| 
 | 
 | ||||||
| 		Context("when Load is called", func() { | 		Context("when Load is called", func() { | ||||||
|  | 			LoadSessionTests := func() { | ||||||
| 				var loadedSession *sessionsapi.SessionState | 				var loadedSession *sessionsapi.SessionState | ||||||
| 				BeforeEach(func() { | 				BeforeEach(func() { | ||||||
| 				req := httptest.NewRequest("GET", "http://example.com/", nil) | 					var err error | ||||||
| 				resp := httptest.NewRecorder() |  | ||||||
| 				err := ss.Save(resp, req, session) |  | ||||||
| 				Expect(err).ToNot(HaveOccurred()) |  | ||||||
| 
 |  | ||||||
| 				for _, cookie := range resp.Result().Cookies() { |  | ||||||
| 					request.AddCookie(cookie) |  | ||||||
| 				} |  | ||||||
| 					loadedSession, err = ss.Load(request) | 					loadedSession, err = ss.Load(request) | ||||||
| 					Expect(err).ToNot(HaveOccurred()) | 					Expect(err).ToNot(HaveOccurred()) | ||||||
| 				}) | 				}) | ||||||
|  | @ -168,10 +256,68 @@ var _ = Describe("NewSessionStore", func() { | ||||||
| 						Expect(loadedSession.ExpiresOn.Equal(session.ExpiresOn)).To(BeTrue()) | 						Expect(loadedSession.ExpiresOn.Equal(session.ExpiresOn)).To(BeTrue()) | ||||||
| 					} | 					} | ||||||
| 				}) | 				}) | ||||||
| 		}) |  | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 	RunSessionTests := func() { | 			BeforeEach(func() { | ||||||
|  | 				req := httptest.NewRequest("GET", "http://example.com/", nil) | ||||||
|  | 				resp := httptest.NewRecorder() | ||||||
|  | 				err := ss.Save(resp, req, session) | ||||||
|  | 				Expect(err).ToNot(HaveOccurred()) | ||||||
|  | 
 | ||||||
|  | 				for _, cookie := range resp.Result().Cookies() { | ||||||
|  | 					request.AddCookie(cookie) | ||||||
|  | 				} | ||||||
|  | 			}) | ||||||
|  | 
 | ||||||
|  | 			Context("before the refresh period", func() { | ||||||
|  | 				LoadSessionTests() | ||||||
|  | 			}) | ||||||
|  | 
 | ||||||
|  | 			// Test TTLs and cleanup of persistent session storage
 | ||||||
|  | 			// For non-persistent we rely on the browser cookie lifecycle
 | ||||||
|  | 			if persistent { | ||||||
|  | 				Context("after the refresh period, but before the cookie expire period", func() { | ||||||
|  | 					BeforeEach(func() { | ||||||
|  | 						switch ss.(type) { | ||||||
|  | 						case *redis.SessionStore: | ||||||
|  | 							mr.FastForward(cookieOpts.CookieRefresh + time.Minute) | ||||||
|  | 						} | ||||||
|  | 					}) | ||||||
|  | 
 | ||||||
|  | 					LoadSessionTests() | ||||||
|  | 				}) | ||||||
|  | 
 | ||||||
|  | 				Context("after the cookie expire period", func() { | ||||||
|  | 					var loadedSession *sessionsapi.SessionState | ||||||
|  | 					var err error | ||||||
|  | 
 | ||||||
|  | 					BeforeEach(func() { | ||||||
|  | 						switch ss.(type) { | ||||||
|  | 						case *redis.SessionStore: | ||||||
|  | 							mr.FastForward(cookieOpts.CookieExpire + time.Minute) | ||||||
|  | 						} | ||||||
|  | 
 | ||||||
|  | 						loadedSession, err = ss.Load(request) | ||||||
|  | 						Expect(err).To(HaveOccurred()) | ||||||
|  | 					}) | ||||||
|  | 
 | ||||||
|  | 					It("returns an error loading the session", func() { | ||||||
|  | 						Expect(err).To(HaveOccurred()) | ||||||
|  | 					}) | ||||||
|  | 
 | ||||||
|  | 					It("returns an empty session", func() { | ||||||
|  | 						Expect(loadedSession).To(BeNil()) | ||||||
|  | 					}) | ||||||
|  | 				}) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 
 | ||||||
|  | 		if persistent { | ||||||
|  | 			PersistentSessionStoreTests() | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	RunSessionTests := func(persistent bool) { | ||||||
| 		Context("with default options", func() { | 		Context("with default options", func() { | ||||||
| 			BeforeEach(func() { | 			BeforeEach(func() { | ||||||
| 				var err error | 				var err error | ||||||
|  | @ -179,7 +325,7 @@ var _ = Describe("NewSessionStore", func() { | ||||||
| 				Expect(err).ToNot(HaveOccurred()) | 				Expect(err).ToNot(HaveOccurred()) | ||||||
| 			}) | 			}) | ||||||
| 
 | 
 | ||||||
| 			SessionStoreInterfaceTests() | 			SessionStoreInterfaceTests(persistent) | ||||||
| 		}) | 		}) | ||||||
| 
 | 
 | ||||||
| 		Context("with non-default options", func() { | 		Context("with non-default options", func() { | ||||||
|  | @ -188,7 +334,7 @@ var _ = Describe("NewSessionStore", func() { | ||||||
| 					CookieName:     "_cookie_name", | 					CookieName:     "_cookie_name", | ||||||
| 					CookiePath:     "/path", | 					CookiePath:     "/path", | ||||||
| 					CookieExpire:   time.Duration(72) * time.Hour, | 					CookieExpire:   time.Duration(72) * time.Hour, | ||||||
| 					CookieRefresh:  time.Duration(3600), | 					CookieRefresh:  time.Duration(2) * time.Hour, | ||||||
| 					CookieSecure:   false, | 					CookieSecure:   false, | ||||||
| 					CookieHTTPOnly: false, | 					CookieHTTPOnly: false, | ||||||
| 					CookieDomain:   "example.com", | 					CookieDomain:   "example.com", | ||||||
|  | @ -199,7 +345,7 @@ var _ = Describe("NewSessionStore", func() { | ||||||
| 				Expect(err).ToNot(HaveOccurred()) | 				Expect(err).ToNot(HaveOccurred()) | ||||||
| 			}) | 			}) | ||||||
| 
 | 
 | ||||||
| 			SessionStoreInterfaceTests() | 			SessionStoreInterfaceTests(persistent) | ||||||
| 		}) | 		}) | ||||||
| 
 | 
 | ||||||
| 		Context("with a cipher", func() { | 		Context("with a cipher", func() { | ||||||
|  | @ -217,7 +363,7 @@ var _ = Describe("NewSessionStore", func() { | ||||||
| 				Expect(err).ToNot(HaveOccurred()) | 				Expect(err).ToNot(HaveOccurred()) | ||||||
| 			}) | 			}) | ||||||
| 
 | 
 | ||||||
| 			SessionStoreInterfaceTests() | 			SessionStoreInterfaceTests(persistent) | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -230,7 +376,7 @@ var _ = Describe("NewSessionStore", func() { | ||||||
| 			CookieName:     "_oauth2_proxy", | 			CookieName:     "_oauth2_proxy", | ||||||
| 			CookiePath:     "/", | 			CookiePath:     "/", | ||||||
| 			CookieExpire:   time.Duration(168) * time.Hour, | 			CookieExpire:   time.Duration(168) * time.Hour, | ||||||
| 			CookieRefresh:  time.Duration(0), | 			CookieRefresh:  time.Duration(1) * time.Hour, | ||||||
| 			CookieSecure:   true, | 			CookieSecure:   true, | ||||||
| 			CookieHTTPOnly: true, | 			CookieHTTPOnly: true, | ||||||
| 		} | 		} | ||||||
|  | @ -260,7 +406,31 @@ var _ = Describe("NewSessionStore", func() { | ||||||
| 		}) | 		}) | ||||||
| 
 | 
 | ||||||
| 		Context("the cookie.SessionStore", func() { | 		Context("the cookie.SessionStore", func() { | ||||||
| 			RunSessionTests() | 			RunSessionTests(false) | ||||||
|  | 		}) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	Context("with type 'redis'", func() { | ||||||
|  | 		BeforeEach(func() { | ||||||
|  | 			var err error | ||||||
|  | 			mr, err = miniredis.Run() | ||||||
|  | 			Expect(err).ToNot(HaveOccurred()) | ||||||
|  | 			opts.Type = options.RedisSessionStoreType | ||||||
|  | 			opts.RedisConnectionURL = "redis://" + mr.Addr() | ||||||
|  | 		}) | ||||||
|  | 
 | ||||||
|  | 		AfterEach(func() { | ||||||
|  | 			mr.Close() | ||||||
|  | 		}) | ||||||
|  | 
 | ||||||
|  | 		It("creates a redis.SessionStore", func() { | ||||||
|  | 			ss, err := sessions.NewSessionStore(opts, cookieOpts) | ||||||
|  | 			Expect(err).NotTo(HaveOccurred()) | ||||||
|  | 			Expect(ss).To(BeAssignableToTypeOf(&redis.SessionStore{})) | ||||||
|  | 		}) | ||||||
|  | 
 | ||||||
|  | 		Context("the redis.SessionStore", func() { | ||||||
|  | 			RunSessionTests(true) | ||||||
| 		}) | 		}) | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue