package validation import ( "time" "github.com/Bose/minisentinel" "github.com/alicebob/miniredis/v2" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" . "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo/extensions/table" . "github.com/onsi/gomega" ) var _ = Describe("Sessions", func() { const ( idTokenConflictMsg = "id_token claim for header \"X-ID-Token\" requires oauth tokens in sessions. session_cookie_minimal cannot be set" accessTokenConflictMsg = "access_token claim for header \"X-Access-Token\" requires oauth tokens in sessions. session_cookie_minimal cannot be set" cookieRefreshMsg = "cookie_refresh > 0 requires oauth tokens in sessions. session_cookie_minimal cannot be set" ) type cookieMinimalTableInput struct { opts *options.Options errStrings []string } DescribeTable("validateSessionCookieMinimal", func(o *cookieMinimalTableInput) { Expect(validateSessionCookieMinimal(o.opts)).To(ConsistOf(o.errStrings)) }, Entry("No minimal cookie session", &cookieMinimalTableInput{ opts: &options.Options{ Session: options.SessionOptions{ Cookie: options.CookieStoreOptions{ Minimal: false, }, }, }, errStrings: []string{}, }), Entry("No minimal cookie session & request header has access_token claim", &cookieMinimalTableInput{ opts: &options.Options{ Session: options.SessionOptions{ Cookie: options.CookieStoreOptions{ Minimal: false, }, }, InjectRequestHeaders: []options.Header{ { Name: "X-Access-Token", Values: []options.HeaderValue{ { ClaimSource: &options.ClaimSource{ Claim: "access_token", }, }, }, }, }, }, errStrings: []string{}, }), Entry("Minimal cookie session no conflicts", &cookieMinimalTableInput{ opts: &options.Options{ Session: options.SessionOptions{ Cookie: options.CookieStoreOptions{ Minimal: true, }, }, }, errStrings: []string{}, }), Entry("Request Header id_token conflict", &cookieMinimalTableInput{ opts: &options.Options{ Session: options.SessionOptions{ Cookie: options.CookieStoreOptions{ Minimal: true, }, }, InjectRequestHeaders: []options.Header{ { Name: "X-ID-Token", Values: []options.HeaderValue{ { ClaimSource: &options.ClaimSource{ Claim: "id_token", }, }, }, }, }, }, errStrings: []string{idTokenConflictMsg}, }), Entry("Response Header id_token conflict", &cookieMinimalTableInput{ opts: &options.Options{ Session: options.SessionOptions{ Cookie: options.CookieStoreOptions{ Minimal: true, }, }, InjectResponseHeaders: []options.Header{ { Name: "X-ID-Token", Values: []options.HeaderValue{ { ClaimSource: &options.ClaimSource{ Claim: "id_token", }, }, }, }, }, }, errStrings: []string{idTokenConflictMsg}, }), Entry("Request Header access_token conflict", &cookieMinimalTableInput{ opts: &options.Options{ Session: options.SessionOptions{ Cookie: options.CookieStoreOptions{ Minimal: true, }, }, InjectRequestHeaders: []options.Header{ { Name: "X-Access-Token", Values: []options.HeaderValue{ { ClaimSource: &options.ClaimSource{ Claim: "access_token", }, }, }, }, }, }, errStrings: []string{accessTokenConflictMsg}, }), Entry("CookieRefresh conflict", &cookieMinimalTableInput{ opts: &options.Options{ Cookie: options.Cookie{ Refresh: time.Hour, }, Session: options.SessionOptions{ Cookie: options.CookieStoreOptions{ Minimal: true, }, }, }, errStrings: []string{cookieRefreshMsg}, }), Entry("Multiple conflicts", &cookieMinimalTableInput{ opts: &options.Options{ Session: options.SessionOptions{ Cookie: options.CookieStoreOptions{ Minimal: true, }, }, InjectResponseHeaders: []options.Header{ { Name: "X-ID-Token", Values: []options.HeaderValue{ { ClaimSource: &options.ClaimSource{ Claim: "id_token", }, }, }, }, }, InjectRequestHeaders: []options.Header{ { Name: "X-Access-Token", Values: []options.HeaderValue{ { ClaimSource: &options.ClaimSource{ Claim: "access_token", }, }, }, }, }, }, errStrings: []string{idTokenConflictMsg, accessTokenConflictMsg}, }), ) const ( clusterAndSentinelMsg = "unable to initialize a redis client: options redis-use-sentinel and redis-use-cluster are mutually exclusive" parseWrongSchemeMsg = "unable to initialize a redis client: unable to parse redis url: redis: invalid URL scheme: https" parseWrongFormatMsg = "unable to initialize a redis client: unable to parse redis url: redis: invalid database number: \"wrong\"" invalidPasswordSetMsg = "unable to set a redis initialization key: WRONGPASS invalid username-password pair" invalidPasswordDelMsg = "unable to delete the redis initialization key: WRONGPASS invalid username-password pair" unreachableRedisSetMsg = "unable to set a redis initialization key: dial tcp 127.0.0.1:65535: connect: connection refused" unreachableRedisDelMsg = "unable to delete the redis initialization key: dial tcp 127.0.0.1:65535: connect: connection refused" unreachableSentinelSetMsg = "unable to set a redis initialization key: redis: all sentinels are unreachable" unrechableSentinelDelMsg = "unable to delete the redis initialization key: redis: all sentinels are unreachable" ) type redisStoreTableInput struct { // miniredis setup details password string useSentinel bool setAddr bool setSentinelAddr bool setMasterName bool opts *options.Options errStrings []string } DescribeTable("validateRedisSessionStore", func(o *redisStoreTableInput) { mr, err := miniredis.Run() Expect(err).ToNot(HaveOccurred()) mr.RequireAuth(o.password) defer mr.Close() if o.setAddr && !o.useSentinel { o.opts.Session.Redis.ConnectionURL = "redis://" + mr.Addr() } if o.useSentinel { ms := minisentinel.NewSentinel(mr) Expect(ms.Start()).To(Succeed()) defer ms.Close() if o.setSentinelAddr { o.opts.Session.Redis.SentinelConnectionURLs = []string{"redis://" + ms.Addr()} } if o.setMasterName { o.opts.Session.Redis.SentinelMasterName = ms.MasterInfo().Name } } Expect(validateRedisSessionStore(o.opts)).To(ConsistOf(o.errStrings)) }, Entry("cookie sessions are skipped", &redisStoreTableInput{ opts: &options.Options{ Session: options.SessionOptions{ Type: options.CookieSessionStoreType, }, }, errStrings: []string{}, }), Entry("connect successfully to pure redis", &redisStoreTableInput{ setAddr: true, opts: &options.Options{ Session: options.SessionOptions{ Type: options.RedisSessionStoreType, }, }, errStrings: []string{}, }), Entry("failed redis connection with wrong address", &redisStoreTableInput{ opts: &options.Options{ Session: options.SessionOptions{ Type: options.RedisSessionStoreType, Redis: options.RedisStoreOptions{ ConnectionURL: "redis://127.0.0.1:65535", }, }, }, errStrings: []string{unreachableRedisSetMsg, unreachableRedisDelMsg}, }), Entry("fail to parse an invalid connection URL with wrong scheme", &redisStoreTableInput{ opts: &options.Options{ Session: options.SessionOptions{ Type: options.RedisSessionStoreType, Redis: options.RedisStoreOptions{ ConnectionURL: "https://example.com", }, }, }, errStrings: []string{parseWrongSchemeMsg}, }), Entry("fail to parse an invalid connection URL with invalid format", &redisStoreTableInput{ opts: &options.Options{ Session: options.SessionOptions{ Type: options.RedisSessionStoreType, Redis: options.RedisStoreOptions{ ConnectionURL: "redis://127.0.0.1:6379/wrong", }, }, }, errStrings: []string{parseWrongFormatMsg}, }), Entry("connect successfully to pure redis with password", &redisStoreTableInput{ password: "abcdef123", setAddr: true, opts: &options.Options{ Session: options.SessionOptions{ Type: options.RedisSessionStoreType, Redis: options.RedisStoreOptions{ Password: "abcdef123", }, }, }, errStrings: []string{}, }), Entry("failed connection with wrong password", &redisStoreTableInput{ password: "abcdef123", setAddr: true, opts: &options.Options{ Session: options.SessionOptions{ Type: options.RedisSessionStoreType, Redis: options.RedisStoreOptions{ Password: "zyxwtuv987", }, }, }, errStrings: []string{invalidPasswordSetMsg, invalidPasswordDelMsg}, }), Entry("connect successfully to sentinel redis", &redisStoreTableInput{ useSentinel: true, setSentinelAddr: true, setMasterName: true, opts: &options.Options{ Session: options.SessionOptions{ Type: options.RedisSessionStoreType, Redis: options.RedisStoreOptions{ UseSentinel: true, }, }, }, errStrings: []string{}, }), Entry("connect successfully to sentinel redis with password", &redisStoreTableInput{ password: "abcdef123", useSentinel: true, setSentinelAddr: true, setMasterName: true, opts: &options.Options{ Session: options.SessionOptions{ Type: options.RedisSessionStoreType, Redis: options.RedisStoreOptions{ Password: "abcdef123", UseSentinel: true, }, }, }, errStrings: []string{}, }), Entry("failed connection to sentinel redis with wrong password", &redisStoreTableInput{ password: "abcdef123", useSentinel: true, setSentinelAddr: true, setMasterName: true, opts: &options.Options{ Session: options.SessionOptions{ Type: options.RedisSessionStoreType, Redis: options.RedisStoreOptions{ Password: "zyxwtuv987", UseSentinel: true, }, }, }, errStrings: []string{invalidPasswordSetMsg, invalidPasswordDelMsg}, }), Entry("failed connection to sentinel redis with wrong master name", &redisStoreTableInput{ useSentinel: true, setSentinelAddr: true, opts: &options.Options{ Session: options.SessionOptions{ Type: options.RedisSessionStoreType, Redis: options.RedisStoreOptions{ UseSentinel: true, SentinelMasterName: "WRONG", }, }, }, errStrings: []string{unreachableSentinelSetMsg, unrechableSentinelDelMsg}, }), Entry("failed connection to sentinel redis with wrong address", &redisStoreTableInput{ useSentinel: true, setMasterName: true, opts: &options.Options{ Session: options.SessionOptions{ Type: options.RedisSessionStoreType, Redis: options.RedisStoreOptions{ UseSentinel: true, SentinelConnectionURLs: []string{"redis://127.0.0.1:65535"}, }, }, }, errStrings: []string{unreachableSentinelSetMsg, unrechableSentinelDelMsg}, }), Entry("sentinel and cluster both enabled fails", &redisStoreTableInput{ opts: &options.Options{ Session: options.SessionOptions{ Type: options.RedisSessionStoreType, Redis: options.RedisStoreOptions{ UseSentinel: true, UseCluster: true, }, }, }, errStrings: []string{clusterAndSentinelMsg}, }), ) })