git.iko.soy/typeable/oauth2-proxy
on master help sign in
feat: add same site option for csrf cookies (#3347)

* the attribute version is obsolete, it will be ignored, please remove it to avoid potential confusion Signed-off-by: Joost <439100+jvnoije@users.noreply.github.com> * Add cookie-csrf-samesite option Most of the code is copied form pull request #1947 Signed-off-by: Joost <439100+jvnoije@users.noreply.github.com> * Update CHANGELOG.md Signed-off-by: Joost <439100+jvnoije@users.noreply.github.com> * Removed release information (review comment) Signed-off-by: Joost <439100+jvnoije@users.noreply.github.com> * All cookie variables in a struct Signed-off-by: Joost <439100+jvnoije@users.noreply.github.com> * doc: add changelog entry for #3347 Signed-off-by: Jan Larwig <jan@larwig.com> * revert: unnecessary removal of docker compose version Signed-off-by: Jan Larwig <jan@larwig.com> * doc: sort csrf flags Signed-off-by: Jan Larwig <jan@larwig.com> --------- Signed-off-by: Joost <439100+jvnoije@users.noreply.github.com> Signed-off-by: Jan Larwig <jan@larwig.com> Co-authored-by: Jan Larwig <jan@larwig.com>

commitcdbdb1128dc09cae34670b7958cff56378137817 authorJoost <439100+jvnoije@users.noreply.github.com> dateWed 18 Mar 2026, 16:14 (6 months ago) parent51ecc50
12 files changed+424−111
CHANGELOG.md+1 docs/docs/configuration/overview.md+5−4 pkg/apis/options/cookie.go+3 pkg/cookies/cookies.go+18−8 pkg/cookies/cookies_suite_test.go+4 pkg/cookies/cookies_test.go+32−50 pkg/cookies/csrf.go+33−14 pkg/cookies/csrf_per_request_test.go+12−7 pkg/cookies/csrf_test.go+259 pkg/sessions/cookie/session_store.go+23−13 pkg/sessions/persistence/ticket.go+23−14 pkg/sessions/tests/session_store_tests.go+11−1
CHANGELOG.md+1
@@ -16,6 +16,7 @@
1616- [#3369](https://github.com/oauth2-proxy/oauth2-proxy/pull/3369) fix: use CSRFExpire instead of Expire for CSRF cookie validation (@Br1an67)
1717- [#3365](https://github.com/oauth2-proxy/oauth2-proxy/pull/3365) fix: filter empty strings from allowed groups (@Br1an67)
1818- [#3338](https://github.com/oauth2-proxy/oauth2-proxy/pull/3338) feat: add --config-test flag for validating configuration (@MayorFaj)
19+- [#3347](https://github.com/oauth2-proxy/oauth2-proxy/pull/3347) feat: add same site option for csrf cookies (@jvnoije)
1920
2021# V7.14.3
2122
docs/docs/configuration/overview.md+5−4
@@ -120,7 +120,7 @@ Provider specific options can be found on their respective subpages.
120120| flag: `--approval-prompt`<br/>toml: `approval_prompt` | string | OAuth approval_prompt | `"force"` |
121121| flag: `--backend-logout-url`<br/>toml: `backend_logout_url` | string | URL to perform backend logout, if you use `{id_token}` in the url it will be replaced by the actual `id_token` of the user session | |
122122| flag: `--client-id`<br/>toml: `client_id` | string | the OAuth Client ID, e.g. `"123456.apps.googleusercontent.com"` | |
123−| flag: `--client-secret-file`<br/>toml: `client_secret_file` | string | the file with OAuth Client Secret. The file must contain the secret only, with no trailing newline | |
123+| flag: `--client-secret-file`<br/>toml: `client_secret_file` | string | the file with OAuth Client Secret. The file must contain the secret only, with no trailing newline | |
124124| flag: `--client-secret`<br/>toml: `client_secret` | string | the OAuth Client Secret | |
125125| flag: `--code-challenge-method`<br/>toml: `code_challenge_method` | string | use PKCE code challenges with the specified method. Either 'plain' or 'S256' (recommended) | |
126126| flag: `--insecure-oidc-allow-unverified-email`<br/>toml: `insecure_oidc_allow_unverified_email` | bool | don't fail if an email address in an id_token is not verified | false |
@@ -140,7 +140,7 @@ Provider specific options can be found on their respective subpages.
140140| flag: `--oidc-enabled-signing-alg`<br/>toml: `oidc_enabled_signing_algs` | string \| list | List of allowed JWT signing algorithms. When oidc discovery is enabled, the effective set is the intersection between this list and the provider's discovered supported algorithms. | |
141141| flag: `--profile-url`<br/>toml: `profile_url` | string | Profile access endpoint | |
142142| flag: `--prompt`<br/>toml: `prompt` | string | [OIDC prompt](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest); if present, `approval-prompt` is ignored | `""` |
143−| flag: `--provider-ca-file`<br/>toml: `provider_ca_files` | string \| list | Paths to CA certificates that should be used when connecting to the provider. If not specified, the default Go trust sources are used instead. |
143+| flag: `--provider-ca-file`<br/>toml: `provider_ca_files` | string \| list | Paths to CA certificates that should be used when connecting to the provider. If not specified, the default Go trust sources are used instead. | |
144144| flag: `--provider-display-name`<br/>toml: `provider_display_name` | string | Override the provider's name with the given string; used for the sign-in page | (depends on provider) |
145145| flag: `--provider`<br/>toml: `provider` | string | OAuth provider | google |
146146| flag: `--pubjwk-url`<br/>toml: `pubjwk_url` | string | JWK pubkey access endpoint: required by login.gov | |
@@ -158,6 +158,7 @@ Provider specific options can be found on their respective subpages.
158158| flag: `--cookie-csrf-expire`<br/>toml: `cookie_csrf_expire` | duration | expire timeframe for CSRF cookie | 15m |
159159| flag: `--cookie-csrf-per-request`<br/>toml:`cookie_csrf_per_request` | bool | Enable having different CSRF cookies per request, making it possible to have parallel requests. | false |
160160| flag: `--cookie-csrf-per-request-limit`<br/>toml: `cookie_csrf_per_request_limit` | int | Sets a limit on the number of CSRF requests cookies that oauth2-proxy will create. The oldest cookie will be removed. Useful if users end up with 431 Request headers too large status codes. Only effective if --cookie-csrf-per-request is true | "infinite" |
161+| flag: `--cookie-csrf-samesite`<br/>toml: `cookie_csrf_samesite` | string | set SameSite CSRF cookie attribute (`"lax"`, `"strict"`, `"none"`, or `""`). When using the default setting, the CSRF cookie samesite value is taken from the session cookie configuration. | `""` |
161162| flag: `--cookie-domain`<br/>toml: `cookie_domains` | 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). | |
162163| flag: `--cookie-expire`<br/>toml: `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 |
163164| flag: `--cookie-httponly`<br/>toml: `cookie_httponly` | bool | set HttpOnly cookie flag | true |
@@ -166,7 +167,7 @@ Provider specific options can be found on their respective subpages.
166167| flag: `--cookie-refresh`<br/>toml: `cookie_refresh` | duration | refresh the cookie after this duration; `0` to disable; not supported by all providers&nbsp;[^1] | |
167168| flag: `--cookie-samesite`<br/>toml: `cookie_samesite` | string | set SameSite cookie attribute (`"lax"`, `"strict"`, `"none"`, or `""`). | `""` |
168169| flag: `--cookie-secret`<br/>toml: `cookie_secret` | string | the seed string for secure cookies (optionally base64 encoded) | |
169−| flag: `--cookie-secret-file`<br/>toml: `cookie_secret_file` | string | File containing the cookie secret (must be raw binary, exactly 16, 24, or 32 bytes). Use dd if=/dev/urandom bs=32 count=1 > cookie.secret to generate | |
170+| flag: `--cookie-secret-file`<br/>toml: `cookie_secret_file` | string | File containing the cookie secret (must be raw binary, exactly 16, 24, or 32 bytes). Use dd if=/dev/urandom bs=32 count=1 > cookie.secret to generate | |
170171| flag: `--cookie-secure`<br/>toml: `cookie_secure` | bool | set [secure (HTTPS only) cookie flag](https://owasp.org/www-community/controls/SecureFlag) | true |
171172
172173[^1]: The following providers support `--cookie-refresh`: ADFS, Azure, GitLab, Google, Keycloak and all other Identity Providers which support the full [OIDC specification](https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens)
@@ -212,7 +213,7 @@ Provider specific options can be found on their respective subpages.
212213| Flag / Config Field | Type | Description | Default |
213214| ----------------------------------------------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------- | ------- |
214215| flag: `--banner`<br/>toml: `banner` | string | custom (html) banner string. Use `"-"` to disable default banner. | |
215−| flag: `--custom-sign-in-logo`<br/>toml: `custom_sign_in_logo` | string | path or a URL to an custom image for the sign_in page logo. Use `"-"` to disable default logo. |
216+| flag: `--custom-sign-in-logo`<br/>toml: `custom_sign_in_logo` | string | path or a URL to an custom image for the sign_in page logo. Use `"-"` to disable default logo. | |
216217| flag: `--custom-templates-dir`<br/>toml: `custom_templates_dir` | string | path to custom html templates | |
217218| flag: `--display-htpasswd-form`<br/>toml: `display_htpasswd_form` | bool | display username / password login form if an htpasswd file is provided | true |
218219| flag: `--footer`<br/>toml: `footer` | string | custom (html) footer string. Use `"-"` to disable default footer. (Can be used to obfuscate the version) | |
pkg/apis/options/cookie.go+3
@@ -24,6 +24,7 @@ type Cookie struct {
2424 CSRFPerRequest bool `flag:"cookie-csrf-per-request" cfg:"cookie_csrf_per_request"`
2525 CSRFPerRequestLimit int `flag:"cookie-csrf-per-request-limit" cfg:"cookie_csrf_per_request_limit"`
2626 CSRFExpire time.Duration `flag:"cookie-csrf-expire" cfg:"cookie_csrf_expire"`
27+ CSRFSameSite string `flag:"cookie-csrf-samesite" cfg:"cookie_csrf_samesite"`
2728}
2829
2930func cookieFlagSet() *pflag.FlagSet {
@@ -42,6 +43,7 @@ func cookieFlagSet() *pflag.FlagSet {
4243 flagSet.Bool("cookie-csrf-per-request", false, "When this property is set to true, then the CSRF cookie name is built based on the state and varies per request. If property is set to false, then CSRF cookie has the same name for all requests.")
4344 flagSet.Int("cookie-csrf-per-request-limit", 0, "Sets a limit on the number of CSRF requests cookies that oauth2-proxy will create. The oldest cookies will be removed. Useful if users end up with 431 Request headers too large status codes.")
4445 flagSet.Duration("cookie-csrf-expire", time.Duration(15)*time.Minute, "expire timeframe for CSRF cookie")
46+ flagSet.String("cookie-csrf-samesite", "", "set SameSite CSRF cookie attribute (ie: \"lax\", \"strict\", \"none\", or \"\"). When using the default setting, the CSRF cookie samesite value is taken from the session cookie configuration.")
4547 return flagSet
4648}
4749
@@ -61,6 +63,7 @@ func cookieDefaults() Cookie {
6163 CSRFPerRequest: false,
6264 CSRFPerRequestLimit: 0,
6365 CSRFExpire: time.Duration(15) * time.Minute,
66+ CSRFSameSite: "",
6467 }
6568}
6669
pkg/cookies/cookies.go+18−8
@@ -7,14 +7,24 @@ import (
77 "strings"
88 "time"
99
10− "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
1110 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
1211 requestutil "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests/util"
1312)
1413
14+type CookieOptions struct {
15+ Name string
16+ Value string
17+ Domains []string
18+ Expiration time.Duration
19+ SameSite string
20+ Path string
21+ HTTPOnly bool
22+ Secure bool
23+}
24+
1525// MakeCookieFromOptions constructs a cookie based on the given *options.CookieOptions,
1626// value and creation time
17−func MakeCookieFromOptions(req *http.Request, name string, value string, opts *options.Cookie, expiration time.Duration) *http.Cookie {
27+func MakeCookieFromOptions(req *http.Request, opts *CookieOptions) *http.Cookie {
1828 domain := GetCookieDomain(req, opts.Domains)
1929 // If nothing matches, create the cookie with the shortest domain
2030 if domain == "" && len(opts.Domains) > 0 {
@@ -26,8 +36,8 @@ func MakeCookieFromOptions(req *http.Request, name string, value string, opts *o
2636 }
2737
2838 c := &http.Cookie{
29− Name: name,
30− Value: value,
39+ Name: opts.Name,
40+ Value: opts.Value,
3141 Path: opts.Path,
3242 Domain: domain,
3343 HttpOnly: opts.HTTPOnly,
@@ -35,9 +45,9 @@ func MakeCookieFromOptions(req *http.Request, name string, value string, opts *o
3545 SameSite: ParseSameSite(opts.SameSite),
3646 }
3747
38− if expiration > time.Duration(0) {
39− c.MaxAge = int(expiration.Seconds())
40− } else if expiration < time.Duration(0) {
48+ if opts.Expiration > time.Duration(0) {
49+ c.MaxAge = int(opts.Expiration.Seconds())
50+ } else if opts.Expiration < time.Duration(0) {
4151 c.MaxAge = -1
4252 }
4353
@@ -58,7 +68,7 @@ func GetCookieDomain(req *http.Request, cookieDomains []string) string {
5868 return ""
5969}
6070
61−// Parse a valid http.SameSite value from a user supplied string for use of making cookies.
71+// ParseSameSite a valid http.SameSite value from a user supplied string for use of making cookies.
6272func ParseSameSite(v string) http.SameSite {
6373 switch v {
6474 case "lax":
pkg/cookies/cookies_suite_test.go+4
@@ -17,6 +17,10 @@ const (
1717 cookieDomain = "o2p.cookies.test"
1818 cookiePath = "/cookie-tests"
1919
20+ sameSiteLax = "lax"
21+ sameSiteStrict = "strict"
22+ sameSiteNone = "none"
23+
2024 nowEpoch = 1609366421
2125)
2226
pkg/cookies/cookies_test.go+32−50
@@ -5,8 +5,6 @@ import (
55 "net/http"
66 "time"
77
8− "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
9−
108 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
119 . "github.com/onsi/ginkgo/v2"
1210 . "github.com/onsi/gomega"
@@ -82,16 +80,12 @@ var _ = Describe("Cookie Tests", func() {
8280 Context("MakeCookieFromOptions", func() {
8381 type makeCookieFromOptionsTableInput struct {
8482 host string
85− name string
86− value string
87− opts options.Cookie
88− expiration time.Duration
83+ opts CookieOptions
8984 now time.Time
9085 expectedOutput int
9186 }
9287
9388 validName := "_oauth2_proxy"
94− validSecret := "secretthirtytwobytes+abcdefghijk"
9589 domains := []string{"www.cookies.test"}
9690
9791 now := time.Now()
@@ -106,62 +100,50 @@ var _ = Describe("Cookie Tests", func() {
106100 )
107101 Expect(err).ToNot(HaveOccurred())
108102
109− Expect(MakeCookieFromOptions(req, in.name, in.value, &in.opts, in.expiration).MaxAge).To(Equal(in.expectedOutput))
103+ Expect(MakeCookieFromOptions(req, &in.opts).MaxAge).To(Equal(in.expectedOutput))
110104 },
111105 Entry("persistent cookie", makeCookieFromOptionsTableInput{
112− host: "www.cookies.test",
113− name: validName,
114− value: "1",
115− opts: options.Cookie{
116− Name: validName,
117− Secret: validSecret,
118− Domains: domains,
119− Path: "",
120− Expire: time.Hour,
121− Refresh: 15 * time.Minute,
122− Secure: true,
123− HTTPOnly: false,
124− SameSite: "",
106+ host: "www.cookies.test",
107+ opts: CookieOptions{
108+ Name: validName,
109+ Value: "1",
110+ Domains: domains,
111+ Expiration: 15 * time.Minute,
112+ SameSite: "",
113+ Path: "",
114+ HTTPOnly: false,
115+ Secure: true,
125116 },
126− expiration: 15 * time.Minute,
127117 now: now,
128118 expectedOutput: int((15 * time.Minute).Seconds()),
129119 }),
130120 Entry("persistent cookie to be cleared", makeCookieFromOptionsTableInput{
131− host: "www.cookies.test",
132− name: validName,
133− value: "1",
134− opts: options.Cookie{
135− Name: validName,
136− Secret: validSecret,
137− Domains: domains,
138− Path: "",
139− Expire: time.Hour * -1,
140− Refresh: 15 * time.Minute,
141− Secure: true,
142− HTTPOnly: false,
143− SameSite: "",
121+ host: "www.cookies.test",
122+ opts: CookieOptions{
123+ Name: validName,
124+ Value: "1",
125+ Domains: domains,
126+ Expiration: time.Hour * -1,
127+ SameSite: "",
128+ Path: "",
129+ HTTPOnly: false,
130+ Secure: true,
144131 },
145− expiration: time.Hour * -1,
146132 now: now,
147133 expectedOutput: -1,
148134 }),
149135 Entry("session cookie", makeCookieFromOptionsTableInput{
150− host: "www.cookies.test",
151− name: validName,
152− value: "1",
153− opts: options.Cookie{
154− Name: validName,
155− Secret: validSecret,
156− Domains: domains,
157− Path: "",
158− Expire: 0,
159− Refresh: 15 * time.Minute,
160− Secure: true,
161− HTTPOnly: false,
162− SameSite: "",
136+ host: "www.cookies.test",
137+ opts: CookieOptions{
138+ Name: validName,
139+ Value: "1",
140+ Domains: domains,
141+ Expiration: 0,
142+ SameSite: "",
143+ Path: "",
144+ HTTPOnly: false,
145+ Secure: true,
163146 },
164− expiration: 0,
165147 now: now,
166148 expectedOutput: expectedMaxAge,
167149 }),
pkg/cookies/csrf.go+33−14
@@ -134,6 +134,15 @@ func (c *csrf) SetSessionNonce(s *sessions.SessionState) {
134134 s.Nonce = c.OIDCNonce
135135}
136136
137+// getCSRFSameSite get the CSRF same site
138+func getCSRFSameSite(opts *options.Cookie) string {
139+ sameSite := opts.CSRFSameSite
140+ if sameSite == "" {
141+ sameSite = opts.SameSite
142+ }
143+ return sameSite
144+}
145+
137146// SetCookie encodes the CSRF to a signed cookie and sets it on the ResponseWriter
138147func (c *csrf) SetCookie(rw http.ResponseWriter, req *http.Request) (*http.Cookie, error) {
139148 encoded, err := c.encodeCookie()
@@ -141,13 +150,18 @@ func (c *csrf) SetCookie(rw http.ResponseWriter, req *http.Request) (*http.Cooki
141150 return nil, err
142151 }
143152
144− cookie := MakeCookieFromOptions(
145− req,
146− c.cookieName(),
147− encoded,
148− c.cookieOpts,
149− c.cookieOpts.CSRFExpire,
150− )
153+ csrfCookieOptions := &CookieOptions{
154+ Name: c.cookieName(),
155+ Value: encoded,
156+ Domains: c.cookieOpts.Domains,
157+ Expiration: c.cookieOpts.CSRFExpire,
158+ SameSite: getCSRFSameSite(c.cookieOpts),
159+ Path: c.cookieOpts.Path,
160+ HTTPOnly: c.cookieOpts.HTTPOnly,
161+ Secure: c.cookieOpts.Secure,
162+ }
163+
164+ cookie := MakeCookieFromOptions(req, csrfCookieOptions)
151165 http.SetCookie(rw, cookie)
152166
153167 return cookie, nil
@@ -197,13 +211,18 @@ func ClearExtraCsrfCookies(opts *options.Cookie, rw http.ResponseWriter, req *ht
197211
198212// ClearCookie removes the CSRF cookie
199213func (c *csrf) ClearCookie(rw http.ResponseWriter, req *http.Request) {
200− http.SetCookie(rw, MakeCookieFromOptions(
201− req,
202− c.cookieName(),
203− "",
204− c.cookieOpts,
205− time.Hour*-1,
206− ))
214+ csrfCookieOptions := &CookieOptions{
215+ Name: c.cookieName(),
216+ Value: "",
217+ Domains: c.cookieOpts.Domains,
218+ Expiration: time.Hour * -1,
219+ SameSite: getCSRFSameSite(c.cookieOpts),
220+ Path: c.cookieOpts.Path,
221+ HTTPOnly: c.cookieOpts.HTTPOnly,
222+ Secure: c.cookieOpts.Secure,
223+ }
224+
225+ http.SetCookie(rw, MakeCookieFromOptions(req, csrfCookieOptions))
207226}
208227
209228// encodeCookie MessagePack encodes and encrypts the CSRF and then creates a
pkg/cookies/csrf_per_request_test.go+12−7
@@ -216,13 +216,18 @@ var _ = Describe("CSRF Cookie with non-fixed name Tests", func() {
216216 for _, csrf := range []*csrf{privateCSRF1, privateCSRF2, privateCSRF3} {
217217 encoded, err := csrf.encodeCookie()
218218 Expect(err).ToNot(HaveOccurred())
219− cookie := MakeCookieFromOptions(
220− req,
221− csrf.cookieName(),
222− encoded,
223− csrf.cookieOpts,
224− csrf.cookieOpts.CSRFExpire,
225− )
219+ csrfCookieOptions := &CookieOptions{
220+ Name: csrf.cookieName(),
221+ Value: encoded,
222+ Domains: csrf.cookieOpts.Domains,
223+ Expiration: csrf.cookieOpts.CSRFExpire,
224+ SameSite: getCSRFSameSite(csrf.cookieOpts),
225+ Path: csrf.cookieOpts.Path,
226+ HTTPOnly: csrf.cookieOpts.HTTPOnly,
227+ Secure: csrf.cookieOpts.Secure,
228+ }
229+
230+ cookie := MakeCookieFromOptions(req, csrfCookieOptions)
226231 cookies = append(cookies, fmt.Sprintf("%v=%v", cookie.Name, cookie.Value))
227232 }
228233
pkg/cookies/csrf_test.go+259
@@ -278,4 +278,263 @@ var _ = Describe("CSRF Cookie Tests", func() {
278278 })
279279 })
280280 })
281+
282+ Context("Test Cookie SameSite", func() {
283+ var req *http.Request
284+ var cookieOpts *options.Cookie
285+
286+ testNow := time.Unix(nowEpoch, 0)
287+
288+ BeforeEach(func() {
289+ // we need to reset the time to ensure the cookie is valid
290+ privateCSRF.clock = time.Now
291+
292+ req = &http.Request{
293+ Method: http.MethodGet,
294+ Proto: "HTTP/1.1",
295+ Host: cookieDomain,
296+
297+ URL: &url.URL{
298+ Scheme: "https",
299+ Host: cookieDomain,
300+ Path: cookiePath,
301+ },
302+ }
303+
304+ cookieOpts = &options.Cookie{
305+ Name: cookieName,
306+ Secret: cookieSecret,
307+ Domains: []string{cookieDomain},
308+ Path: cookiePath,
309+ Expire: time.Hour,
310+ Secure: true,
311+ HTTPOnly: true,
312+ CSRFPerRequest: false,
313+ CSRFExpire: time.Hour,
314+ }
315+ })
316+
317+ It("Call SetCookie when CSRF SameSite is not defined. Expected result: CSRF cookie SameSite is the same as session cookie.", func() {
318+ // prepare
319+ cookieOpts.SameSite = sameSiteLax
320+ CSRF, _ := NewCSRF(cookieOpts, "verifier")
321+ rw := httptest.NewRecorder()
322+ CSRF.(*csrf).clock = func() time.Time { return testNow }
323+
324+ // test
325+ _, err := CSRF.SetCookie(rw, req)
326+
327+ // validate
328+ Expect(err).ToNot(HaveOccurred())
329+ Expect(rw.Header().Get("Set-Cookie")).To(ContainSubstring(
330+ fmt.Sprintf(
331+ "; Path=%s; Domain=%s; Max-Age=%d; HttpOnly; Secure; SameSite=Lax",
332+ cookiePath,
333+ cookieDomain,
334+ int(cookieOpts.CSRFExpire.Seconds()),
335+ ),
336+ ))
337+ })
338+
339+ It("Call SetCookie when CSRF SameSite is an empty string. Expected result: CSRF cookie SameSite is the same as session cookie.", func() {
340+ // prepare
341+ cookieOpts.SameSite = sameSiteLax
342+ cookieOpts.CSRFSameSite = ""
343+ CSRF, _ := NewCSRF(cookieOpts, "verifier")
344+ rw := httptest.NewRecorder()
345+ CSRF.(*csrf).clock = func() time.Time { return testNow }
346+
347+ // test
348+ _, err := CSRF.SetCookie(rw, req)
349+
350+ // validate
351+ Expect(err).ToNot(HaveOccurred())
352+ Expect(rw.Header().Get("Set-Cookie")).To(ContainSubstring(
353+ fmt.Sprintf(
354+ "; Path=%s; Domain=%s; Max-Age=%d; HttpOnly; Secure; SameSite=Lax",
355+ cookiePath,
356+ cookieDomain,
357+ int(cookieOpts.CSRFExpire.Seconds()),
358+ ),
359+ ))
360+ })
361+
362+ It("Call SetCookie when CSRF SameSite is 'none'. Expected result: CSRF cookie SameSite is None.", func() {
363+ // prepare
364+ cookieOpts.SameSite = sameSiteLax
365+ cookieOpts.CSRFSameSite = sameSiteNone
366+ CSRF, _ := NewCSRF(cookieOpts, "verifier")
367+ rw := httptest.NewRecorder()
368+ CSRF.(*csrf).clock = func() time.Time { return testNow }
369+
370+ // test
371+ _, err := CSRF.SetCookie(rw, req)
372+
373+ // validate
374+ Expect(err).ToNot(HaveOccurred())
375+ Expect(rw.Header().Get("Set-Cookie")).To(ContainSubstring(
376+ fmt.Sprintf(
377+ "; Path=%s; Domain=%s; Max-Age=%d; HttpOnly; Secure; SameSite=None",
378+ cookiePath,
379+ cookieDomain,
380+ int(cookieOpts.CSRFExpire.Seconds()),
381+ ),
382+ ))
383+ })
384+
385+ It("Call SetCookie when CSRF SameSite is 'strict'. Expected result: CSRF cookie SameSite is Strict.", func() {
386+ // prepare
387+ cookieOpts.SameSite = sameSiteLax
388+ cookieOpts.CSRFSameSite = sameSiteStrict
389+ CSRF, _ := NewCSRF(cookieOpts, "verifier")
390+ rw := httptest.NewRecorder()
391+ CSRF.(*csrf).clock = func() time.Time { return testNow }
392+
393+ // test
394+ _, err := CSRF.SetCookie(rw, req)
395+
396+ // validate
397+ Expect(err).ToNot(HaveOccurred())
398+ Expect(rw.Header().Get("Set-Cookie")).To(ContainSubstring(
399+ fmt.Sprintf(
400+ "; Path=%s; Domain=%s; Max-Age=%d; HttpOnly; Secure; SameSite=Strict",
401+ cookiePath,
402+ cookieDomain,
403+ int(cookieOpts.CSRFExpire.Seconds()),
404+ ),
405+ ))
406+ })
407+
408+ It("Call SetCookie when CSRF SameSite is 'lax'. Expected result: CSRF cookie SameSite is Lax.", func() {
409+ // prepare
410+ cookieOpts.SameSite = sameSiteStrict
411+ cookieOpts.CSRFSameSite = sameSiteLax
412+ CSRF, _ := NewCSRF(cookieOpts, "verifier")
413+ rw := httptest.NewRecorder()
414+ CSRF.(*csrf).clock = func() time.Time { return testNow }
415+
416+ // test
417+ _, err := CSRF.SetCookie(rw, req)
418+
419+ // validate
420+ Expect(err).ToNot(HaveOccurred())
421+ Expect(rw.Header().Get("Set-Cookie")).To(ContainSubstring(
422+ fmt.Sprintf(
423+ "; Path=%s; Domain=%s; Max-Age=%d; HttpOnly; Secure; SameSite=Lax",
424+ cookiePath,
425+ cookieDomain,
426+ int(cookieOpts.CSRFExpire.Seconds()),
427+ ),
428+ ))
429+ })
430+
431+ It("Call ClearCookie when CSRF SameSite is not defined. Expected result: CSRF cookie SameSite is the same as session cookie.", func() {
432+ // prepare
433+ cookieOpts.SameSite = sameSiteLax
434+ CSRF, _ := NewCSRF(cookieOpts, "verifier")
435+ rw := httptest.NewRecorder()
436+ CSRF.(*csrf).clock = func() time.Time { return testNow }
437+
438+ // test
439+ CSRF.ClearCookie(rw, req)
440+
441+ // validate
442+ Expect(rw.Header().Get("Set-Cookie")).To(Equal(
443+ fmt.Sprintf(
444+ "%s=; Path=%s; Domain=%s; Max-Age=0; HttpOnly; Secure; SameSite=Lax",
445+ CSRF.(*csrf).cookieName(),
446+ cookiePath,
447+ cookieDomain,
448+ ),
449+ ))
450+ })
451+
452+ It("Call ClearCookie when CSRF SameSite is an empty string. Expected result: CSRF cookie SameSite is the same as session cookie.", func() {
453+ // prepare
454+ cookieOpts.SameSite = sameSiteLax
455+ cookieOpts.CSRFSameSite = ""
456+ CSRF, _ := NewCSRF(cookieOpts, "verifier")
457+ rw := httptest.NewRecorder()
458+ CSRF.(*csrf).clock = func() time.Time { return testNow }
459+
460+ // test
461+ CSRF.ClearCookie(rw, req)
462+
463+ // validate
464+ Expect(rw.Header().Get("Set-Cookie")).To(Equal(
465+ fmt.Sprintf(
466+ "%s=; Path=%s; Domain=%s; Max-Age=0; HttpOnly; Secure; SameSite=Lax",
467+ CSRF.(*csrf).cookieName(),
468+ cookiePath,
469+ cookieDomain,
470+ ),
471+ ))
472+ })
473+
474+ It("Call ClearCookie when CSRF SameSite is 'none'. Expected result: CSRF cookie SameSite is None.", func() {
475+ // prepare
476+ cookieOpts.SameSite = sameSiteLax
477+ cookieOpts.CSRFSameSite = sameSiteNone
478+ CSRF, _ := NewCSRF(cookieOpts, "verifier")
479+ rw := httptest.NewRecorder()
480+ CSRF.(*csrf).clock = func() time.Time { return testNow }
481+
482+ // test
483+ CSRF.ClearCookie(rw, req)
484+
485+ // validate
486+ Expect(rw.Header().Get("Set-Cookie")).To(Equal(
487+ fmt.Sprintf(
488+ "%s=; Path=%s; Domain=%s; Max-Age=0; HttpOnly; Secure; SameSite=None",
489+ CSRF.(*csrf).cookieName(),
490+ cookiePath,
491+ cookieDomain,
492+ ),
493+ ))
494+ })
495+
496+ It("Call ClearCookie when CSRF SameSite is 'strict'. Expected result: CSRF cookie SameSite is Strict.", func() {
497+ // prepare
498+ cookieOpts.SameSite = sameSiteLax
499+ cookieOpts.CSRFSameSite = sameSiteStrict
500+ CSRF, _ := NewCSRF(cookieOpts, "verifier")
501+ rw := httptest.NewRecorder()
502+ CSRF.(*csrf).clock = func() time.Time { return testNow }
503+
504+ // test
505+ CSRF.ClearCookie(rw, req)
506+
507+ // validate
508+ Expect(rw.Header().Get("Set-Cookie")).To(Equal(
509+ fmt.Sprintf(
510+ "%s=; Path=%s; Domain=%s; Max-Age=0; HttpOnly; Secure; SameSite=Strict",
511+ CSRF.(*csrf).cookieName(),
512+ cookiePath,
513+ cookieDomain,
514+ ),
515+ ))
516+ })
517+
518+ It("Call ClearCookie when CSRF SameSite is 'lax'. Expected result: CSRF cookie SameSite is Lax.", func() {
519+ // prepare
520+ cookieOpts.SameSite = sameSiteStrict
521+ cookieOpts.CSRFSameSite = sameSiteLax
522+ CSRF, _ := NewCSRF(cookieOpts, "verifier")
523+ rw := httptest.NewRecorder()
524+ CSRF.(*csrf).clock = func() time.Time { return testNow }
525+
526+ // test
527+ CSRF.ClearCookie(rw, req)
528+
529+ // validate
530+ Expect(rw.Header().Get("Set-Cookie")).To(Equal(
531+ fmt.Sprintf(
532+ "%s=; Path=%s; Domain=%s; Max-Age=0; HttpOnly; Secure; SameSite=Lax",
533+ CSRF.(*csrf).cookieName(),
534+ cookiePath,
535+ cookieDomain,
536+ ),
537+ ))
538+ })
539+ })
281540})
pkg/sessions/cookie/session_store.go+23−13
@@ -76,7 +76,17 @@ func (s *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) error {
7676
7777 for _, c := range req.Cookies() {
7878 if cookieNameRegex.MatchString(c.Name) {
79− clearCookie := s.makeCookie(req, c.Name, "", time.Hour*-1)
79+ sessionCookieOptions := &pkgcookies.CookieOptions{
80+ Name: c.Name,
81+ Value: "",
82+ Domains: s.Cookie.Domains,
83+ Expiration: time.Hour * -1,
84+ SameSite: s.Cookie.SameSite,
85+ Path: s.Cookie.Path,
86+ HTTPOnly: s.Cookie.HTTPOnly,
87+ Secure: s.Cookie.Secure,
88+ }
89+ clearCookie := pkgcookies.MakeCookieFromOptions(req, sessionCookieOptions)
8090
8191 http.SetCookie(rw, clearCookie)
8292 }
@@ -117,7 +127,7 @@ func (s *SessionStore) setSessionCookie(rw http.ResponseWriter, req *http.Reques
117127 return nil
118128}
119129
120−// makeSessionCookie creates an http.Cookie containing the authenticated user's
130+// makeSessionCookie creates a http.Cookie containing the authenticated user's
121131// authentication details
122132func (s *SessionStore) makeSessionCookie(req *http.Request, value []byte, now time.Time) ([]*http.Cookie, error) {
123133 strValue := string(value)
@@ -132,23 +142,23 @@ func (s *SessionStore) makeSessionCookie(req *http.Request, value []byte, now ti
132142 return nil, err
133143 }
134144 }
135− c := s.makeCookie(req, s.Cookie.Name, strValue, s.Cookie.Expire)
145+ sessionCookieOptions := &pkgcookies.CookieOptions{
146+ Name: s.Cookie.Name,
147+ Value: strValue,
148+ Domains: s.Cookie.Domains,
149+ Expiration: s.Cookie.Expire,
150+ SameSite: s.Cookie.SameSite,
151+ Path: s.Cookie.Path,
152+ HTTPOnly: s.Cookie.HTTPOnly,
153+ Secure: s.Cookie.Secure,
154+ }
155+ c := pkgcookies.MakeCookieFromOptions(req, sessionCookieOptions)
136156 if len(c.String()) > maxCookieLength {
137157 return splitCookie(c), nil
138158 }
139159 return []*http.Cookie{c}, nil
140160}
141161
142−func (s *SessionStore) makeCookie(req *http.Request, name string, value string, expiration time.Duration) *http.Cookie {
143− return pkgcookies.MakeCookieFromOptions(
144− req,
145− name,
146− value,
147− s.Cookie,
148− expiration,
149− )
150−}
151−
152162// NewCookieSessionStore initialises a new instance of the SessionStore from
153163// the configuration given
154164func NewCookieSessionStore(opts *options.SessionOptions, cookieOpts *options.Cookie) (sessions.SessionStore, error) {
pkg/sessions/persistence/ticket.go+23−14
@@ -221,13 +221,17 @@ func (t *ticket) setCookie(rw http.ResponseWriter, req *http.Request, s *session
221221// clearCookie removes any cookies that would be where this ticket
222222// would set them
223223func (t *ticket) clearCookie(rw http.ResponseWriter, req *http.Request) {
224− http.SetCookie(rw, cookies.MakeCookieFromOptions(
225− req,
226− t.options.Name,
227− "",
228− t.options,
229− time.Hour*-1,
230− ))
224+ cookieOptions := &cookies.CookieOptions{
225+ Name: t.options.Name,
226+ Value: "",
227+ Domains: t.options.Domains,
228+ Expiration: time.Hour * -1,
229+ SameSite: t.options.SameSite,
230+ Path: t.options.Path,
231+ HTTPOnly: t.options.HTTPOnly,
232+ Secure: t.options.Secure,
233+ }
234+ http.SetCookie(rw, cookies.MakeCookieFromOptions(req, cookieOptions))
231235}
232236
233237// makeCookie makes a cookie, signing the value if present
@@ -244,13 +248,18 @@ func (t *ticket) makeCookie(req *http.Request, value string, expires time.Durati
244248 }
245249 }
246250
247− return cookies.MakeCookieFromOptions(
248− req,
249− t.options.Name,
250− value,
251− t.options,
252− expires,
253− ), nil
251+ cookieOptions := &cookies.CookieOptions{
252+ Name: t.options.Name,
253+ Value: value,
254+ Domains: t.options.Domains,
255+ Expiration: expires,
256+ SameSite: t.options.SameSite,
257+ Path: t.options.Path,
258+ HTTPOnly: t.options.HTTPOnly,
259+ Secure: t.options.Secure,
260+ }
261+
262+ return cookies.MakeCookieFromOptions(req, cookieOptions), nil
254263}
255264
256265// makeCipher makes a AES-GCM cipher out of the ticket's secret
pkg/sessions/tests/session_store_tests.go+11−1
@@ -422,7 +422,17 @@ func SessionStoreInterfaceTests(in *testInput) {
422422 broken := "BrokenSessionFromADifferentSessionImplementation"
423423 value, err := encryption.SignedValue(in.cookieOpts.Secret, in.cookieOpts.Name, []byte(broken), time.Now())
424424 Expect(err).ToNot(HaveOccurred())
425− cookie := cookiesapi.MakeCookieFromOptions(in.request, in.cookieOpts.Name, value, in.cookieOpts, in.cookieOpts.Expire)
425+ cookieOptions := &cookiesapi.CookieOptions{
426+ Name: in.cookieOpts.Name,
427+ Value: value,
428+ Domains: in.cookieOpts.Domains,
429+ Expiration: in.cookieOpts.Expire,
430+ SameSite: in.cookieOpts.SameSite,
431+ Path: in.cookieOpts.Path,
432+ HTTPOnly: in.cookieOpts.HTTPOnly,
433+ Secure: in.cookieOpts.Secure,
434+ }
435+ cookie := cookiesapi.MakeCookieFromOptions(in.request, cookieOptions)
426436 in.request.AddCookie(cookie)
427437
428438 err = in.ss().Save(in.response, in.request, in.session)