git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge pull request #147 from pusher/session-store

Add initial session-store interface and implementation

commit17e97ab884be77b7be62b7c35313e2e0d8dd77f0 authorJoel Speed <Joel.speed@hotmail.co.uk> dateMon 20 May 2019, 10:18 (7 years ago) parenta1130e44ad4b11
38 files changed+985−137
CHANGELOG.md+5−1 Gopkg.lock+129 Gopkg.toml+4 Makefile+1 docs/3_configuration.md → docs/configuration/configuration.md+3−1 docs/configuration/sessions.md+34 env_options.go+15−2 env_options_test.go+26−6 main.go+2 oauthproxy.go+7−6 oauthproxy_test.go+14−13 options.go+18−18 pkg/apis/options/cookie.go+15 pkg/apis/options/sessions.go+14 pkg/apis/sessions/interfaces.go+12 providers/session_state.go → pkg/apis/sessions/session_state.go+1−1 providers/session_state_test.go → pkg/apis/sessions/session_state_test.go+31−30 pkg/cookies/cookies.go+34 pkg/sessions/cookie/session_store.go+232 pkg/sessions/session_store.go+19 pkg/sessions/session_store_test.go+254 pkg/sessions/utils/utils.go+41 providers/azure.go+2−1 providers/azure_test.go+7−6 providers/facebook.go+3−2 providers/github.go+3−2 providers/github_test.go+7−6 providers/gitlab.go+2−1 providers/gitlab_test.go+4−3 providers/google.go+4−3 providers/internal_util_test.go+3−2 providers/linkedin.go+3−2 providers/linkedin_test.go+4−3 providers/logingov.go+3−2 providers/oidc.go+8−8 providers/provider_default.go+11−10 providers/provider_default_test.go+2−1 providers/providers.go+8−7
CHANGELOG.md+5−1
@@ -10,7 +10,11 @@
1010
1111## Changes since v3.2.0
1212
13−- [#114](https://github.com/pusher/oauth2_proxy/pull/114), [#154](https://github.com/pusher/oauth2_proxy/pull/154) Documentation is now available live at our [docs website](https://pusher.github.io/oauth2_proxy/) (@JoelSpeed, @icelynjennings)
13+- [#147](https://github.com/pusher/outh2_proxy/pull/147) Add SessionStore interfaces and initial implementation (@JoelSpeed)
14+ - Allows for multiple different session storage implementations including client and server side
15+ - Adds tests suite for interface to ensure consistency across implementations
16+ - Refactor some configuration options (around cookies) into packages
17+- [#114](https://github.com/pusher/oauth2_proxy/pull/114), [#154](https://github.com/pusher/oauth2_proxy/pull/154) Documentation is now available live at our [docs website](https://pusher.github.io/oauth2_proxy/) (@JoelSpeed, @icelynjennings)
1418- [#146](https://github.com/pusher/oauth2_proxy/pull/146) Use full email address as `User` if the auth response did not contain a `User` field (@gargath)
1519- [#144](https://github.com/pusher/oauth2_proxy/pull/144) Use GO 1.12 for ARM builds (@kskewes)
1620- [#142](https://github.com/pusher/oauth2_proxy/pull/142) ARM Docker USER fix (@kskewes)
Gopkg.lock+129
@@ -57,6 +57,20 @@
5757 pruneopts = ""
5858 revision = "1e59b77b52bf8e4b449a57e6f79f21226d571845"
5959
60+[[projects]]
61+ digest = "1:b3c5b95e56c06f5aa72cb2500e6ee5f44fcd122872d4fec2023a488e561218bc"
62+ name = "github.com/hpcloud/tail"
63+ packages = [
64+ ".",
65+ "ratelimiter",
66+ "util",
67+ "watch",
68+ "winfile",
69+ ]
70+ pruneopts = ""
71+ revision = "a30252cb686a21eb2d0b98132633053ec2f7f1e5"
72+ version = "v1.0.0"
73+
6074[[projects]]
6175 digest = "1:af67386ca553c04c6222f7b5b2f17bc97a5dfb3b81b706882c7fd8c72c30cf8f"
6276 name = "github.com/mbland/hmacauth"
@@ -73,6 +87,54 @@
7387 pruneopts = ""
7488 revision = "20ba7d382d05facb01e02eb777af0c5f229c5c95"
7589
90+[[projects]]
91+ digest = "1:a3735b0978a8b53fc2ac97a6f46ca1189f0712a00df86d6ec4cf26c1a25e6d77"
92+ name = "github.com/onsi/ginkgo"
93+ packages = [
94+ ".",
95+ "config",
96+ "internal/codelocation",
97+ "internal/containernode",
98+ "internal/failer",
99+ "internal/leafnodes",
100+ "internal/remote",
101+ "internal/spec",
102+ "internal/spec_iterator",
103+ "internal/specrunner",
104+ "internal/suite",
105+ "internal/testingtproxy",
106+ "internal/writer",
107+ "reporters",
108+ "reporters/stenographer",
109+ "reporters/stenographer/support/go-colorable",
110+ "reporters/stenographer/support/go-isatty",
111+ "types",
112+ ]
113+ pruneopts = ""
114+ revision = "eea6ad008b96acdaa524f5b409513bf062b500ad"
115+ version = "v1.8.0"
116+
117+[[projects]]
118+ digest = "1:dbafce2fddb1ca331646fe2ac9c9413980368b19a60a4406a6e5861680bd73be"
119+ name = "github.com/onsi/gomega"
120+ packages = [
121+ ".",
122+ "format",
123+ "internal/assertion",
124+ "internal/asyncassertion",
125+ "internal/oraclematcher",
126+ "internal/testingtsupport",
127+ "matchers",
128+ "matchers/support/goraph/bipartitegraph",
129+ "matchers/support/goraph/edge",
130+ "matchers/support/goraph/node",
131+ "matchers/support/goraph/util",
132+ "types",
133+ ]
134+ pruneopts = ""
135+ revision = "90e289841c1ed79b7a598a7cd9959750cb5e89e2"
136+ version = "v1.5.0"
137+
76138[[projects]]
77139 digest = "1:256484dbbcd271f9ecebc6795b2df8cad4c458dd0f5fd82a8c2fa0c29f233411"
78140 name = "github.com/pmezard/go-difflib"
@@ -131,6 +193,9 @@
131193 packages = [
132194 "context",
133195 "context/ctxhttp",
196+ "html",
197+ "html/atom",
198+ "html/charset",
134199 "websocket",
135200 ]
136201 pruneopts = ""
@@ -150,6 +215,42 @@
150215 pruneopts = ""
151216 revision = "9ff8ebcc8e241d46f52ecc5bff0e5a2f2dbef402"
152217
218+[[projects]]
219+ branch = "master"
220+ digest = "1:67a6e61e60283fd7dce50eba228080bff8805d9d69b2f121d7ec2260d120c4a8"
221+ name = "golang.org/x/sys"
222+ packages = ["unix"]
223+ pruneopts = ""
224+ revision = "ca7f33d4116e3a1f9425755d6a44e7ed9b4c97df"
225+
226+[[projects]]
227+ digest = "1:740b51a55815493a8d0f2b1e0d0ae48fe48953bf7eaf3fcc4198823bf67768c0"
228+ name = "golang.org/x/text"
229+ packages = [
230+ "encoding",
231+ "encoding/charmap",
232+ "encoding/htmlindex",
233+ "encoding/internal",
234+ "encoding/internal/identifier",
235+ "encoding/japanese",
236+ "encoding/korean",
237+ "encoding/simplifiedchinese",
238+ "encoding/traditionalchinese",
239+ "encoding/unicode",
240+ "internal/gen",
241+ "internal/language",
242+ "internal/language/compact",
243+ "internal/tag",
244+ "internal/utf8internal",
245+ "language",
246+ "runes",
247+ "transform",
248+ "unicode/cldr",
249+ ]
250+ pruneopts = ""
251+ revision = "342b2e1fbaa52c93f31447ad2c6abc048c63e475"
252+ version = "v0.3.2"
253+
153254[[projects]]
154255 branch = "master"
155256 digest = "1:dc1fb726dbbe79c86369941eae1e3b431b8fc6f11dbd37f7899dc758a43cc3ed"
@@ -182,6 +283,15 @@
182283 revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a"
183284 version = "v1.0.0"
184285
286+[[projects]]
287+ digest = "1:eb53021a8aa3f599d29c7102e65026242bdedce998a54837dc67f14b6a97c5fd"
288+ name = "gopkg.in/fsnotify.v1"
289+ packages = ["."]
290+ pruneopts = ""
291+ revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9"
292+ source = "https://github.com/fsnotify/fsnotify.git"
293+ version = "v1.4.7"
294+
185295[[projects]]
186296 digest = "1:cb5b2a45a3dd41c01ff779c54ae4c8aab0271d6d3b3f734c8a8bd2c890299ef2"
187297 name = "gopkg.in/fsnotify/fsnotify.v1"
@@ -210,6 +320,22 @@
210320 revision = "f8f38de21b4dcd69d0413faf231983f5fd6634b1"
211321 version = "v2.1.3"
212322
323+[[projects]]
324+ branch = "v1"
325+ digest = "1:a96d16bd088460f2e0685d46c39bcf1208ba46e0a977be2df49864ec7da447dd"
326+ name = "gopkg.in/tomb.v1"
327+ packages = ["."]
328+ pruneopts = ""
329+ revision = "dd632973f1e7218eb1089048e0798ec9ae7dceb8"
330+
331+[[projects]]
332+ digest = "1:cedccf16b71e86db87a24f8d4c70b0a855872eb967cb906a66b95de56aefbd0d"
333+ name = "gopkg.in/yaml.v2"
334+ packages = ["."]
335+ pruneopts = ""
336+ revision = "51d6538a90f86fe93ac480b35f37b2be17fef232"
337+ version = "v2.2.2"
338+
213339[solve-meta]
214340 analyzer-name = "dep"
215341 analyzer-version = 1
@@ -220,6 +346,8 @@
220346 "github.com/dgrijalva/jwt-go",
221347 "github.com/mbland/hmacauth",
222348 "github.com/mreiferson/go-options",
349+ "github.com/onsi/ginkgo",
350+ "github.com/onsi/gomega",
223351 "github.com/stretchr/testify/assert",
224352 "github.com/stretchr/testify/require",
225353 "github.com/yhat/wsutil",
@@ -231,6 +359,7 @@
231359 "google.golang.org/api/googleapi",
232360 "gopkg.in/fsnotify/fsnotify.v1",
233361 "gopkg.in/natefinch/lumberjack.v2",
362+ "gopkg.in/square/go-jose.v2",
234363 ]
235364 solver-name = "gps-cdcl"
236365 solver-version = 1
Gopkg.toml+4
@@ -35,6 +35,10 @@
3535 name = "gopkg.in/fsnotify/fsnotify.v1"
3636 version = "~1.2.0"
3737
38+[[override]]
39+ name = "gopkg.in/fsnotify.v1"
40+ source = "https://github.com/fsnotify/fsnotify.git"
41+
3842[[constraint]]
3943 branch = "master"
4044 name = "golang.org/x/crypto"
Makefile+1
@@ -33,6 +33,7 @@ lint: $(GOMETALINTER)
3333 --enable=deadcode \
3434 --enable=gofmt \
3535 --enable=goimports \
36+ --deadline=120s \
3637 --tests ./...
3738
3839.PHONY: dep
docs/3_configuration.md → docs/configuration/configuration.mdrenamed+3−1
@@ -1,7 +1,8 @@
11---
22layout: default
33title: Configuration
4−permalink: /configuration
4+permalink: /docs/configuration
5+has_children: true
56nav_order: 3
67---
78
@@ -78,6 +79,7 @@ Usage of oauth2_proxy:
7879 -request-logging-format: Template for request log lines (see "Logging Configuration" paragraph below)
7980 -resource string: The resource that is protected (Azure AD only)
8081 -scope string: OAuth scope specification
82+ -session-store-type: Session data storage backend (default: cookie)
8183 -set-xauthrequest: set X-Auth-Request-User and X-Auth-Request-Email response headers (useful in Nginx auth_request mode)
8284 -set-authorization-header: set Authorization Bearer response header (useful in Nginx auth_request mode)
8385 -signature-key string: GAP-Signature request signature key (algorithm:secretkey)
docs/configuration/sessions.mdnew file+34
@@ -0,0 +1,34 @@
1+---
2+layout: default
3+title: Sessions
4+permalink: /configuration
5+parent: Configuration
6+nav_order: 3
7+---
8+
9+## Sessions
10+
11+Sessions allow a user's authentication to be tracked between multiple HTTP
12+requests to a service.
13+
14+The OAuth2 Proxy uses a Cookie to track user sessions and will store the session
15+data in one of the available session storage backends.
16+
17+At present the available backends are (as passed to `--session-store-type`):
18+- [cookie](cookie-storage) (deafult)
19+
20+### Cookie Storage
21+
22+The Cookie storage backend is the default backend implementation and has
23+been used in the OAuth2 Proxy historically.
24+
25+With the Cookie storage backend, all session information is stored in client
26+side cookies and transferred with each and every request.
27+
28+The following should be known when using this implementation:
29+- Since all state is stored client side, this storage backend means that the OAuth2 Proxy is completely stateless
30+- Cookies are signed server side to prevent modification client-side
31+- It is recommended to set a `cookie-secret` which will ensure data is encrypted within the cookie data.
32+- Since multiple requests can be made concurrently to the OAuth2 Proxy, this session implementation
33+cannot lock sessions and while updating and refreshing sessions, there can be conflicts which force
34+users to re-authenticate
env_options.go+15−2
@@ -15,14 +15,27 @@ type EnvOptions map[string]interface{}
1515// Fields in the options struct must have an `env` and `cfg` tag to be read
1616// from the environment
1717func (cfg EnvOptions) LoadEnvForStruct(options interface{}) {
18− val := reflect.ValueOf(options).Elem()
19− typ := val.Type()
18+ val := reflect.ValueOf(options)
19+ var typ reflect.Type
20+ if val.Kind() == reflect.Ptr {
21+ typ = val.Elem().Type()
22+ } else {
23+ typ = val.Type()
24+ }
25+
2026 for i := 0; i < typ.NumField(); i++ {
2127 // pull out the struct tags:
2228 // flag - the name of the command line flag
2329 // deprecated - (optional) the name of the deprecated command line flag
2430 // cfg - (optional, defaults to underscored flag) the name of the config file option
2531 field := typ.Field(i)
32+ fieldV := reflect.Indirect(val).Field(i)
33+
34+ if field.Type.Kind() == reflect.Struct && field.Anonymous {
35+ cfg.LoadEnvForStruct(fieldV.Interface())
36+ continue
37+ }
38+
2639 flagName := field.Tag.Get("flag")
2740 envName := field.Tag.Get("env")
2841 cfgName := field.Tag.Get("cfg")
env_options_test.go+26−6
@@ -1,26 +1,46 @@
1−package main
1+package main_test
22
33import (
44 "os"
55 "testing"
66
7+ proxy "github.com/pusher/oauth2_proxy"
78 "github.com/stretchr/testify/assert"
89)
910
10−type envTest struct {
11− testField string `cfg:"target_field" env:"TEST_ENV_FIELD"`
11+type EnvTest struct {
12+ TestField string `cfg:"target_field" env:"TEST_ENV_FIELD"`
13+ EnvTestEmbed
14+}
15+
16+type EnvTestEmbed struct {
17+ TestFieldEmbed string `cfg:"target_field_embed" env:"TEST_ENV_FIELD_EMBED"`
1218}
1319
1420func TestLoadEnvForStruct(t *testing.T) {
1521
16− cfg := make(EnvOptions)
17− cfg.LoadEnvForStruct(&envTest{})
22+ cfg := make(proxy.EnvOptions)
23+ cfg.LoadEnvForStruct(&EnvTest{})
1824
1925 _, ok := cfg["target_field"]
2026 assert.Equal(t, ok, false)
2127
2228 os.Setenv("TEST_ENV_FIELD", "1234abcd")
23− cfg.LoadEnvForStruct(&envTest{})
29+ cfg.LoadEnvForStruct(&EnvTest{})
2430 v := cfg["target_field"]
2531 assert.Equal(t, v, "1234abcd")
2632}
33+
34+func TestLoadEnvForStructWithEmbeddedFields(t *testing.T) {
35+
36+ cfg := make(proxy.EnvOptions)
37+ cfg.LoadEnvForStruct(&EnvTest{})
38+
39+ _, ok := cfg["target_field_embed"]
40+ assert.Equal(t, ok, false)
41+
42+ os.Setenv("TEST_ENV_FIELD_EMBED", "1234abcd")
43+ cfg.LoadEnvForStruct(&EnvTest{})
44+ v := cfg["target_field_embed"]
45+ assert.Equal(t, v, "1234abcd")
46+}
main.go+2
@@ -75,6 +75,8 @@ func main() {
7575 flagSet.Bool("cookie-secure", true, "set secure (HTTPS) cookie flag")
7676 flagSet.Bool("cookie-httponly", true, "set HttpOnly cookie flag")
7777
78+ flagSet.String("session-store-type", "cookie", "the session storage provider to use")
79+
7880 flagSet.String("logging-filename", "", "File to log requests to, empty for stdout")
7981 flagSet.Int("logging-max-size", 100, "Maximum size in megabytes of the log file before rotation")
8082 flagSet.Int("logging-max-age", 7, "Maximum number of days to retain old log files")
oauthproxy.go+7−6
@@ -16,6 +16,7 @@ import (
1616 "github.com/mbland/hmacauth"
1717 "github.com/pusher/oauth2_proxy/cookie"
1818 "github.com/pusher/oauth2_proxy/logger"
19+ "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
1920 "github.com/pusher/oauth2_proxy/providers"
2021 "github.com/yhat/wsutil"
2122)
@@ -292,7 +293,7 @@ func (p *OAuthProxy) displayCustomLoginForm() bool {
292293 return p.HtpasswdFile != nil && p.DisplayHtpasswdForm
293294}
294295
295−func (p *OAuthProxy) redeemCode(host, code string) (s *providers.SessionState, err error) {
296+func (p *OAuthProxy) redeemCode(host, code string) (s *sessions.SessionState, err error) {
296297 if code == "" {
297298 return nil, errors.New("missing code")
298299 }
@@ -484,7 +485,7 @@ func (p *OAuthProxy) SetSessionCookie(rw http.ResponseWriter, req *http.Request,
484485}
485486
486487// LoadCookiedSession reads the user's authentication details from the request
487−func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*providers.SessionState, time.Duration, error) {
488+func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*sessions.SessionState, time.Duration, error) {
488489 var age time.Duration
489490 c, err := loadCookie(req, p.CookieName)
490491 if err != nil {
@@ -506,7 +507,7 @@ func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*providers.SessionSt
506507}
507508
508509// SaveSession creates a new session cookie value and sets this on the response
509−func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *providers.SessionState) error {
510+func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *sessions.SessionState) error {
510511 value, err := p.provider.CookieForSession(s, p.CookieCipher)
511512 if err != nil {
512513 return err
@@ -693,7 +694,7 @@ func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
693694
694695 user, ok := p.ManualSignIn(rw, req)
695696 if ok {
696− session := &providers.SessionState{User: user}
697+ session := &sessions.SessionState{User: user}
697698 p.SaveSession(rw, req, session)
698699 http.Redirect(rw, req, redirect, 302)
699700 } else {
@@ -944,7 +945,7 @@ func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int
944945
945946// CheckBasicAuth checks the requests Authorization header for basic auth
946947// credentials and authenticates these against the proxies HtpasswdFile
947−func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*providers.SessionState, error) {
948+func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*sessions.SessionState, error) {
948949 if p.HtpasswdFile == nil {
949950 return nil, nil
950951 }
@@ -966,7 +967,7 @@ func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*providers.SessionState,
966967 }
967968 if p.HtpasswdFile.Validate(pair[0], pair[1]) {
968969 logger.PrintAuthf(pair[0], req, logger.AuthSuccess, "Authenticated via basic auth and HTpasswd File")
969− return &providers.SessionState{User: pair[0]}, nil
970+ return &sessions.SessionState{User: pair[0]}, nil
970971 }
971972 logger.PrintAuthf(pair[0], req, logger.AuthFailure, "Invalid authentication via basic auth: not in Htpasswd File")
972973 return nil, nil
oauthproxy_test.go+14−13
@@ -16,6 +16,7 @@ import (
1616
1717 "github.com/mbland/hmacauth"
1818 "github.com/pusher/oauth2_proxy/logger"
19+ "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
1920 "github.com/pusher/oauth2_proxy/providers"
2021 "github.com/stretchr/testify/assert"
2122 "github.com/stretchr/testify/require"
@@ -253,11 +254,11 @@ func NewTestProvider(providerURL *url.URL, emailAddress string) *TestProvider {
253254 }
254255}
255256
256−func (tp *TestProvider) GetEmailAddress(session *providers.SessionState) (string, error) {
257+func (tp *TestProvider) GetEmailAddress(session *sessions.SessionState) (string, error) {
257258 return tp.EmailAddress, nil
258259}
259260
260−func (tp *TestProvider) ValidateSessionState(session *providers.SessionState) bool {
261+func (tp *TestProvider) ValidateSessionState(session *sessions.SessionState) bool {
261262 return tp.ValidToken
262263}
263264
@@ -637,7 +638,7 @@ func (p *ProcessCookieTest) MakeCookie(value string, ref time.Time) []*http.Cook
637638 return p.proxy.MakeSessionCookie(p.req, value, p.opts.CookieExpire, ref)
638639}
639640
640−func (p *ProcessCookieTest) SaveSession(s *providers.SessionState, ref time.Time) error {
641+func (p *ProcessCookieTest) SaveSession(s *sessions.SessionState, ref time.Time) error {
641642 value, err := p.proxy.provider.CookieForSession(s, p.proxy.CookieCipher)
642643 if err != nil {
643644 return err
@@ -648,14 +649,14 @@ func (p *ProcessCookieTest) SaveSession(s *providers.SessionState, ref time.Time
648649 return nil
649650}
650651
651−func (p *ProcessCookieTest) LoadCookiedSession() (*providers.SessionState, time.Duration, error) {
652+func (p *ProcessCookieTest) LoadCookiedSession() (*sessions.SessionState, time.Duration, error) {
652653 return p.proxy.LoadCookiedSession(p.req)
653654}
654655
655656func TestLoadCookiedSession(t *testing.T) {
656657 pcTest := NewProcessCookieTestWithDefaults()
657658
658− startSession := &providers.SessionState{Email: "john.doe@example.com", AccessToken: "my_access_token"}
659+ startSession := &sessions.SessionState{Email: "john.doe@example.com", AccessToken: "my_access_token"}
659660 pcTest.SaveSession(startSession, time.Now())
660661
661662 session, _, err := pcTest.LoadCookiedSession()
@@ -680,7 +681,7 @@ func TestProcessCookieRefreshNotSet(t *testing.T) {
680681 pcTest.proxy.CookieExpire = time.Duration(23) * time.Hour
681682 reference := time.Now().Add(time.Duration(-2) * time.Hour)
682683
683− startSession := &providers.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"}
684+ startSession := &sessions.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"}
684685 pcTest.SaveSession(startSession, reference)
685686
686687 session, age, err := pcTest.LoadCookiedSession()
@@ -695,7 +696,7 @@ func TestProcessCookieFailIfCookieExpired(t *testing.T) {
695696 pcTest := NewProcessCookieTestWithDefaults()
696697 pcTest.proxy.CookieExpire = time.Duration(24) * time.Hour
697698 reference := time.Now().Add(time.Duration(25) * time.Hour * -1)
698− startSession := &providers.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"}
699+ startSession := &sessions.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"}
699700 pcTest.SaveSession(startSession, reference)
700701
701702 session, _, err := pcTest.LoadCookiedSession()
@@ -709,7 +710,7 @@ func TestProcessCookieFailIfRefreshSetAndCookieExpired(t *testing.T) {
709710 pcTest := NewProcessCookieTestWithDefaults()
710711 pcTest.proxy.CookieExpire = time.Duration(24) * time.Hour
711712 reference := time.Now().Add(time.Duration(25) * time.Hour * -1)
712− startSession := &providers.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"}
713+ startSession := &sessions.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"}
713714 pcTest.SaveSession(startSession, reference)
714715
715716 pcTest.proxy.CookieRefresh = time.Hour
@@ -729,7 +730,7 @@ func NewAuthOnlyEndpointTest() *ProcessCookieTest {
729730
730731func TestAuthOnlyEndpointAccepted(t *testing.T) {
731732 test := NewAuthOnlyEndpointTest()
732− startSession := &providers.SessionState{
733+ startSession := &sessions.SessionState{
733734 Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"}
734735 test.SaveSession(startSession, time.Now())
735736
@@ -752,7 +753,7 @@ func TestAuthOnlyEndpointUnauthorizedOnExpiration(t *testing.T) {
752753 test := NewAuthOnlyEndpointTest()
753754 test.proxy.CookieExpire = time.Duration(24) * time.Hour
754755 reference := time.Now().Add(time.Duration(25) * time.Hour * -1)
755− startSession := &providers.SessionState{
756+ startSession := &sessions.SessionState{
756757 Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"}
757758 test.SaveSession(startSession, reference)
758759
@@ -764,7 +765,7 @@ func TestAuthOnlyEndpointUnauthorizedOnExpiration(t *testing.T) {
764765
765766func TestAuthOnlyEndpointUnauthorizedOnEmailValidationFailure(t *testing.T) {
766767 test := NewAuthOnlyEndpointTest()
767− startSession := &providers.SessionState{
768+ startSession := &sessions.SessionState{
768769 Email: "michael.bland@gsa.gov", AccessToken: "my_access_token"}
769770 test.SaveSession(startSession, time.Now())
770771 test.validateUser = false
@@ -795,7 +796,7 @@ func TestAuthOnlyEndpointSetXAuthRequestHeaders(t *testing.T) {
795796 pcTest.req, _ = http.NewRequest("GET",
796797 pcTest.opts.ProxyPrefix+"/auth", nil)
797798
798− startSession := &providers.SessionState{
799+ startSession := &sessions.SessionState{
799800 User: "oauth_user", Email: "oauth_user@example.com", AccessToken: "oauth_token"}
800801 pcTest.SaveSession(startSession, time.Now())
801802
@@ -927,7 +928,7 @@ func (st *SignatureTest) MakeRequestWithExpectedKey(method, body, key string) {
927928 req := httptest.NewRequest(method, "/foo/bar", bodyBuf)
928929 req.Header = st.header
929930
930− state := &providers.SessionState{
931+ state := &sessions.SessionState{
931932 Email: "mbland@acm.org", AccessToken: "my_access_token"}
932933 value, err := proxy.provider.CookieForSession(state, proxy.CookieCipher)
933934 if err != nil {
options.go+18−18
@@ -18,6 +18,7 @@ import (
1818 "github.com/dgrijalva/jwt-go"
1919 "github.com/mbland/hmacauth"
2020 "github.com/pusher/oauth2_proxy/logger"
21+ "github.com/pusher/oauth2_proxy/pkg/apis/options"
2122 "github.com/pusher/oauth2_proxy/providers"
2223 "gopkg.in/natefinch/lumberjack.v2"
2324)
@@ -49,14 +50,11 @@ type Options struct {
4950 CustomTemplatesDir string `flag:"custom-templates-dir" cfg:"custom_templates_dir" env:"OAUTH2_PROXY_CUSTOM_TEMPLATES_DIR"`
5051 Footer string `flag:"footer" cfg:"footer" env:"OAUTH2_PROXY_FOOTER"`
5152
52− CookieName string `flag:"cookie-name" cfg:"cookie_name" env:"OAUTH2_PROXY_COOKIE_NAME"`
53− CookieSecret string `flag:"cookie-secret" cfg:"cookie_secret" env:"OAUTH2_PROXY_COOKIE_SECRET"`
54− CookieDomain string `flag:"cookie-domain" cfg:"cookie_domain" env:"OAUTH2_PROXY_COOKIE_DOMAIN"`
55− CookiePath string `flag:"cookie-path" cfg:"cookie_path" env:"OAUTH2_PROXY_COOKIE_PATH"`
56− CookieExpire time.Duration `flag:"cookie-expire" cfg:"cookie_expire" env:"OAUTH2_PROXY_COOKIE_EXPIRE"`
57− CookieRefresh time.Duration `flag:"cookie-refresh" cfg:"cookie_refresh" env:"OAUTH2_PROXY_COOKIE_REFRESH"`
58− CookieSecure bool `flag:"cookie-secure" cfg:"cookie_secure" env:"OAUTH2_PROXY_COOKIE_SECURE"`
59− CookieHTTPOnly bool `flag:"cookie-httponly" cfg:"cookie_httponly" env:"OAUTH2_PROXY_COOKIE_HTTPONLY"`
53+ // Embed CookieOptions
54+ options.CookieOptions
55+
56+ // Embed SessionOptions
57+ options.SessionOptions
6058
6159 Upstreams []string `flag:"upstream" cfg:"upstreams" env:"OAUTH2_PROXY_UPSTREAMS"`
6260 SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex" env:"OAUTH2_PROXY_SKIP_AUTH_REGEX"`
@@ -126,16 +124,18 @@ type SignatureData struct {
126124// NewOptions constructs a new Options with defaulted values
127125func NewOptions() *Options {
128126 return &Options{
129− ProxyPrefix: "/oauth2",
130− ProxyWebSockets: true,
131− HTTPAddress: "127.0.0.1:4180",
132− HTTPSAddress: ":443",
133− DisplayHtpasswdForm: true,
134− CookieName: "_oauth2_proxy",
135− CookieSecure: true,
136− CookieHTTPOnly: true,
137− CookieExpire: time.Duration(168) * time.Hour,
138− CookieRefresh: time.Duration(0),
127+ ProxyPrefix: "/oauth2",
128+ ProxyWebSockets: true,
129+ HTTPAddress: "127.0.0.1:4180",
130+ HTTPSAddress: ":443",
131+ DisplayHtpasswdForm: true,
132+ CookieOptions: options.CookieOptions{
133+ CookieName: "_oauth2_proxy",
134+ CookieSecure: true,
135+ CookieHTTPOnly: true,
136+ CookieExpire: time.Duration(168) * time.Hour,
137+ CookieRefresh: time.Duration(0),
138+ },
139139 SetXAuthRequest: false,
140140 SkipAuthPreflight: false,
141141 PassBasicAuth: true,
pkg/apis/options/cookie.gonew file+15
@@ -0,0 +1,15 @@
1+package options
2+
3+import "time"
4+
5+// CookieOptions contains configuration options relating to Cookie configuration
6+type CookieOptions struct {
7+ CookieName string `flag:"cookie-name" cfg:"cookie_name" env:"OAUTH2_PROXY_COOKIE_NAME"`
8+ CookieSecret string `flag:"cookie-secret" cfg:"cookie_secret" env:"OAUTH2_PROXY_COOKIE_SECRET"`
9+ CookieDomain string `flag:"cookie-domain" cfg:"cookie_domain" env:"OAUTH2_PROXY_COOKIE_DOMAIN"`
10+ CookiePath string `flag:"cookie-path" cfg:"cookie_path" env:"OAUTH2_PROXY_COOKIE_PATH"`
11+ CookieExpire time.Duration `flag:"cookie-expire" cfg:"cookie_expire" env:"OAUTH2_PROXY_COOKIE_EXPIRE"`
12+ CookieRefresh time.Duration `flag:"cookie-refresh" cfg:"cookie_refresh" env:"OAUTH2_PROXY_COOKIE_REFRESH"`
13+ CookieSecure bool `flag:"cookie-secure" cfg:"cookie_secure" env:"OAUTH2_PROXY_COOKIE_SECURE"`
14+ CookieHTTPOnly bool `flag:"cookie-httponly" cfg:"cookie_httponly" env:"OAUTH2_PROXY_COOKIE_HTTPONLY"`
15+}
pkg/apis/options/sessions.gonew file+14
@@ -0,0 +1,14 @@
1+package options
2+
3+// SessionOptions contains configuration options for the SessionStore providers.
4+type SessionOptions struct {
5+ Type string `flag:"session-store-type" cfg:"session_store_type" env:"OAUTH2_PROXY_SESSION_STORE_TYPE"`
6+ CookieStoreOptions
7+}
8+
9+// CookieSessionStoreType is used to indicate the CookieSessionStore should be
10+// used for storing sessions.
11+var CookieSessionStoreType = "cookie"
12+
13+// CookieStoreOptions contains configuration options for the CookieSessionStore.
14+type CookieStoreOptions struct{}
pkg/apis/sessions/interfaces.gonew file+12
@@ -0,0 +1,12 @@
1+package sessions
2+
3+import (
4+ "net/http"
5+)
6+
7+// SessionStore is an interface to storing user sessions in the proxy
8+type SessionStore interface {
9+ Save(rw http.ResponseWriter, req *http.Request, s *SessionState) error
10+ Load(req *http.Request) (*SessionState, error)
11+ Clear(rw http.ResponseWriter, req *http.Request) error
12+}
providers/session_state.go → pkg/apis/sessions/session_state.gorenamed+1−1
@@ -1,4 +1,4 @@
1−package providers
1+package sessions
22
33import (
44 "encoding/json"
providers/session_state_test.go → pkg/apis/sessions/session_state_test.gorenamed+31−30
@@ -1,4 +1,4 @@
1−package providers
1+package sessions_test
22
33import (
44 "fmt"
@@ -6,6 +6,7 @@ import (
66 "time"
77
88 "github.com/pusher/oauth2_proxy/cookie"
9+ "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
910 "github.com/stretchr/testify/assert"
1011)
1112
@@ -17,7 +18,7 @@ func TestSessionStateSerialization(t *testing.T) {
1718 assert.Equal(t, nil, err)
1819 c2, err := cookie.NewCipher([]byte(altSecret))
1920 assert.Equal(t, nil, err)
20− s := &SessionState{
21+ s := &sessions.SessionState{
2122 Email: "user@domain.com",
2223 AccessToken: "token1234",
2324 IDToken: "rawtoken1234",
@@ -27,7 +28,7 @@ func TestSessionStateSerialization(t *testing.T) {
2728 encoded, err := s.EncodeSessionState(c)
2829 assert.Equal(t, nil, err)
2930
30− ss, err := DecodeSessionState(encoded, c)
31+ ss, err := sessions.DecodeSessionState(encoded, c)
3132 t.Logf("%#v", ss)
3233 assert.Equal(t, nil, err)
3334 assert.Equal(t, "user@domain.com", ss.User)
@@ -38,7 +39,7 @@ func TestSessionStateSerialization(t *testing.T) {
3839 assert.Equal(t, s.RefreshToken, ss.RefreshToken)
3940
4041 // ensure a different cipher can't decode properly (ie: it gets gibberish)
41− ss, err = DecodeSessionState(encoded, c2)
42+ ss, err = sessions.DecodeSessionState(encoded, c2)
4243 t.Logf("%#v", ss)
4344 assert.Equal(t, nil, err)
4445 assert.NotEqual(t, "user@domain.com", ss.User)
@@ -54,7 +55,7 @@ func TestSessionStateSerializationWithUser(t *testing.T) {
5455 assert.Equal(t, nil, err)
5556 c2, err := cookie.NewCipher([]byte(altSecret))
5657 assert.Equal(t, nil, err)
57− s := &SessionState{
58+ s := &sessions.SessionState{
5859 User: "just-user",
5960 Email: "user@domain.com",
6061 AccessToken: "token1234",
@@ -64,7 +65,7 @@ func TestSessionStateSerializationWithUser(t *testing.T) {
6465 encoded, err := s.EncodeSessionState(c)
6566 assert.Equal(t, nil, err)
6667
67− ss, err := DecodeSessionState(encoded, c)
68+ ss, err := sessions.DecodeSessionState(encoded, c)
6869 t.Logf("%#v", ss)
6970 assert.Equal(t, nil, err)
7071 assert.Equal(t, s.User, ss.User)
@@ -74,7 +75,7 @@ func TestSessionStateSerializationWithUser(t *testing.T) {
7475 assert.Equal(t, s.RefreshToken, ss.RefreshToken)
7576
7677 // ensure a different cipher can't decode properly (ie: it gets gibberish)
77− ss, err = DecodeSessionState(encoded, c2)
78+ ss, err = sessions.DecodeSessionState(encoded, c2)
7879 t.Logf("%#v", ss)
7980 assert.Equal(t, nil, err)
8081 assert.NotEqual(t, s.User, ss.User)
@@ -85,7 +86,7 @@ func TestSessionStateSerializationWithUser(t *testing.T) {
8586}
8687
8788func TestSessionStateSerializationNoCipher(t *testing.T) {
88− s := &SessionState{
89+ s := &sessions.SessionState{
8990 Email: "user@domain.com",
9091 AccessToken: "token1234",
9192 ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
@@ -95,7 +96,7 @@ func TestSessionStateSerializationNoCipher(t *testing.T) {
9596 assert.Equal(t, nil, err)
9697
9798 // only email should have been serialized
98− ss, err := DecodeSessionState(encoded, nil)
99+ ss, err := sessions.DecodeSessionState(encoded, nil)
99100 assert.Equal(t, nil, err)
100101 assert.Equal(t, "user@domain.com", ss.User)
101102 assert.Equal(t, s.Email, ss.Email)
@@ -104,7 +105,7 @@ func TestSessionStateSerializationNoCipher(t *testing.T) {
104105}
105106
106107func TestSessionStateSerializationNoCipherWithUser(t *testing.T) {
107− s := &SessionState{
108+ s := &sessions.SessionState{
108109 User: "just-user",
109110 Email: "user@domain.com",
110111 AccessToken: "token1234",
@@ -115,7 +116,7 @@ func TestSessionStateSerializationNoCipherWithUser(t *testing.T) {
115116 assert.Equal(t, nil, err)
116117
117118 // only email should have been serialized
118− ss, err := DecodeSessionState(encoded, nil)
119+ ss, err := sessions.DecodeSessionState(encoded, nil)
119120 assert.Equal(t, nil, err)
120121 assert.Equal(t, s.User, ss.User)
121122 assert.Equal(t, s.Email, ss.Email)
@@ -124,18 +125,18 @@ func TestSessionStateSerializationNoCipherWithUser(t *testing.T) {
124125}
125126
126127func TestExpired(t *testing.T) {
127− s := &SessionState{ExpiresOn: time.Now().Add(time.Duration(-1) * time.Minute)}
128+ s := &sessions.SessionState{ExpiresOn: time.Now().Add(time.Duration(-1) * time.Minute)}
128129 assert.Equal(t, true, s.IsExpired())
129130
130− s = &SessionState{ExpiresOn: time.Now().Add(time.Duration(1) * time.Minute)}
131+ s = &sessions.SessionState{ExpiresOn: time.Now().Add(time.Duration(1) * time.Minute)}
131132 assert.Equal(t, false, s.IsExpired())
132133
133− s = &SessionState{}
134+ s = &sessions.SessionState{}
134135 assert.Equal(t, false, s.IsExpired())
135136}
136137
137138type testCase struct {
138− SessionState
139+ sessions.SessionState
139140 Encoded string
140141 Cipher *cookie.Cipher
141142 Error bool
@@ -150,14 +151,14 @@ func TestEncodeSessionState(t *testing.T) {
150151
151152 testCases := []testCase{
152153 {
153− SessionState: SessionState{
154+ SessionState: sessions.SessionState{
154155 Email: "user@domain.com",
155156 User: "just-user",
156157 },
157158 Encoded: `{"Email":"user@domain.com","User":"just-user"}`,
158159 },
159160 {
160− SessionState: SessionState{
161+ SessionState: sessions.SessionState{
161162 Email: "user@domain.com",
162163 User: "just-user",
163164 AccessToken: "token1234",
@@ -171,7 +172,7 @@ func TestEncodeSessionState(t *testing.T) {
171172
172173 for i, tc := range testCases {
173174 encoded, err := tc.EncodeSessionState(tc.Cipher)
174− t.Logf("i:%d Encoded:%#v SessionState:%#v Error:%#v", i, encoded, tc.SessionState, err)
175+ t.Logf("i:%d Encoded:%#vsessions.SessionState:%#v Error:%#v", i, encoded, tc.SessionState, err)
175176 if tc.Error {
176177 assert.Error(t, err)
177178 assert.Empty(t, encoded)
@@ -182,7 +183,7 @@ func TestEncodeSessionState(t *testing.T) {
182183 }
183184}
184185
185−// TestDecodeSessionState tests DecodeSessionState with the test vector
186+// TestDecodeSessionState testssessions.DecodeSessionState with the test vector
186187func TestDecodeSessionState(t *testing.T) {
187188 e := time.Now().Add(time.Duration(1) * time.Hour)
188189 eJSON, _ := e.MarshalJSON()
@@ -194,34 +195,34 @@ func TestDecodeSessionState(t *testing.T) {
194195
195196 testCases := []testCase{
196197 {
197− SessionState: SessionState{
198+ SessionState: sessions.SessionState{
198199 Email: "user@domain.com",
199200 User: "just-user",
200201 },
201202 Encoded: `{"Email":"user@domain.com","User":"just-user"}`,
202203 },
203204 {
204− SessionState: SessionState{
205+ SessionState: sessions.SessionState{
205206 Email: "user@domain.com",
206207 User: "user@domain.com",
207208 },
208209 Encoded: `{"Email":"user@domain.com"}`,
209210 },
210211 {
211− SessionState: SessionState{
212+ SessionState: sessions.SessionState{
212213 User: "just-user",
213214 },
214215 Encoded: `{"User":"just-user"}`,
215216 },
216217 {
217− SessionState: SessionState{
218+ SessionState: sessions.SessionState{
218219 Email: "user@domain.com",
219220 User: "just-user",
220221 },
221222 Encoded: fmt.Sprintf(`{"Email":"user@domain.com","User":"just-user","AccessToken":"I6s+ml+/MldBMgHIiC35BTKTh57skGX24w==","IDToken":"xojNdyyjB1HgYWh6XMtXY/Ph5eCVxa1cNsklJw==","RefreshToken":"qEX0x6RmASxo4dhlBG6YuRs9Syn/e9sHu/+K","ExpiresOn":%s}`, eString),
222223 },
223224 {
224− SessionState: SessionState{
225+ SessionState: sessions.SessionState{
225226 Email: "user@domain.com",
226227 User: "just-user",
227228 AccessToken: "token1234",
@@ -233,7 +234,7 @@ func TestDecodeSessionState(t *testing.T) {
233234 Cipher: c,
234235 },
235236 {
236− SessionState: SessionState{
237+ SessionState: sessions.SessionState{
237238 Email: "user@domain.com",
238239 User: "just-user",
239240 },
@@ -251,7 +252,7 @@ func TestDecodeSessionState(t *testing.T) {
251252 Error: true,
252253 },
253254 {
254− SessionState: SessionState{
255+ SessionState: sessions.SessionState{
255256 User: "just-user",
256257 Email: "user@domain.com",
257258 },
@@ -272,7 +273,7 @@ func TestDecodeSessionState(t *testing.T) {
272273 Error: true,
273274 },
274275 {
275− SessionState: SessionState{
276+ SessionState: sessions.SessionState{
276277 Email: "user@domain.com",
277278 User: "just-user",
278279 AccessToken: "token1234",
@@ -283,7 +284,7 @@ func TestDecodeSessionState(t *testing.T) {
283284 Cipher: c,
284285 },
285286 {
286− SessionState: SessionState{
287+ SessionState: sessions.SessionState{
287288 Email: "user@domain.com",
288289 User: "just-user",
289290 AccessToken: "token1234",
@@ -297,8 +298,8 @@ func TestDecodeSessionState(t *testing.T) {
297298 }
298299
299300 for i, tc := range testCases {
300− ss, err := DecodeSessionState(tc.Encoded, tc.Cipher)
301− t.Logf("i:%d Encoded:%#v SessionState:%#v Error:%#v", i, tc.Encoded, ss, err)
301+ ss, err := sessions.DecodeSessionState(tc.Encoded, tc.Cipher)
302+ t.Logf("i:%d Encoded:%#vsessions.SessionState:%#v Error:%#v", i, tc.Encoded, ss, err)
302303 if tc.Error {
303304 assert.Error(t, err)
304305 assert.Nil(t, ss)
pkg/cookies/cookies.gonew file+34
@@ -0,0 +1,34 @@
1+package cookies
2+
3+import (
4+ "net"
5+ "net/http"
6+ "strings"
7+ "time"
8+
9+ "github.com/pusher/oauth2_proxy/logger"
10+)
11+
12+// MakeCookie constructs a cookie from the given parameters,
13+// discovering the domain from the request if not specified.
14+func MakeCookie(req *http.Request, name string, value string, path string, domain string, httpOnly bool, secure bool, expiration time.Duration, now time.Time) *http.Cookie {
15+ if domain != "" {
16+ host := req.Host
17+ if h, _, err := net.SplitHostPort(host); err == nil {
18+ host = h
19+ }
20+ if !strings.HasSuffix(host, domain) {
21+ logger.Printf("Warning: request host is %q but using configured cookie domain of %q", host, domain)
22+ }
23+ }
24+
25+ return &http.Cookie{
26+ Name: name,
27+ Value: value,
28+ Path: path,
29+ Domain: domain,
30+ HttpOnly: httpOnly,
31+ Secure: secure,
32+ Expires: now.Add(expiration),
33+ }
34+}
pkg/sessions/cookie/session_store.gonew file+232
@@ -0,0 +1,232 @@
1+package cookie
2+
3+import (
4+ "errors"
5+ "fmt"
6+ "net/http"
7+ "regexp"
8+ "strings"
9+ "time"
10+
11+ "github.com/pusher/oauth2_proxy/cookie"
12+ "github.com/pusher/oauth2_proxy/pkg/apis/options"
13+ "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
14+ "github.com/pusher/oauth2_proxy/pkg/cookies"
15+ "github.com/pusher/oauth2_proxy/pkg/sessions/utils"
16+)
17+
18+const (
19+ // Cookies are limited to 4kb including the length of the cookie name,
20+ // the cookie name can be up to 256 bytes
21+ maxCookieLength = 3840
22+)
23+
24+// Ensure CookieSessionStore implements the interface
25+var _ sessions.SessionStore = &SessionStore{}
26+
27+// SessionStore is an implementation of the sessions.SessionStore
28+// interface that stores sessions in client side cookies
29+type SessionStore struct {
30+ CookieCipher *cookie.Cipher
31+ CookieDomain string
32+ CookieExpire time.Duration
33+ CookieHTTPOnly bool
34+ CookieName string
35+ CookiePath string
36+ CookieSecret string
37+ CookieSecure bool
38+}
39+
40+// Save takes a sessions.SessionState and stores the information from it
41+// within Cookies set on the HTTP response writer
42+func (s *SessionStore) Save(rw http.ResponseWriter, req *http.Request, ss *sessions.SessionState) error {
43+ value, err := utils.CookieForSession(ss, s.CookieCipher)
44+ if err != nil {
45+ return err
46+ }
47+ s.setSessionCookie(rw, req, value)
48+ return nil
49+}
50+
51+// Load reads sessions.SessionState information from Cookies within the
52+// HTTP request object
53+func (s *SessionStore) Load(req *http.Request) (*sessions.SessionState, error) {
54+ c, err := loadCookie(req, s.CookieName)
55+ if err != nil {
56+ // always http.ErrNoCookie
57+ return nil, fmt.Errorf("Cookie %q not present", s.CookieName)
58+ }
59+ val, _, ok := cookie.Validate(c, s.CookieSecret, s.CookieExpire)
60+ if !ok {
61+ return nil, errors.New("Cookie Signature not valid")
62+ }
63+
64+ session, err := utils.SessionFromCookie(val, s.CookieCipher)
65+ if err != nil {
66+ return nil, err
67+ }
68+ return session, nil
69+}
70+
71+// Clear clears any saved session information by writing a cookie to
72+// clear the session
73+func (s *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) error {
74+ var cookies []*http.Cookie
75+
76+ // matches CookieName, CookieName_<number>
77+ var cookieNameRegex = regexp.MustCompile(fmt.Sprintf("^%s(_\\d+)?$", s.CookieName))
78+
79+ for _, c := range req.Cookies() {
80+ if cookieNameRegex.MatchString(c.Name) {
81+ clearCookie := s.makeCookie(req, c.Name, "", time.Hour*-1)
82+
83+ http.SetCookie(rw, clearCookie)
84+ cookies = append(cookies, clearCookie)
85+ }
86+ }
87+
88+ return nil
89+}
90+
91+// setSessionCookie adds the user's session cookie to the response
92+func (s *SessionStore) setSessionCookie(rw http.ResponseWriter, req *http.Request, val string) {
93+ for _, c := range s.makeSessionCookie(req, val, s.CookieExpire, time.Now()) {
94+ http.SetCookie(rw, c)
95+ }
96+}
97+
98+// makeSessionCookie creates an http.Cookie containing the authenticated user's
99+// authentication details
100+func (s *SessionStore) makeSessionCookie(req *http.Request, value string, expiration time.Duration, now time.Time) []*http.Cookie {
101+ if value != "" {
102+ value = cookie.SignedValue(s.CookieSecret, s.CookieName, value, now)
103+ }
104+ c := s.makeCookie(req, s.CookieName, value, expiration)
105+ if len(c.Value) > 4096-len(s.CookieName) {
106+ return splitCookie(c)
107+ }
108+ return []*http.Cookie{c}
109+}
110+
111+func (s *SessionStore) makeCookie(req *http.Request, name string, value string, expiration time.Duration) *http.Cookie {
112+ return cookies.MakeCookie(
113+ req,
114+ name,
115+ value,
116+ s.CookiePath,
117+ s.CookieDomain,
118+ s.CookieHTTPOnly,
119+ s.CookieSecure,
120+ expiration,
121+ time.Now(),
122+ )
123+}
124+
125+// NewCookieSessionStore initialises a new instance of the SessionStore from
126+// the configuration given
127+func NewCookieSessionStore(opts options.CookieStoreOptions, cookieOpts *options.CookieOptions) (sessions.SessionStore, error) {
128+ var cipher *cookie.Cipher
129+ if len(cookieOpts.CookieSecret) > 0 {
130+ var err error
131+ cipher, err = cookie.NewCipher(utils.SecretBytes(cookieOpts.CookieSecret))
132+ if err != nil {
133+ return nil, fmt.Errorf("unable to create cipher: %v", err)
134+ }
135+ }
136+
137+ return &SessionStore{
138+ CookieCipher: cipher,
139+ CookieDomain: cookieOpts.CookieDomain,
140+ CookieExpire: cookieOpts.CookieExpire,
141+ CookieHTTPOnly: cookieOpts.CookieHTTPOnly,
142+ CookieName: cookieOpts.CookieName,
143+ CookiePath: cookieOpts.CookiePath,
144+ CookieSecret: cookieOpts.CookieSecret,
145+ CookieSecure: cookieOpts.CookieSecure,
146+ }, nil
147+}
148+
149+// splitCookie reads the full cookie generated to store the session and splits
150+// it into a slice of cookies which fit within the 4kb cookie limit indexing
151+// the cookies from 0
152+func splitCookie(c *http.Cookie) []*http.Cookie {
153+ if len(c.Value) < maxCookieLength {
154+ return []*http.Cookie{c}
155+ }
156+ cookies := []*http.Cookie{}
157+ valueBytes := []byte(c.Value)
158+ count := 0
159+ for len(valueBytes) > 0 {
160+ new := copyCookie(c)
161+ new.Name = fmt.Sprintf("%s_%d", c.Name, count)
162+ count++
163+ if len(valueBytes) < maxCookieLength {
164+ new.Value = string(valueBytes)
165+ valueBytes = []byte{}
166+ } else {
167+ newValue := valueBytes[:maxCookieLength]
168+ valueBytes = valueBytes[maxCookieLength:]
169+ new.Value = string(newValue)
170+ }
171+ cookies = append(cookies, new)
172+ }
173+ return cookies
174+}
175+
176+// loadCookie retreieves the sessions state cookie from the http request.
177+// If a single cookie is present this will be returned, otherwise it attempts
178+// to reconstruct a cookie split up by splitCookie
179+func loadCookie(req *http.Request, cookieName string) (*http.Cookie, error) {
180+ c, err := req.Cookie(cookieName)
181+ if err == nil {
182+ return c, nil
183+ }
184+ cookies := []*http.Cookie{}
185+ err = nil
186+ count := 0
187+ for err == nil {
188+ var c *http.Cookie
189+ c, err = req.Cookie(fmt.Sprintf("%s_%d", cookieName, count))
190+ if err == nil {
191+ cookies = append(cookies, c)
192+ count++
193+ }
194+ }
195+ if len(cookies) == 0 {
196+ return nil, fmt.Errorf("Could not find cookie %s", cookieName)
197+ }
198+ return joinCookies(cookies)
199+}
200+
201+// joinCookies takes a slice of cookies from the request and reconstructs the
202+// full session cookie
203+func joinCookies(cookies []*http.Cookie) (*http.Cookie, error) {
204+ if len(cookies) == 0 {
205+ return nil, fmt.Errorf("list of cookies must be > 0")
206+ }
207+ if len(cookies) == 1 {
208+ return cookies[0], nil
209+ }
210+ c := copyCookie(cookies[0])
211+ for i := 1; i < len(cookies); i++ {
212+ c.Value += cookies[i].Value
213+ }
214+ c.Name = strings.TrimRight(c.Name, "_0")
215+ return c, nil
216+}
217+
218+func copyCookie(c *http.Cookie) *http.Cookie {
219+ return &http.Cookie{
220+ Name: c.Name,
221+ Value: c.Value,
222+ Path: c.Path,
223+ Domain: c.Domain,
224+ Expires: c.Expires,
225+ RawExpires: c.RawExpires,
226+ MaxAge: c.MaxAge,
227+ Secure: c.Secure,
228+ HttpOnly: c.HttpOnly,
229+ Raw: c.Raw,
230+ Unparsed: c.Unparsed,
231+ }
232+}
pkg/sessions/session_store.gonew file+19
@@ -0,0 +1,19 @@
1+package sessions
2+
3+import (
4+ "fmt"
5+
6+ "github.com/pusher/oauth2_proxy/pkg/apis/options"
7+ "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
8+ "github.com/pusher/oauth2_proxy/pkg/sessions/cookie"
9+)
10+
11+// NewSessionStore creates a SessionStore from the provided configuration
12+func NewSessionStore(opts *options.SessionOptions, cookieOpts *options.CookieOptions) (sessions.SessionStore, error) {
13+ switch opts.Type {
14+ case options.CookieSessionStoreType:
15+ return cookie.NewCookieSessionStore(opts.CookieStoreOptions, cookieOpts)
16+ default:
17+ return nil, fmt.Errorf("unknown session store type '%s'", opts.Type)
18+ }
19+}
pkg/sessions/session_store_test.gonew file+254
@@ -0,0 +1,254 @@
1+package sessions_test
2+
3+import (
4+ "crypto/rand"
5+ "encoding/base64"
6+ "net/http"
7+ "net/http/httptest"
8+ "testing"
9+ "time"
10+
11+ . "github.com/onsi/ginkgo"
12+ . "github.com/onsi/gomega"
13+ "github.com/pusher/oauth2_proxy/pkg/apis/options"
14+ sessionsapi "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
15+ "github.com/pusher/oauth2_proxy/pkg/cookies"
16+ "github.com/pusher/oauth2_proxy/pkg/sessions"
17+ "github.com/pusher/oauth2_proxy/pkg/sessions/cookie"
18+)
19+
20+func TestSessionStore(t *testing.T) {
21+ RegisterFailHandler(Fail)
22+ RunSpecs(t, "SessionStore")
23+}
24+
25+var _ = Describe("NewSessionStore", func() {
26+ var opts *options.SessionOptions
27+ var cookieOpts *options.CookieOptions
28+
29+ var request *http.Request
30+ var response *httptest.ResponseRecorder
31+ var session *sessionsapi.SessionState
32+ var ss sessionsapi.SessionStore
33+
34+ CheckCookieOptions := func() {
35+ Context("the cookies returned", func() {
36+ var cookies []*http.Cookie
37+ BeforeEach(func() {
38+ cookies = response.Result().Cookies()
39+ })
40+
41+ It("have the correct name set", func() {
42+ if len(cookies) == 1 {
43+ Expect(cookies[0].Name).To(Equal(cookieOpts.CookieName))
44+ } else {
45+ for _, cookie := range cookies {
46+ Expect(cookie.Name).To(ContainSubstring(cookieOpts.CookieName))
47+ }
48+ }
49+ })
50+
51+ It("have the correct path set", func() {
52+ for _, cookie := range cookies {
53+ Expect(cookie.Path).To(Equal(cookieOpts.CookiePath))
54+ }
55+ })
56+
57+ It("have the correct domain set", func() {
58+ for _, cookie := range cookies {
59+ Expect(cookie.Domain).To(Equal(cookieOpts.CookieDomain))
60+ }
61+ })
62+
63+ It("have the correct HTTPOnly set", func() {
64+ for _, cookie := range cookies {
65+ Expect(cookie.HttpOnly).To(Equal(cookieOpts.CookieHTTPOnly))
66+ }
67+ })
68+
69+ It("have the correct secure set", func() {
70+ for _, cookie := range cookies {
71+ Expect(cookie.Secure).To(Equal(cookieOpts.CookieSecure))
72+ }
73+ })
74+
75+ })
76+ }
77+
78+ SessionStoreInterfaceTests := func() {
79+ Context("when Save is called", func() {
80+ BeforeEach(func() {
81+ err := ss.Save(response, request, session)
82+ Expect(err).ToNot(HaveOccurred())
83+ })
84+
85+ It("sets a `set-cookie` header in the response", func() {
86+ Expect(response.Header().Get("set-cookie")).ToNot(BeEmpty())
87+ })
88+
89+ CheckCookieOptions()
90+ })
91+
92+ Context("when Clear is called", func() {
93+ BeforeEach(func() {
94+ cookie := cookies.MakeCookie(request,
95+ cookieOpts.CookieName,
96+ "foo",
97+ cookieOpts.CookiePath,
98+ cookieOpts.CookieDomain,
99+ cookieOpts.CookieHTTPOnly,
100+ cookieOpts.CookieSecure,
101+ cookieOpts.CookieExpire,
102+ time.Now(),
103+ )
104+ request.AddCookie(cookie)
105+ err := ss.Clear(response, request)
106+ Expect(err).ToNot(HaveOccurred())
107+ })
108+
109+ It("sets a `set-cookie` header in the response", func() {
110+ Expect(response.Header().Get("Set-Cookie")).ToNot(BeEmpty())
111+ })
112+
113+ CheckCookieOptions()
114+ })
115+
116+ Context("when Load is called", func() {
117+ var loadedSession *sessionsapi.SessionState
118+ BeforeEach(func() {
119+ req := httptest.NewRequest("GET", "http://example.com/", nil)
120+ resp := httptest.NewRecorder()
121+ err := ss.Save(resp, req, session)
122+ Expect(err).ToNot(HaveOccurred())
123+
124+ for _, cookie := range resp.Result().Cookies() {
125+ request.AddCookie(cookie)
126+ }
127+ loadedSession, err = ss.Load(request)
128+ Expect(err).ToNot(HaveOccurred())
129+ })
130+
131+ It("loads a session equal to the original session", func() {
132+ if cookieOpts.CookieSecret == "" {
133+ // Only Email and User stored in session when encrypted
134+ Expect(loadedSession.Email).To(Equal(session.Email))
135+ Expect(loadedSession.User).To(Equal(session.User))
136+ } else {
137+ // All fields stored in session if encrypted
138+
139+ // Can't compare time.Time using Equal() so remove ExpiresOn from sessions
140+ l := *loadedSession
141+ l.ExpiresOn = time.Time{}
142+ s := *session
143+ s.ExpiresOn = time.Time{}
144+ Expect(l).To(Equal(s))
145+
146+ // Compare time.Time separately
147+ Expect(loadedSession.ExpiresOn.Equal(session.ExpiresOn)).To(BeTrue())
148+ }
149+ })
150+ })
151+ }
152+
153+ RunSessionTests := func() {
154+ Context("with default options", func() {
155+ BeforeEach(func() {
156+ var err error
157+ ss, err = sessions.NewSessionStore(opts, cookieOpts)
158+ Expect(err).ToNot(HaveOccurred())
159+ })
160+
161+ SessionStoreInterfaceTests()
162+ })
163+
164+ Context("with non-default options", func() {
165+ BeforeEach(func() {
166+ cookieOpts = &options.CookieOptions{
167+ CookieName: "_cookie_name",
168+ CookiePath: "/path",
169+ CookieExpire: time.Duration(72) * time.Hour,
170+ CookieRefresh: time.Duration(3600),
171+ CookieSecure: false,
172+ CookieHTTPOnly: false,
173+ CookieDomain: "example.com",
174+ }
175+
176+ var err error
177+ ss, err = sessions.NewSessionStore(opts, cookieOpts)
178+ Expect(err).ToNot(HaveOccurred())
179+ })
180+
181+ SessionStoreInterfaceTests()
182+ })
183+
184+ Context("with a cookie-secret set", func() {
185+ BeforeEach(func() {
186+ secret := make([]byte, 32)
187+ _, err := rand.Read(secret)
188+ Expect(err).ToNot(HaveOccurred())
189+ cookieOpts.CookieSecret = base64.URLEncoding.EncodeToString(secret)
190+
191+ ss, err = sessions.NewSessionStore(opts, cookieOpts)
192+ Expect(err).ToNot(HaveOccurred())
193+ })
194+
195+ SessionStoreInterfaceTests()
196+ })
197+ }
198+
199+ BeforeEach(func() {
200+ ss = nil
201+ opts = &options.SessionOptions{}
202+
203+ // Set default options in CookieOptions
204+ cookieOpts = &options.CookieOptions{
205+ CookieName: "_oauth2_proxy",
206+ CookiePath: "/",
207+ CookieExpire: time.Duration(168) * time.Hour,
208+ CookieRefresh: time.Duration(0),
209+ CookieSecure: true,
210+ CookieHTTPOnly: true,
211+ }
212+
213+ session = &sessionsapi.SessionState{
214+ AccessToken: "AccessToken",
215+ IDToken: "IDToken",
216+ ExpiresOn: time.Now().Add(1 * time.Hour),
217+ RefreshToken: "RefreshToken",
218+ Email: "john.doe@example.com",
219+ User: "john.doe",
220+ }
221+
222+ request = httptest.NewRequest("GET", "http://example.com/", nil)
223+ response = httptest.NewRecorder()
224+ })
225+
226+ Context("with type 'cookie'", func() {
227+ BeforeEach(func() {
228+ opts.Type = options.CookieSessionStoreType
229+ })
230+
231+ It("creates a cookie.SessionStore", func() {
232+ ss, err := sessions.NewSessionStore(opts, cookieOpts)
233+ Expect(err).NotTo(HaveOccurred())
234+ Expect(ss).To(BeAssignableToTypeOf(&cookie.SessionStore{}))
235+ })
236+
237+ Context("the cookie.SessionStore", func() {
238+ RunSessionTests()
239+ })
240+ })
241+
242+ Context("with an invalid type", func() {
243+ BeforeEach(func() {
244+ opts.Type = "invalid-type"
245+ })
246+
247+ It("returns an error", func() {
248+ ss, err := sessions.NewSessionStore(opts, cookieOpts)
249+ Expect(err).To(HaveOccurred())
250+ Expect(err.Error()).To(Equal("unknown session store type 'invalid-type'"))
251+ Expect(ss).To(BeNil())
252+ })
253+ })
254+})
pkg/sessions/utils/utils.gonew file+41
@@ -0,0 +1,41 @@
1+package utils
2+
3+import (
4+ "encoding/base64"
5+
6+ "github.com/pusher/oauth2_proxy/cookie"
7+ "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
8+)
9+
10+// CookieForSession serializes a session state for storage in a cookie
11+func CookieForSession(s *sessions.SessionState, c *cookie.Cipher) (string, error) {
12+ return s.EncodeSessionState(c)
13+}
14+
15+// SessionFromCookie deserializes a session from a cookie value
16+func SessionFromCookie(v string, c *cookie.Cipher) (s *sessions.SessionState, err error) {
17+ return sessions.DecodeSessionState(v, c)
18+}
19+
20+// SecretBytes attempts to base64 decode the secret, if that fails it treats the secret as binary
21+func SecretBytes(secret string) []byte {
22+ b, err := base64.URLEncoding.DecodeString(addPadding(secret))
23+ if err == nil {
24+ return []byte(addPadding(string(b)))
25+ }
26+ return []byte(secret)
27+}
28+
29+func addPadding(secret string) string {
30+ padding := len(secret) % 4
31+ switch padding {
32+ case 1:
33+ return secret + "==="
34+ case 2:
35+ return secret + "=="
36+ case 3:
37+ return secret + "="
38+ default:
39+ return secret
40+ }
41+}
providers/azure.go+2−1
@@ -9,6 +9,7 @@ import (
99 "github.com/bitly/go-simplejson"
1010 "github.com/pusher/oauth2_proxy/api"
1111 "github.com/pusher/oauth2_proxy/logger"
12+ "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
1213)
1314
1415// AzureProvider represents an Azure based Identity Provider
@@ -88,7 +89,7 @@ func getEmailFromJSON(json *simplejson.Json) (string, error) {
8889}
8990
9091// GetEmailAddress returns the Account email address
91−func (p *AzureProvider) GetEmailAddress(s *SessionState) (string, error) {
92+func (p *AzureProvider) GetEmailAddress(s *sessions.SessionState) (string, error) {
9293 var email string
9394 var err error
9495
providers/azure_test.go+7−6
@@ -6,6 +6,7 @@ import (
66 "net/url"
77 "testing"
88
9+ "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
910 "github.com/stretchr/testify/assert"
1011)
1112
@@ -128,7 +129,7 @@ func TestAzureProviderGetEmailAddress(t *testing.T) {
128129 bURL, _ := url.Parse(b.URL)
129130 p := testAzureProvider(bURL.Host)
130131
131− session := &SessionState{AccessToken: "imaginary_access_token"}
132+ session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
132133 email, err := p.GetEmailAddress(session)
133134 assert.Equal(t, nil, err)
134135 assert.Equal(t, "user@windows.net", email)
@@ -141,7 +142,7 @@ func TestAzureProviderGetEmailAddressMailNull(t *testing.T) {
141142 bURL, _ := url.Parse(b.URL)
142143 p := testAzureProvider(bURL.Host)
143144
144− session := &SessionState{AccessToken: "imaginary_access_token"}
145+ session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
145146 email, err := p.GetEmailAddress(session)
146147 assert.Equal(t, nil, err)
147148 assert.Equal(t, "user@windows.net", email)
@@ -154,7 +155,7 @@ func TestAzureProviderGetEmailAddressGetUserPrincipalName(t *testing.T) {
154155 bURL, _ := url.Parse(b.URL)
155156 p := testAzureProvider(bURL.Host)
156157
157− session := &SessionState{AccessToken: "imaginary_access_token"}
158+ session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
158159 email, err := p.GetEmailAddress(session)
159160 assert.Equal(t, nil, err)
160161 assert.Equal(t, "user@windows.net", email)
@@ -167,7 +168,7 @@ func TestAzureProviderGetEmailAddressFailToGetEmailAddress(t *testing.T) {
167168 bURL, _ := url.Parse(b.URL)
168169 p := testAzureProvider(bURL.Host)
169170
170− session := &SessionState{AccessToken: "imaginary_access_token"}
171+ session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
171172 email, err := p.GetEmailAddress(session)
172173 assert.Equal(t, "type assertion to string failed", err.Error())
173174 assert.Equal(t, "", email)
@@ -180,7 +181,7 @@ func TestAzureProviderGetEmailAddressEmptyUserPrincipalName(t *testing.T) {
180181 bURL, _ := url.Parse(b.URL)
181182 p := testAzureProvider(bURL.Host)
182183
183− session := &SessionState{AccessToken: "imaginary_access_token"}
184+ session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
184185 email, err := p.GetEmailAddress(session)
185186 assert.Equal(t, nil, err)
186187 assert.Equal(t, "", email)
@@ -193,7 +194,7 @@ func TestAzureProviderGetEmailAddressIncorrectOtherMails(t *testing.T) {
193194 bURL, _ := url.Parse(b.URL)
194195 p := testAzureProvider(bURL.Host)
195196
196− session := &SessionState{AccessToken: "imaginary_access_token"}
197+ session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
197198 email, err := p.GetEmailAddress(session)
198199 assert.Equal(t, "type assertion to string failed", err.Error())
199200 assert.Equal(t, "", email)
providers/facebook.go+3−2
@@ -7,6 +7,7 @@ import (
77 "net/url"
88
99 "github.com/pusher/oauth2_proxy/api"
10+ "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
1011)
1112
1213// FacebookProvider represents an Facebook based Identity Provider
@@ -54,7 +55,7 @@ func getFacebookHeader(accessToken string) http.Header {
5455}
5556
5657// GetEmailAddress returns the Account email address
57−func (p *FacebookProvider) GetEmailAddress(s *SessionState) (string, error) {
58+func (p *FacebookProvider) GetEmailAddress(s *sessions.SessionState) (string, error) {
5859 if s.AccessToken == "" {
5960 return "", errors.New("missing access token")
6061 }
@@ -79,6 +80,6 @@ func (p *FacebookProvider) GetEmailAddress(s *SessionState) (string, error) {
7980}
8081
8182// ValidateSessionState validates the AccessToken
82−func (p *FacebookProvider) ValidateSessionState(s *SessionState) bool {
83+func (p *FacebookProvider) ValidateSessionState(s *sessions.SessionState) bool {
8384 return validateToken(p, s.AccessToken, getFacebookHeader(s.AccessToken))
8485}
providers/github.go+3−2
@@ -11,6 +11,7 @@ import (
1111 "strings"
1212
1313 "github.com/pusher/oauth2_proxy/logger"
14+ "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
1415)
1516
1617// GitHubProvider represents an GitHub based Identity Provider
@@ -200,7 +201,7 @@ func (p *GitHubProvider) hasOrgAndTeam(accessToken string) (bool, error) {
200201}
201202
202203// GetEmailAddress returns the Account email address
203−func (p *GitHubProvider) GetEmailAddress(s *SessionState) (string, error) {
204+func (p *GitHubProvider) GetEmailAddress(s *sessions.SessionState) (string, error) {
204205
205206 var emails []struct {
206207 Email string `json:"email"`
@@ -259,7 +260,7 @@ func (p *GitHubProvider) GetEmailAddress(s *SessionState) (string, error) {
259260}
260261
261262// GetUserName returns the Account user name
262−func (p *GitHubProvider) GetUserName(s *SessionState) (string, error) {
263+func (p *GitHubProvider) GetUserName(s *sessions.SessionState) (string, error) {
263264 var user struct {
264265 Login string `json:"login"`
265266 Email string `json:"email"`
providers/github_test.go+7−6
@@ -6,6 +6,7 @@ import (
66 "net/url"
77 "testing"
88
9+ "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
910 "github.com/stretchr/testify/assert"
1011)
1112
@@ -103,7 +104,7 @@ func TestGitHubProviderGetEmailAddress(t *testing.T) {
103104 bURL, _ := url.Parse(b.URL)
104105 p := testGitHubProvider(bURL.Host)
105106
106− session := &SessionState{AccessToken: "imaginary_access_token"}
107+ session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
107108 email, err := p.GetEmailAddress(session)
108109 assert.Equal(t, nil, err)
109110 assert.Equal(t, "michael.bland@gsa.gov", email)
@@ -116,7 +117,7 @@ func TestGitHubProviderGetEmailAddressNotVerified(t *testing.T) {
116117 bURL, _ := url.Parse(b.URL)
117118 p := testGitHubProvider(bURL.Host)
118119
119− session := &SessionState{AccessToken: "imaginary_access_token"}
120+ session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
120121 email, err := p.GetEmailAddress(session)
121122 assert.Equal(t, nil, err)
122123 assert.Empty(t, "", email)
@@ -134,7 +135,7 @@ func TestGitHubProviderGetEmailAddressWithOrg(t *testing.T) {
134135 p := testGitHubProvider(bURL.Host)
135136 p.Org = "testorg1"
136137
137− session := &SessionState{AccessToken: "imaginary_access_token"}
138+ session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
138139 email, err := p.GetEmailAddress(session)
139140 assert.Equal(t, nil, err)
140141 assert.Equal(t, "michael.bland@gsa.gov", email)
@@ -152,7 +153,7 @@ func TestGitHubProviderGetEmailAddressFailedRequest(t *testing.T) {
152153 // We'll trigger a request failure by using an unexpected access
153154 // token. Alternatively, we could allow the parsing of the payload as
154155 // JSON to fail.
155− session := &SessionState{AccessToken: "unexpected_access_token"}
156+ session := &sessions.SessionState{AccessToken: "unexpected_access_token"}
156157 email, err := p.GetEmailAddress(session)
157158 assert.NotEqual(t, nil, err)
158159 assert.Equal(t, "", email)
@@ -165,7 +166,7 @@ func TestGitHubProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {
165166 bURL, _ := url.Parse(b.URL)
166167 p := testGitHubProvider(bURL.Host)
167168
168− session := &SessionState{AccessToken: "imaginary_access_token"}
169+ session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
169170 email, err := p.GetEmailAddress(session)
170171 assert.NotEqual(t, nil, err)
171172 assert.Equal(t, "", email)
@@ -178,7 +179,7 @@ func TestGitHubProviderGetUserName(t *testing.T) {
178179 bURL, _ := url.Parse(b.URL)
179180 p := testGitHubProvider(bURL.Host)
180181
181− session := &SessionState{AccessToken: "imaginary_access_token"}
182+ session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
182183 email, err := p.GetUserName(session)
183184 assert.Equal(t, nil, err)
184185 assert.Equal(t, "mbland", email)
providers/gitlab.go+2−1
@@ -6,6 +6,7 @@ import (
66
77 "github.com/pusher/oauth2_proxy/api"
88 "github.com/pusher/oauth2_proxy/logger"
9+ "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
910)
1011
1112// GitLabProvider represents an GitLab based Identity Provider
@@ -44,7 +45,7 @@ func NewGitLabProvider(p *ProviderData) *GitLabProvider {
4445}
4546
4647// GetEmailAddress returns the Account email address
47−func (p *GitLabProvider) GetEmailAddress(s *SessionState) (string, error) {
48+func (p *GitLabProvider) GetEmailAddress(s *sessions.SessionState) (string, error) {
4849
4950 req, err := http.NewRequest("GET",
5051 p.ValidateURL.String()+"?access_token="+s.AccessToken, nil)
providers/gitlab_test.go+4−3
@@ -6,6 +6,7 @@ import (
66 "net/url"
77 "testing"
88
9+ "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
910 "github.com/stretchr/testify/assert"
1011)
1112
@@ -89,7 +90,7 @@ func TestGitLabProviderGetEmailAddress(t *testing.T) {
8990 bURL, _ := url.Parse(b.URL)
9091 p := testGitLabProvider(bURL.Host)
9192
92− session := &SessionState{AccessToken: "imaginary_access_token"}
93+ session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
9394 email, err := p.GetEmailAddress(session)
9495 assert.Equal(t, nil, err)
9596 assert.Equal(t, "michael.bland@gsa.gov", email)
@@ -107,7 +108,7 @@ func TestGitLabProviderGetEmailAddressFailedRequest(t *testing.T) {
107108 // We'll trigger a request failure by using an unexpected access
108109 // token. Alternatively, we could allow the parsing of the payload as
109110 // JSON to fail.
110− session := &SessionState{AccessToken: "unexpected_access_token"}
111+ session := &sessions.SessionState{AccessToken: "unexpected_access_token"}
111112 email, err := p.GetEmailAddress(session)
112113 assert.NotEqual(t, nil, err)
113114 assert.Equal(t, "", email)
@@ -120,7 +121,7 @@ func TestGitLabProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {
120121 bURL, _ := url.Parse(b.URL)
121122 p := testGitLabProvider(bURL.Host)
122123
123− session := &SessionState{AccessToken: "imaginary_access_token"}
124+ session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
124125 email, err := p.GetEmailAddress(session)
125126 assert.NotEqual(t, nil, err)
126127 assert.Equal(t, "", email)
providers/google.go+4−3
@@ -14,6 +14,7 @@ import (
1414 "time"
1515
1616 "github.com/pusher/oauth2_proxy/logger"
17+ "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
1718 "golang.org/x/oauth2"
1819 "golang.org/x/oauth2/google"
1920 admin "google.golang.org/api/admin/directory/v1"
@@ -96,7 +97,7 @@ func claimsFromIDToken(idToken string) (*claims, error) {
9697}
9798
9899// Redeem exchanges the OAuth2 authentication token for an ID token
99−func (p *GoogleProvider) Redeem(redirectURL, code string) (s *SessionState, err error) {
100+func (p *GoogleProvider) Redeem(redirectURL, code string) (s *sessions.SessionState, err error) {
100101 if code == "" {
101102 err = errors.New("missing code")
102103 return
@@ -145,7 +146,7 @@ func (p *GoogleProvider) Redeem(redirectURL, code string) (s *SessionState, err
145146 if err != nil {
146147 return
147148 }
148− s = &SessionState{
149+ s = &sessions.SessionState{
149150 AccessToken: jsonResponse.AccessToken,
150151 IDToken: jsonResponse.IDToken,
151152 ExpiresOn: time.Now().Add(time.Duration(jsonResponse.ExpiresIn) * time.Second).Truncate(time.Second),
@@ -258,7 +259,7 @@ func (p *GoogleProvider) ValidateGroup(email string) bool {
258259
259260// RefreshSessionIfNeeded checks if the session has expired and uses the
260261// RefreshToken to fetch a new ID token if required
261−func (p *GoogleProvider) RefreshSessionIfNeeded(s *SessionState) (bool, error) {
262+func (p *GoogleProvider) RefreshSessionIfNeeded(s *sessions.SessionState) (bool, error) {
262263 if s == nil || s.ExpiresOn.After(time.Now()) || s.RefreshToken == "" {
263264 return false, nil
264265 }
providers/internal_util_test.go+3−2
@@ -7,6 +7,7 @@ import (
77 "net/url"
88 "testing"
99
10+ "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
1011 "github.com/stretchr/testify/assert"
1112)
1213
@@ -19,13 +20,13 @@ type ValidateSessionStateTestProvider struct {
1920 *ProviderData
2021}
2122
22−func (tp *ValidateSessionStateTestProvider) GetEmailAddress(s *SessionState) (string, error) {
23+func (tp *ValidateSessionStateTestProvider) GetEmailAddress(s *sessions.SessionState) (string, error) {
2324 return "", errors.New("not implemented")
2425}
2526
2627// Note that we're testing the internal validateToken() used to implement
2728// several Provider's ValidateSessionState() implementations
28−func (tp *ValidateSessionStateTestProvider) ValidateSessionState(s *SessionState) bool {
29+func (tp *ValidateSessionStateTestProvider) ValidateSessionState(s *sessions.SessionState) bool {
2930 return false
3031}
3132
providers/linkedin.go+3−2
@@ -7,6 +7,7 @@ import (
77 "net/url"
88
99 "github.com/pusher/oauth2_proxy/api"
10+ "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
1011)
1112
1213// LinkedInProvider represents an LinkedIn based Identity Provider
@@ -50,7 +51,7 @@ func getLinkedInHeader(accessToken string) http.Header {
5051}
5152
5253// GetEmailAddress returns the Account email address
53−func (p *LinkedInProvider) GetEmailAddress(s *SessionState) (string, error) {
54+func (p *LinkedInProvider) GetEmailAddress(s *sessions.SessionState) (string, error) {
5455 if s.AccessToken == "" {
5556 return "", errors.New("missing access token")
5657 }
@@ -73,6 +74,6 @@ func (p *LinkedInProvider) GetEmailAddress(s *SessionState) (string, error) {
7374}
7475
7576// ValidateSessionState validates the AccessToken
76−func (p *LinkedInProvider) ValidateSessionState(s *SessionState) bool {
77+func (p *LinkedInProvider) ValidateSessionState(s *sessions.SessionState) bool {
7778 return validateToken(p, s.AccessToken, getLinkedInHeader(s.AccessToken))
7879}
providers/linkedin_test.go+4−3
@@ -6,6 +6,7 @@ import (
66 "net/url"
77 "testing"
88
9+ "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
910 "github.com/stretchr/testify/assert"
1011)
1112
@@ -97,7 +98,7 @@ func TestLinkedInProviderGetEmailAddress(t *testing.T) {
9798 bURL, _ := url.Parse(b.URL)
9899 p := testLinkedInProvider(bURL.Host)
99100
100− session := &SessionState{AccessToken: "imaginary_access_token"}
101+ session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
101102 email, err := p.GetEmailAddress(session)
102103 assert.Equal(t, nil, err)
103104 assert.Equal(t, "user@linkedin.com", email)
@@ -113,7 +114,7 @@ func TestLinkedInProviderGetEmailAddressFailedRequest(t *testing.T) {
113114 // We'll trigger a request failure by using an unexpected access
114115 // token. Alternatively, we could allow the parsing of the payload as
115116 // JSON to fail.
116− session := &SessionState{AccessToken: "unexpected_access_token"}
117+ session := &sessions.SessionState{AccessToken: "unexpected_access_token"}
117118 email, err := p.GetEmailAddress(session)
118119 assert.NotEqual(t, nil, err)
119120 assert.Equal(t, "", email)
@@ -126,7 +127,7 @@ func TestLinkedInProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {
126127 bURL, _ := url.Parse(b.URL)
127128 p := testLinkedInProvider(bURL.Host)
128129
129− session := &SessionState{AccessToken: "imaginary_access_token"}
130+ session := &sessions.SessionState{AccessToken: "imaginary_access_token"}
130131 email, err := p.GetEmailAddress(session)
131132 assert.NotEqual(t, nil, err)
132133 assert.Equal(t, "", email)
providers/logingov.go+3−2
@@ -13,6 +13,7 @@ import (
1313 "time"
1414
1515 "github.com/dgrijalva/jwt-go"
16+ "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
1617 "gopkg.in/square/go-jose.v2"
1718)
1819
@@ -173,7 +174,7 @@ func emailFromUserInfo(accessToken string, userInfoEndpoint string) (email strin
173174}
174175
175176// Redeem exchanges the OAuth2 authentication token for an ID token
176−func (p *LoginGovProvider) Redeem(redirectURL, code string) (s *SessionState, err error) {
177+func (p *LoginGovProvider) Redeem(redirectURL, code string) (s *sessions.SessionState, err error) {
177178 if code == "" {
178179 err = errors.New("missing code")
179180 return
@@ -248,7 +249,7 @@ func (p *LoginGovProvider) Redeem(redirectURL, code string) (s *SessionState, er
248249 }
249250
250251 // Store the data that we found in the session state
251− s = &SessionState{
252+ s = &sessions.SessionState{
252253 AccessToken: jsonResponse.AccessToken,
253254 IDToken: jsonResponse.IDToken,
254255 ExpiresOn: time.Now().Add(time.Duration(jsonResponse.ExpiresIn) * time.Second).Truncate(time.Second),
providers/oidc.go+8−8
@@ -5,9 +5,9 @@ import (
55 "fmt"
66 "time"
77
8− "golang.org/x/oauth2"
9−
108 oidc "github.com/coreos/go-oidc"
9+ "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
10+ "golang.org/x/oauth2"
1111)
1212
1313// OIDCProvider represents an OIDC based Identity Provider
@@ -24,7 +24,7 @@ func NewOIDCProvider(p *ProviderData) *OIDCProvider {
2424}
2525
2626// Redeem exchanges the OAuth2 authentication token for an ID token
27−func (p *OIDCProvider) Redeem(redirectURL, code string) (s *SessionState, err error) {
27+func (p *OIDCProvider) Redeem(redirectURL, code string) (s *sessions.SessionState, err error) {
2828 ctx := context.Background()
2929 c := oauth2.Config{
3030 ClientID: p.ClientID,
@@ -47,7 +47,7 @@ func (p *OIDCProvider) Redeem(redirectURL, code string) (s *SessionState, err er
4747
4848// RefreshSessionIfNeeded checks if the session has expired and uses the
4949// RefreshToken to fetch a new ID token if required
50−func (p *OIDCProvider) RefreshSessionIfNeeded(s *SessionState) (bool, error) {
50+func (p *OIDCProvider) RefreshSessionIfNeeded(s *sessions.SessionState) (bool, error) {
5151 if s == nil || s.ExpiresOn.After(time.Now()) || s.RefreshToken == "" {
5252 return false, nil
5353 }
@@ -63,7 +63,7 @@ func (p *OIDCProvider) RefreshSessionIfNeeded(s *SessionState) (bool, error) {
6363 return true, nil
6464}
6565
66−func (p *OIDCProvider) redeemRefreshToken(s *SessionState) (err error) {
66+func (p *OIDCProvider) redeemRefreshToken(s *sessions.SessionState) (err error) {
6767 c := oauth2.Config{
6868 ClientID: p.ClientID,
6969 ClientSecret: p.ClientSecret,
@@ -92,7 +92,7 @@ func (p *OIDCProvider) redeemRefreshToken(s *SessionState) (err error) {
9292 return
9393}
9494
95−func (p *OIDCProvider) createSessionState(ctx context.Context, token *oauth2.Token) (*SessionState, error) {
95+func (p *OIDCProvider) createSessionState(ctx context.Context, token *oauth2.Token) (*sessions.SessionState, error) {
9696 rawIDToken, ok := token.Extra("id_token").(string)
9797 if !ok {
9898 return nil, fmt.Errorf("token response did not contain an id_token")
@@ -122,7 +122,7 @@ func (p *OIDCProvider) createSessionState(ctx context.Context, token *oauth2.Tok
122122 return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
123123 }
124124
125− return &SessionState{
125+ return &sessions.SessionState{
126126 AccessToken: token.AccessToken,
127127 IDToken: rawIDToken,
128128 RefreshToken: token.RefreshToken,
@@ -133,7 +133,7 @@ func (p *OIDCProvider) createSessionState(ctx context.Context, token *oauth2.Tok
133133}
134134
135135// ValidateSessionState checks that the session's IDToken is still valid
136−func (p *OIDCProvider) ValidateSessionState(s *SessionState) bool {
136+func (p *OIDCProvider) ValidateSessionState(s *sessions.SessionState) bool {
137137 ctx := context.Background()
138138 _, err := p.Verifier.Verify(ctx, s.IDToken)
139139 if err != nil {
providers/provider_default.go+11−10
@@ -10,10 +10,11 @@ import (
1010 "net/url"
1111
1212 "github.com/pusher/oauth2_proxy/cookie"
13+ "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
1314)
1415
1516// Redeem provides a default implementation of the OAuth2 token redemption process
16−func (p *ProviderData) Redeem(redirectURL, code string) (s *SessionState, err error) {
17+func (p *ProviderData) Redeem(redirectURL, code string) (s *sessions.SessionState, err error) {
1718 if code == "" {
1819 err = errors.New("missing code")
1920 return
@@ -59,7 +60,7 @@ func (p *ProviderData) Redeem(redirectURL, code string) (s *SessionState, err er
5960 }
6061 err = json.Unmarshal(body, &jsonResponse)
6162 if err == nil {
62− s = &SessionState{
63+ s = &sessions.SessionState{
6364 AccessToken: jsonResponse.AccessToken,
6465 }
6566 return
@@ -71,7 +72,7 @@ func (p *ProviderData) Redeem(redirectURL, code string) (s *SessionState, err er
7172 return
7273 }
7374 if a := v.Get("access_token"); a != "" {
74− s = &SessionState{AccessToken: a}
75+ s = &sessions.SessionState{AccessToken: a}
7576 } else {
7677 err = fmt.Errorf("no access token found %s", body)
7778 }
@@ -94,22 +95,22 @@ func (p *ProviderData) GetLoginURL(redirectURI, state string) string {
9495}
9596
9697// CookieForSession serializes a session state for storage in a cookie
97−func (p *ProviderData) CookieForSession(s *SessionState, c *cookie.Cipher) (string, error) {
98+func (p *ProviderData) CookieForSession(s *sessions.SessionState, c *cookie.Cipher) (string, error) {
9899 return s.EncodeSessionState(c)
99100}
100101
101102// SessionFromCookie deserializes a session from a cookie value
102−func (p *ProviderData) SessionFromCookie(v string, c *cookie.Cipher) (s *SessionState, err error) {
103− return DecodeSessionState(v, c)
103+func (p *ProviderData) SessionFromCookie(v string, c *cookie.Cipher) (s *sessions.SessionState, err error) {
104+ return sessions.DecodeSessionState(v, c)
104105}
105106
106107// GetEmailAddress returns the Account email address
107−func (p *ProviderData) GetEmailAddress(s *SessionState) (string, error) {
108+func (p *ProviderData) GetEmailAddress(s *sessions.SessionState) (string, error) {
108109 return "", errors.New("not implemented")
109110}
110111
111112// GetUserName returns the Account username
112−func (p *ProviderData) GetUserName(s *SessionState) (string, error) {
113+func (p *ProviderData) GetUserName(s *sessions.SessionState) (string, error) {
113114 return "", errors.New("not implemented")
114115}
115116
@@ -120,12 +121,12 @@ func (p *ProviderData) ValidateGroup(email string) bool {
120121}
121122
122123// ValidateSessionState validates the AccessToken
123−func (p *ProviderData) ValidateSessionState(s *SessionState) bool {
124+func (p *ProviderData) ValidateSessionState(s *sessions.SessionState) bool {
124125 return validateToken(p, s.AccessToken, nil)
125126}
126127
127128// RefreshSessionIfNeeded should refresh the user's session if required and
128129// do nothing if a refresh is not required
129−func (p *ProviderData) RefreshSessionIfNeeded(s *SessionState) (bool, error) {
130+func (p *ProviderData) RefreshSessionIfNeeded(s *sessions.SessionState) (bool, error) {
130131 return false, nil
131132}
providers/provider_default_test.go+2−1
@@ -4,12 +4,13 @@ import (
44 "testing"
55 "time"
66
7+ "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
78 "github.com/stretchr/testify/assert"
89)
910
1011func TestRefresh(t *testing.T) {
1112 p := &ProviderData{}
12− refreshed, err := p.RefreshSessionIfNeeded(&SessionState{
13+ refreshed, err := p.RefreshSessionIfNeeded(&sessions.SessionState{
1314 ExpiresOn: time.Now().Add(time.Duration(-11) * time.Minute),
1415 })
1516 assert.Equal(t, false, refreshed)
providers/providers.go+8−7
@@ -2,20 +2,21 @@ package providers
22
33import (
44 "github.com/pusher/oauth2_proxy/cookie"
5+ "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
56)
67
78// Provider represents an upstream identity provider implementation
89type Provider interface {
910 Data() *ProviderData
10− GetEmailAddress(*SessionState) (string, error)
11− GetUserName(*SessionState) (string, error)
12− Redeem(string, string) (*SessionState, error)
11+ GetEmailAddress(*sessions.SessionState) (string, error)
12+ GetUserName(*sessions.SessionState) (string, error)
13+ Redeem(string, string) (*sessions.SessionState, error)
1314 ValidateGroup(string) bool
14− ValidateSessionState(*SessionState) bool
15+ ValidateSessionState(*sessions.SessionState) bool
1516 GetLoginURL(redirectURI, finalRedirect string) string
16− RefreshSessionIfNeeded(*SessionState) (bool, error)
17− SessionFromCookie(string, *cookie.Cipher) (*SessionState, error)
18− CookieForSession(*SessionState, *cookie.Cipher) (string, error)
17+ RefreshSessionIfNeeded(*sessions.SessionState) (bool, error)
18+ SessionFromCookie(string, *cookie.Cipher) (*sessions.SessionState, error)
19+ CookieForSession(*sessions.SessionState, *cookie.Cipher) (string, error)
1920}
2021
2122// New provides a new Provider based on the configured provider string