git.iko.soy/typeable/oauth2-proxy
on master help sign in
Session-Cookie Support (#1713)

* Create session cookie when cookie-expire set 0 * Fix format * add test * fix lint error * fix test code * fix conflicted test case * update test case of cookie expiration * update tests of csrf cookies * update docs * Update docs/docs/configuration/overview.md Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> --------- Co-authored-by: tanuki884 <morkazuk@fsi.co.jp> Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk>

commitd107d885e465d3c99c4c0c47f61da39b1e82c898 authort-katsumura <83070078+t-katsumura@users.noreply.github.com> dateWed 16 Aug 2023, 20:23 (3 years ago) parentd9416c3
11 files changed+137−7
CHANGELOG.md+1 docs/docs/configuration/overview.md+1−1 go.mod+3−3 go.sum+9 pkg/cookies/cookies.go+4−1 pkg/cookies/cookies_test.go+73 pkg/cookies/csrf_test.go+1 pkg/encryption/utils.go+1−1 pkg/encryption/utils_test.go+27 pkg/validation/cookie.go+2−1 pkg/validation/cookie_test.go+15
CHANGELOG.md+1
@@ -14,6 +14,7 @@
1414- [#1906](https://github.com/oauth2-proxy/oauth2-proxy/pull/1906) Fix PKCE code verifier generation to never use UTF-8 characters
1515- [#1839](https://github.com/oauth2-proxy/oauth2-proxy/pull/1839) Add readiness checks for deeper health checks (@kobim)
1616- [#1927](https://github.com/oauth2-proxy/oauth2-proxy/pull/1927) Fix default scope settings for none oidc providers
17+- [#1713](https://github.com/oauth2-proxy/oauth2-proxy/pull/1713) Add session cookie support (@t-katsumura @tanuki884)
1718- [#1951](https://github.com/oauth2-proxy/oauth2-proxy/pull/1951) Fix validate URL, check if query string marker (?) or separator (&) needs to be appended (@miguelborges99)
1819- [#1920](https://github.com/oauth2-proxy/oauth2-proxy/pull/1920) Make sure emailClaim is not overriden if userIDClaim is not set
1920- [#2010](https://github.com/oauth2-proxy/oauth2-proxy/pull/2010) Log the difference between invalid email and not authorized session
docs/docs/configuration/overview.md+1−1
@@ -88,7 +88,7 @@ An example [oauth2-proxy.cfg](https://github.com/oauth2-proxy/oauth2-proxy/blob/
8888| `--code-challenge-method` | string | use PKCE code challenges with the specified method. Either 'plain' or 'S256' (recommended) | |
8989| `--config` | string | path to config file | |
9090| `--cookie-domain` | string \| list | Optional cookie domains to force cookies to (e.g. `.yourcompany.com`). The longest domain matching the request's host will be used (or the shortest cookie domain if there is no match). | |
91−| `--cookie-expire` | duration | expire timeframe for cookie | 168h0m0s |
91+| `--cookie-expire` | duration | expire timeframe for cookie. If set to 0, cookie becomes a session-cookie which will expire when the browser is closed. | 168h0m0s |
9292| `--cookie-httponly` | bool | set HttpOnly cookie flag | true |
9393| `--cookie-name` | string | the name of the cookie that the oauth_proxy creates. Should be changed to use a [cookie prefix](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#cookie_prefixes) (`__Host-` or `__Secure-`) if `--cookie-secure` is set. | `"_oauth2_proxy"` |
9494| `--cookie-path` | string | an optional cookie path to force cookies to (e.g. `/poc/`) | `"/"` |
go.mod+3−3
@@ -20,7 +20,7 @@ require (
2020 github.com/oauth2-proxy/mockoidc v0.0.0-20220221072942-e3afe97dec43
2121 github.com/oauth2-proxy/tools/reference-gen v0.0.0-20210118095127-56ffd7384404
2222 github.com/onsi/ginkgo v1.16.5
23− github.com/onsi/gomega v1.27.2
23+ github.com/onsi/gomega v1.27.6
2424 github.com/pierrec/lz4/v4 v4.1.17
2525 github.com/prometheus/client_golang v1.14.0
2626 github.com/redis/go-redis/v9 v9.0.2
@@ -30,7 +30,7 @@ require (
3030 github.com/stretchr/testify v1.8.1
3131 github.com/vmihailenco/msgpack/v5 v5.3.5
3232 golang.org/x/crypto v0.7.0
33− golang.org/x/exp v0.0.0-20230307190834-24139beb5833
33+ golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53
3434 golang.org/x/net v0.8.0
3535 golang.org/x/oauth2 v0.6.0
3636 golang.org/x/sync v0.1.0
@@ -52,7 +52,7 @@ require (
5252 github.com/go-jose/go-jose/v3 v3.0.0 // indirect
5353 github.com/go-logr/logr v1.2.3 // indirect
5454 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
55− github.com/golang/protobuf v1.5.2 // indirect
55+ github.com/golang/protobuf v1.5.3 // indirect
5656 github.com/google/go-cmp v0.5.9 // indirect
5757 github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
5858 github.com/googleapis/gax-go/v2 v2.7.0 // indirect
go.sum+9
@@ -127,6 +127,7 @@ github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbV
127127github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
128128github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
129129github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
130+github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
130131github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
131132github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
132133github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
@@ -162,6 +163,8 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
162163github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
163164github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
164165github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
166+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
167+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
165168github.com/gomodule/redigo v1.7.1-0.20190322064113-39e2c31b7ca3 h1:6amM4HsNPOvMLVc2ZnyqrjeQ92YAVWn7T4WBKK87inY=
166169github.com/gomodule/redigo v1.7.1-0.20190322064113-39e2c31b7ca3/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
167170github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@@ -272,10 +275,13 @@ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108
272275github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
273276github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
274277github.com/onsi/ginkgo/v2 v2.8.4 h1:gf5mIQ8cLFieruNLAdgijHF1PYfLphKm2dxxcUtcqK0=
278+github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU=
275279github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
276280github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
277281github.com/onsi/gomega v1.27.2 h1:SKU0CXeKE/WVgIV1T61kSa3+IRE8Ekrv9rdXDwwTqnY=
278282github.com/onsi/gomega v1.27.2/go.mod h1:5mR3phAHpkAVIDkHEUBY6HGVsU+cpcEscrGPB4oPlZI=
283+github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
284+github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
279285github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
280286github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
281287github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us=
@@ -403,6 +409,8 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
403409golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
404410golang.org/x/exp v0.0.0-20230307190834-24139beb5833 h1:SChBja7BCQewoTAU7IgvucQKMIXrEpFxNMs0spT3/5s=
405411golang.org/x/exp v0.0.0-20230307190834-24139beb5833/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
412+golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o=
413+golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
406414golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
407415golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
408416golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -616,6 +624,7 @@ golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4f
616624golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
617625golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
618626golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
627+golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
619628golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
620629golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
621630golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
pkg/cookies/cookies.go+4−1
@@ -30,12 +30,15 @@ func MakeCookieFromOptions(req *http.Request, name string, value string, opts *o
3030 Value: value,
3131 Path: opts.Path,
3232 Domain: domain,
33− Expires: now.Add(expiration),
3433 HttpOnly: opts.HTTPOnly,
3534 Secure: opts.Secure,
3635 SameSite: ParseSameSite(opts.SameSite),
3736 }
3837
38+ if expiration != time.Duration(0) {
39+ c.Expires = now.Add(expiration)
40+ }
41+
3942 warnInvalidDomain(c, req)
4043
4144 return c
pkg/cookies/cookies_test.go+73
@@ -3,6 +3,9 @@ package cookies
33import (
44 "fmt"
55 "net/http"
6+ "time"
7+
8+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
69
710 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
811 . "github.com/onsi/ginkgo"
@@ -76,4 +79,74 @@ var _ = Describe("Cookie Tests", func() {
7679 }),
7780 )
7881 })
82+
83+ Context("MakeCookieFromOptions", func() {
84+ type makeCookieFromOptionsTableInput struct {
85+ host string
86+ name string
87+ value string
88+ opts options.Cookie
89+ expiration time.Duration
90+ now time.Time
91+ expectedOutput time.Time
92+ }
93+
94+ validName := "_oauth2_proxy"
95+ validSecret := "secretthirtytwobytes+abcdefghijk"
96+ domains := []string{"www.cookies.test"}
97+
98+ now := time.Now()
99+ var expectedExpires time.Time
100+
101+ DescribeTable("should return cookies with or without expiration",
102+ func(in makeCookieFromOptionsTableInput) {
103+ req, err := http.NewRequest(
104+ http.MethodGet,
105+ fmt.Sprintf("https://%s/%s", in.host, cookiePath),
106+ nil,
107+ )
108+ Expect(err).ToNot(HaveOccurred())
109+
110+ Expect(MakeCookieFromOptions(req, in.name, in.value, &in.opts, in.expiration, in.now).Expires).To(Equal(in.expectedOutput))
111+ },
112+ Entry("persistent cookie", makeCookieFromOptionsTableInput{
113+ host: "www.cookies.test",
114+ name: validName,
115+ value: "1",
116+ opts: options.Cookie{
117+ Name: validName,
118+ Secret: validSecret,
119+ Domains: domains,
120+ Path: "",
121+ Expire: time.Hour,
122+ Refresh: 15 * time.Minute,
123+ Secure: true,
124+ HTTPOnly: false,
125+ SameSite: "",
126+ },
127+ expiration: 15 * time.Minute,
128+ now: now,
129+ expectedOutput: now.Add(15 * time.Minute),
130+ }),
131+ Entry("session cookie", makeCookieFromOptionsTableInput{
132+ host: "www.cookies.test",
133+ name: validName,
134+ value: "1",
135+ opts: options.Cookie{
136+ Name: validName,
137+ Secret: validSecret,
138+ Domains: domains,
139+ Path: "",
140+ Expire: 0,
141+ Refresh: 15 * time.Minute,
142+ Secure: true,
143+ HTTPOnly: false,
144+ SameSite: "",
145+ },
146+ expiration: 0,
147+ now: now,
148+ expectedOutput: expectedExpires,
149+ }),
150+ )
151+ })
79152})
pkg/cookies/csrf_test.go+1
@@ -31,6 +31,7 @@ var _ = Describe("CSRF Cookie Tests", func() {
3131 Secure: true,
3232 HTTPOnly: true,
3333 CSRFPerRequest: false,
34+ CSRFExpire: time.Hour,
3435 }
3536
3637 var err error
pkg/encryption/utils.go+1−1
@@ -58,7 +58,7 @@ func Validate(cookie *http.Cookie, seed string, expiration time.Duration) (value
5858 // creation timestamp stored in the cookie falls within the
5959 // window defined by (Now()-expiration, Now()].
6060 t = time.Unix(int64(ts), 0)
61− if t.After(time.Now().Add(expiration*-1)) && t.Before(time.Now().Add(time.Minute*5)) {
61+ if (expiration == time.Duration(0)) || (t.After(time.Now().Add(expiration*-1)) && t.Before(time.Now().Add(time.Minute*5))) {
6262 // it's a valid cookie. now get the contents
6363 rawValue, err := base64.URLEncoding.DecodeString(parts[0])
6464 if err == nil {
pkg/encryption/utils_test.go+27
@@ -7,8 +7,11 @@ import (
77 "encoding/base64"
88 "fmt"
99 "io"
10+ "net/http"
11+ "strconv"
1012 "strings"
1113 "testing"
14+ "time"
1215 "unicode"
1316
1417 "github.com/stretchr/testify/assert"
@@ -103,6 +106,30 @@ func TestSignAndValidate(t *testing.T) {
103106 assert.False(t, checkSignature(sha1sig, seed, key, "tampered", epoch))
104107}
105108
109+func TestValidate(t *testing.T) {
110+ seed := "0123456789abcdef"
111+ key := "cookie-name"
112+ value := base64.URLEncoding.EncodeToString([]byte("I am soooo encoded"))
113+ epoch := int64(123456789)
114+ epochStr := strconv.FormatInt(epoch, 10)
115+
116+ sha256sig, err := cookieSignature(sha256.New, seed, key, value, epochStr)
117+ assert.NoError(t, err)
118+
119+ cookie := &http.Cookie{
120+ Name: key,
121+ Value: value + "|" + epochStr + "|" + sha256sig,
122+ }
123+
124+ validValue, timestamp, ok := Validate(cookie, seed, 0)
125+ assert.True(t, ok)
126+ assert.Equal(t, timestamp, time.Unix(epoch, 0))
127+
128+ expectedValue, err := base64.URLEncoding.DecodeString(value)
129+ assert.NoError(t, err)
130+ assert.Equal(t, validValue, expectedValue)
131+}
132+
106133func TestGenerateRandomASCIIString(t *testing.T) {
107134 randomString, err := GenerateRandomASCIIString(96)
108135 assert.NoError(t, err)
pkg/validation/cookie.go+2−1
@@ -4,6 +4,7 @@ import (
44 "fmt"
55 "net/http"
66 "sort"
7+ "time"
78
89 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
910 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption"
@@ -12,7 +13,7 @@ import (
1213func validateCookie(o options.Cookie) []string {
1314 msgs := validateCookieSecret(o.Secret)
1415
15− if o.Refresh >= o.Expire {
16+ if o.Expire != time.Duration(0) && o.Refresh >= o.Expire {
1617 msgs = append(msgs, fmt.Sprintf(
1718 "cookie_refresh (%q) must be less than cookie_expire (%q)",
1819 o.Refresh.String(),
pkg/validation/cookie_test.go+15
@@ -256,6 +256,21 @@ func TestValidateCookie(t *testing.T) {
256256 invalidSameSiteMsg,
257257 },
258258 },
259+ {
260+ name: "with session cookie configuration",
261+ cookie: options.Cookie{
262+ Name: validName,
263+ Secret: validSecret,
264+ Domains: domains,
265+ Path: "",
266+ Expire: 0,
267+ Refresh: 15 * time.Minute,
268+ Secure: true,
269+ HTTPOnly: false,
270+ SameSite: "",
271+ },
272+ errStrings: []string{},
273+ },
259274 }
260275
261276 for _, tc := range testCases {