From 17896700ba0bbc346c99621ab5f65bdf4c43c0c1 Mon Sep 17 00:00:00 2001 From: Joost <439100+jvnoije@users.noreply.github.com> Date: Fri, 20 Feb 2026 11:07:06 +0100 Subject: [PATCH 1/5] 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> --- contrib/local-environment/docker-compose-alpha-config.yaml | 1 - contrib/local-environment/docker-compose-gitea.yaml | 1 - contrib/local-environment/docker-compose-keycloak.yaml | 1 - contrib/local-environment/docker-compose-nginx.yaml | 1 - contrib/local-environment/docker-compose-traefik.yaml | 1 - contrib/local-environment/docker-compose.yaml | 1 - 6 files changed, 6 deletions(-) diff --git a/contrib/local-environment/docker-compose-alpha-config.yaml b/contrib/local-environment/docker-compose-alpha-config.yaml index 595ce4e4..95e386ca 100644 --- a/contrib/local-environment/docker-compose-alpha-config.yaml +++ b/contrib/local-environment/docker-compose-alpha-config.yaml @@ -10,7 +10,6 @@ # make alpha-config- (eg make nginx-up, make nginx-down) # # Access http://localhost:4180 to initiate a login cycle -version: "3.0" services: oauth2-proxy: container_name: oauth2-proxy diff --git a/contrib/local-environment/docker-compose-gitea.yaml b/contrib/local-environment/docker-compose-gitea.yaml index 65968fe8..6e8583ea 100644 --- a/contrib/local-environment/docker-compose-gitea.yaml +++ b/contrib/local-environment/docker-compose-gitea.yaml @@ -10,7 +10,6 @@ # # Access http://oauth2-proxy.localtest.me:4180 to initiate a login cycle using user=admin@example.com, password=password # Access http://gitea.localtest.me:3000 with the same credentials to check out the settings -version: '3.0' services: oauth2-proxy: container_name: oauth2-proxy diff --git a/contrib/local-environment/docker-compose-keycloak.yaml b/contrib/local-environment/docker-compose-keycloak.yaml index cc56f4ae..6576a9ef 100644 --- a/contrib/local-environment/docker-compose-keycloak.yaml +++ b/contrib/local-environment/docker-compose-keycloak.yaml @@ -10,7 +10,6 @@ # # Access http://oauth2-proxy.localtest.me:4180 to initiate a login cycle using user=admin@example.com, password=password # Access http://keycloak.localtest.me:9080 with the same credentials to check out the settings -version: '3.0' services: oauth2-proxy: container_name: oauth2-proxy diff --git a/contrib/local-environment/docker-compose-nginx.yaml b/contrib/local-environment/docker-compose-nginx.yaml index 771815b1..69ff0f14 100644 --- a/contrib/local-environment/docker-compose-nginx.yaml +++ b/contrib/local-environment/docker-compose-nginx.yaml @@ -19,7 +19,6 @@ # 127.0.0.1 oauth2-proxy.localhost # 127.0.0.1 httpbin.oauth2-proxy.localhost # 127.0.0.1 oauth2-proxy.oauth2-proxy.localhost -version: "3.0" services: oauth2-proxy: image: quay.io/oauth2-proxy/oauth2-proxy:v7.14.2 diff --git a/contrib/local-environment/docker-compose-traefik.yaml b/contrib/local-environment/docker-compose-traefik.yaml index b5d25e2f..d3a49e0c 100644 --- a/contrib/local-environment/docker-compose-traefik.yaml +++ b/contrib/local-environment/docker-compose-traefik.yaml @@ -19,7 +19,6 @@ # 127.0.0.1 oauth2-proxy.localhost # 127.0.0.1 httpbin.oauth2-proxy.localhost # 127.0.0.1 oauth2-proxy.oauth2-proxy.localhost -version: '3.0' services: oauth2-proxy: diff --git a/contrib/local-environment/docker-compose.yaml b/contrib/local-environment/docker-compose.yaml index 12ddeb68..97bc4cef 100644 --- a/contrib/local-environment/docker-compose.yaml +++ b/contrib/local-environment/docker-compose.yaml @@ -9,7 +9,6 @@ # make (eg. make up, make down) # # Access http://oauth2-proxy.localtest.me:4180 to initiate a login cycle -version: "3.0" services: oauth2-proxy: container_name: oauth2-proxy From d0a07642ac3e00f2ca68f2b13d0cad939e77004b Mon Sep 17 00:00:00 2001 From: Joost <439100+jvnoije@users.noreply.github.com> Date: Fri, 20 Feb 2026 11:18:39 +0100 Subject: [PATCH 2/5] 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> --- CHANGELOG.md | 5 + docs/docs/configuration/overview.md | 9 +- pkg/apis/options/cookie.go | 3 + pkg/cookies/cookies.go | 4 +- pkg/cookies/cookies_suite_test.go | 4 + pkg/cookies/cookies_test.go | 2 +- pkg/cookies/csrf.go | 11 + pkg/cookies/csrf_per_request_test.go | 1 + pkg/cookies/csrf_test.go | 259 ++++++++++++++++++++++ pkg/sessions/cookie/session_store.go | 1 + pkg/sessions/persistence/ticket.go | 2 + pkg/sessions/tests/session_store_tests.go | 2 +- 12 files changed, 295 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76c506ba..12f30458 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,18 @@ ## Release Highlights +- It is now possible to set a SameSite in CSRF cookie different from the one defined for the session cookie. + ## Important Notes +- [#](https://github.com/oauth2-proxy/oauth2-proxy/pull/) New option "--cookie-csrf-samesite" added, to define the SameSite value of CSRF cookie. If option is not configured, then CSRF cookie SameSite is equal to the one configured for the session cookie. + ## Breaking Changes ## Changes since v7.14.2 - [#3183](https://github.com/oauth2-proxy/oauth2-proxy/pull/3183) fix: allow URL parameters to configure username, password and max idle connection timeout if the matching configuration is empty. +- [#](https://github.com/oauth2-proxy/oauth2-proxy/pull/) Added new option to configure the SameSite value of CSRF cookie (e.g.: "--cookie-csrf-samesite"). # V7.14.2 diff --git a/docs/docs/configuration/overview.md b/docs/docs/configuration/overview.md index 7bd7bf07..b2a54e4a 100644 --- a/docs/docs/configuration/overview.md +++ b/docs/docs/configuration/overview.md @@ -83,7 +83,7 @@ Provider specific options can be found on their respective subpages. | flag: `--approval-prompt`
toml: `approval_prompt` | string | OAuth approval_prompt | `"force"` | | flag: `--backend-logout-url`
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 | | | flag: `--client-id`
toml: `client_id` | string | the OAuth Client ID, e.g. `"123456.apps.googleusercontent.com"` | | -| flag: `--client-secret-file`
toml: `client_secret_file` | string | the file with OAuth Client Secret. The file must contain the secret only, with no trailing newline | | +| flag: `--client-secret-file`
toml: `client_secret_file` | string | the file with OAuth Client Secret. The file must contain the secret only, with no trailing newline | | | flag: `--client-secret`
toml: `client_secret` | string | the OAuth Client Secret | | | flag: `--code-challenge-method`
toml: `code_challenge_method` | string | use PKCE code challenges with the specified method. Either 'plain' or 'S256' (recommended) | | | flag: `--insecure-oidc-allow-unverified-email`
toml: `insecure_oidc_allow_unverified_email` | bool | don't fail if an email address in an id_token is not verified | false | @@ -102,7 +102,7 @@ Provider specific options can be found on their respective subpages. | flag: `--oidc-public-key-file`
toml: `oidc_public_key_files` | string | Path to public key file in PEM format to use for verifying JWT tokens (may be given multiple times). Required if OIDC discovery is disabled na JWKS URL isn't provided | string \| list | | flag: `--profile-url`
toml: `profile_url` | string | Profile access endpoint | | | flag: `--prompt`
toml: `prompt` | string | [OIDC prompt](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest); if present, `approval-prompt` is ignored | `""` | -| flag: `--provider-ca-file`
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. | +| flag: `--provider-ca-file`
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. | | | flag: `--provider-display-name`
toml: `provider_display_name` | string | Override the provider's name with the given string; used for the sign-in page | (depends on provider) | | flag: `--provider`
toml: `provider` | string | OAuth provider | google | | flag: `--pubjwk-url`
toml: `pubjwk_url` | string | JWK pubkey access endpoint: required by login.gov | | @@ -121,6 +121,7 @@ Provider specific options can be found on their respective subpages. | flag: `--cookie-csrf-per-request`
toml:`cookie_csrf_per_request` | bool | Enable having different CSRF cookies per request, making it possible to have parallel requests. | false | | flag: `--cookie-csrf-per-request-limit`
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" | | flag: `--cookie-domain`
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). | | +| flag: `--cookie-csrf-samesite`
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. | `""` | | flag: `--cookie-expire`
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 | | flag: `--cookie-httponly`
toml: `cookie_httponly` | bool | set HttpOnly cookie flag | true | | flag: `--cookie-name`
toml: `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"` | @@ -128,7 +129,7 @@ Provider specific options can be found on their respective subpages. | flag: `--cookie-refresh`
toml: `cookie_refresh` | duration | refresh the cookie after this duration; `0` to disable; not supported by all providers [^1] | | | flag: `--cookie-samesite`
toml: `cookie_samesite` | string | set SameSite cookie attribute (`"lax"`, `"strict"`, `"none"`, or `""`). | `""` | | flag: `--cookie-secret`
toml: `cookie_secret` | string | the seed string for secure cookies (optionally base64 encoded) | | -| flag: `--cookie-secret-file`
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 | | +| flag: `--cookie-secret-file`
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 | | | flag: `--cookie-secure`
toml: `cookie_secure` | bool | set [secure (HTTPS only) cookie flag](https://owasp.org/www-community/controls/SecureFlag) | true | [^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) @@ -174,7 +175,7 @@ Provider specific options can be found on their respective subpages. | Flag / Config Field | Type | Description | Default | | ----------------------------------------------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------- | ------- | | flag: `--banner`
toml: `banner` | string | custom (html) banner string. Use `"-"` to disable default banner. | | -| flag: `--custom-sign-in-logo`
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. | +| flag: `--custom-sign-in-logo`
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. | | | flag: `--custom-templates-dir`
toml: `custom_templates_dir` | string | path to custom html templates | | | flag: `--display-htpasswd-form`
toml: `display_htpasswd_form` | bool | display username / password login form if an htpasswd file is provided | true | | flag: `--footer`
toml: `footer` | string | custom (html) footer string. Use `"-"` to disable default footer. (Can be used to obfuscate the version) | | diff --git a/pkg/apis/options/cookie.go b/pkg/apis/options/cookie.go index 3653b7d0..3dee9505 100644 --- a/pkg/apis/options/cookie.go +++ b/pkg/apis/options/cookie.go @@ -24,6 +24,7 @@ type Cookie struct { CSRFPerRequest bool `flag:"cookie-csrf-per-request" cfg:"cookie_csrf_per_request"` CSRFPerRequestLimit int `flag:"cookie-csrf-per-request-limit" cfg:"cookie_csrf_per_request_limit"` CSRFExpire time.Duration `flag:"cookie-csrf-expire" cfg:"cookie_csrf_expire"` + CSRFSameSite string `flag:"cookie-csrf-samesite" cfg:"cookie_csrf_samesite"` } func cookieFlagSet() *pflag.FlagSet { @@ -42,6 +43,7 @@ func cookieFlagSet() *pflag.FlagSet { 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.") 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.") flagSet.Duration("cookie-csrf-expire", time.Duration(15)*time.Minute, "expire timeframe for CSRF cookie") + 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.") return flagSet } @@ -61,6 +63,7 @@ func cookieDefaults() Cookie { CSRFPerRequest: false, CSRFPerRequestLimit: 0, CSRFExpire: time.Duration(15) * time.Minute, + CSRFSameSite: "", } } diff --git a/pkg/cookies/cookies.go b/pkg/cookies/cookies.go index 24ae2841..178f77f4 100644 --- a/pkg/cookies/cookies.go +++ b/pkg/cookies/cookies.go @@ -14,7 +14,7 @@ import ( // MakeCookieFromOptions constructs a cookie based on the given *options.CookieOptions, // value and creation time -func MakeCookieFromOptions(req *http.Request, name string, value string, opts *options.Cookie, expiration time.Duration) *http.Cookie { +func MakeCookieFromOptions(req *http.Request, name string, value string, opts *options.Cookie, expiration time.Duration, sameSite string) *http.Cookie { domain := GetCookieDomain(req, opts.Domains) // If nothing matches, create the cookie with the shortest domain if domain == "" && len(opts.Domains) > 0 { @@ -32,7 +32,7 @@ func MakeCookieFromOptions(req *http.Request, name string, value string, opts *o Domain: domain, HttpOnly: opts.HTTPOnly, Secure: opts.Secure, - SameSite: ParseSameSite(opts.SameSite), + SameSite: ParseSameSite(sameSite), } if expiration > time.Duration(0) { diff --git a/pkg/cookies/cookies_suite_test.go b/pkg/cookies/cookies_suite_test.go index f4893cbd..a11dd798 100644 --- a/pkg/cookies/cookies_suite_test.go +++ b/pkg/cookies/cookies_suite_test.go @@ -17,6 +17,10 @@ const ( cookieDomain = "o2p.cookies.test" cookiePath = "/cookie-tests" + sameSiteLax = "lax" + sameSiteStrict = "strict" + sameSiteNone = "none" + nowEpoch = 1609366421 ) diff --git a/pkg/cookies/cookies_test.go b/pkg/cookies/cookies_test.go index ef0fbd4f..5664dffe 100644 --- a/pkg/cookies/cookies_test.go +++ b/pkg/cookies/cookies_test.go @@ -106,7 +106,7 @@ var _ = Describe("Cookie Tests", func() { ) Expect(err).ToNot(HaveOccurred()) - Expect(MakeCookieFromOptions(req, in.name, in.value, &in.opts, in.expiration).MaxAge).To(Equal(in.expectedOutput)) + Expect(MakeCookieFromOptions(req, in.name, in.value, &in.opts, in.expiration, in.opts.SameSite).MaxAge).To(Equal(in.expectedOutput)) }, Entry("persistent cookie", makeCookieFromOptionsTableInput{ host: "www.cookies.test", diff --git a/pkg/cookies/csrf.go b/pkg/cookies/csrf.go index 939578a2..cef7a029 100644 --- a/pkg/cookies/csrf.go +++ b/pkg/cookies/csrf.go @@ -134,6 +134,15 @@ func (c *csrf) SetSessionNonce(s *sessions.SessionState) { s.Nonce = c.OIDCNonce } +// getCSRFSameSite get the CSRF same site +func getCSRFSameSite(opts *options.Cookie) string { + sameSite := opts.CSRFSameSite + if sameSite == "" { + sameSite = opts.SameSite + } + return sameSite +} + // SetCookie encodes the CSRF to a signed cookie and sets it on the ResponseWriter func (c *csrf) SetCookie(rw http.ResponseWriter, req *http.Request) (*http.Cookie, error) { encoded, err := c.encodeCookie() @@ -147,6 +156,7 @@ func (c *csrf) SetCookie(rw http.ResponseWriter, req *http.Request) (*http.Cooki encoded, c.cookieOpts, c.cookieOpts.CSRFExpire, + getCSRFSameSite(c.cookieOpts), ) http.SetCookie(rw, cookie) @@ -203,6 +213,7 @@ func (c *csrf) ClearCookie(rw http.ResponseWriter, req *http.Request) { "", c.cookieOpts, time.Hour*-1, + getCSRFSameSite(c.cookieOpts), )) } diff --git a/pkg/cookies/csrf_per_request_test.go b/pkg/cookies/csrf_per_request_test.go index 6a17013b..6ec07d83 100644 --- a/pkg/cookies/csrf_per_request_test.go +++ b/pkg/cookies/csrf_per_request_test.go @@ -222,6 +222,7 @@ var _ = Describe("CSRF Cookie with non-fixed name Tests", func() { encoded, csrf.cookieOpts, csrf.cookieOpts.CSRFExpire, + csrf.cookieOpts.CSRFSameSite, ) cookies = append(cookies, fmt.Sprintf("%v=%v", cookie.Name, cookie.Value)) } diff --git a/pkg/cookies/csrf_test.go b/pkg/cookies/csrf_test.go index 085b91df..ff7491f2 100644 --- a/pkg/cookies/csrf_test.go +++ b/pkg/cookies/csrf_test.go @@ -254,4 +254,263 @@ var _ = Describe("CSRF Cookie Tests", func() { }) }) }) + + Context("Test Cookie SameSite", func() { + var req *http.Request + var cookieOpts *options.Cookie + + testNow := time.Unix(nowEpoch, 0) + + BeforeEach(func() { + // we need to reset the time to ensure the cookie is valid + privateCSRF.clock = time.Now + + req = &http.Request{ + Method: http.MethodGet, + Proto: "HTTP/1.1", + Host: cookieDomain, + + URL: &url.URL{ + Scheme: "https", + Host: cookieDomain, + Path: cookiePath, + }, + } + + cookieOpts = &options.Cookie{ + Name: cookieName, + Secret: cookieSecret, + Domains: []string{cookieDomain}, + Path: cookiePath, + Expire: time.Hour, + Secure: true, + HTTPOnly: true, + CSRFPerRequest: false, + CSRFExpire: time.Hour, + } + }) + + It("Call SetCookie when CSRF SameSite is not defined. Expected result: CSRF cookie SameSite is the same as session cookie.", func() { + // prepare + cookieOpts.SameSite = sameSiteLax + CSRF, _ := NewCSRF(cookieOpts, "verifier") + rw := httptest.NewRecorder() + CSRF.(*csrf).clock = func() time.Time { return testNow } + + // test + _, err := CSRF.SetCookie(rw, req) + + // validate + Expect(err).ToNot(HaveOccurred()) + Expect(rw.Header().Get("Set-Cookie")).To(ContainSubstring( + fmt.Sprintf( + "; Path=%s; Domain=%s; Max-Age=%d; HttpOnly; Secure; SameSite=Lax", + cookiePath, + cookieDomain, + int(cookieOpts.CSRFExpire.Seconds()), + ), + )) + }) + + It("Call SetCookie when CSRF SameSite is an empty string. Expected result: CSRF cookie SameSite is the same as session cookie.", func() { + // prepare + cookieOpts.SameSite = sameSiteLax + cookieOpts.CSRFSameSite = "" + CSRF, _ := NewCSRF(cookieOpts, "verifier") + rw := httptest.NewRecorder() + CSRF.(*csrf).clock = func() time.Time { return testNow } + + // test + _, err := CSRF.SetCookie(rw, req) + + // validate + Expect(err).ToNot(HaveOccurred()) + Expect(rw.Header().Get("Set-Cookie")).To(ContainSubstring( + fmt.Sprintf( + "; Path=%s; Domain=%s; Max-Age=%d; HttpOnly; Secure; SameSite=Lax", + cookiePath, + cookieDomain, + int(cookieOpts.CSRFExpire.Seconds()), + ), + )) + }) + + It("Call SetCookie when CSRF SameSite is 'none'. Expected result: CSRF cookie SameSite is None.", func() { + // prepare + cookieOpts.SameSite = sameSiteLax + cookieOpts.CSRFSameSite = sameSiteNone + CSRF, _ := NewCSRF(cookieOpts, "verifier") + rw := httptest.NewRecorder() + CSRF.(*csrf).clock = func() time.Time { return testNow } + + // test + _, err := CSRF.SetCookie(rw, req) + + // validate + Expect(err).ToNot(HaveOccurred()) + Expect(rw.Header().Get("Set-Cookie")).To(ContainSubstring( + fmt.Sprintf( + "; Path=%s; Domain=%s; Max-Age=%d; HttpOnly; Secure; SameSite=None", + cookiePath, + cookieDomain, + int(cookieOpts.CSRFExpire.Seconds()), + ), + )) + }) + + It("Call SetCookie when CSRF SameSite is 'strict'. Expected result: CSRF cookie SameSite is Strict.", func() { + // prepare + cookieOpts.SameSite = sameSiteLax + cookieOpts.CSRFSameSite = sameSiteStrict + CSRF, _ := NewCSRF(cookieOpts, "verifier") + rw := httptest.NewRecorder() + CSRF.(*csrf).clock = func() time.Time { return testNow } + + // test + _, err := CSRF.SetCookie(rw, req) + + // validate + Expect(err).ToNot(HaveOccurred()) + Expect(rw.Header().Get("Set-Cookie")).To(ContainSubstring( + fmt.Sprintf( + "; Path=%s; Domain=%s; Max-Age=%d; HttpOnly; Secure; SameSite=Strict", + cookiePath, + cookieDomain, + int(cookieOpts.CSRFExpire.Seconds()), + ), + )) + }) + + It("Call SetCookie when CSRF SameSite is 'lax'. Expected result: CSRF cookie SameSite is Lax.", func() { + // prepare + cookieOpts.SameSite = sameSiteStrict + cookieOpts.CSRFSameSite = sameSiteLax + CSRF, _ := NewCSRF(cookieOpts, "verifier") + rw := httptest.NewRecorder() + CSRF.(*csrf).clock = func() time.Time { return testNow } + + // test + _, err := CSRF.SetCookie(rw, req) + + // validate + Expect(err).ToNot(HaveOccurred()) + Expect(rw.Header().Get("Set-Cookie")).To(ContainSubstring( + fmt.Sprintf( + "; Path=%s; Domain=%s; Max-Age=%d; HttpOnly; Secure; SameSite=Lax", + cookiePath, + cookieDomain, + int(cookieOpts.CSRFExpire.Seconds()), + ), + )) + }) + + It("Call ClearCookie when CSRF SameSite is not defined. Expected result: CSRF cookie SameSite is the same as session cookie.", func() { + // prepare + cookieOpts.SameSite = sameSiteLax + CSRF, _ := NewCSRF(cookieOpts, "verifier") + rw := httptest.NewRecorder() + CSRF.(*csrf).clock = func() time.Time { return testNow } + + // test + CSRF.ClearCookie(rw, req) + + // validate + Expect(rw.Header().Get("Set-Cookie")).To(Equal( + fmt.Sprintf( + "%s=; Path=%s; Domain=%s; Max-Age=0; HttpOnly; Secure; SameSite=Lax", + CSRF.(*csrf).cookieName(), + cookiePath, + cookieDomain, + ), + )) + }) + + It("Call ClearCookie when CSRF SameSite is an empty string. Expected result: CSRF cookie SameSite is the same as session cookie.", func() { + // prepare + cookieOpts.SameSite = sameSiteLax + cookieOpts.CSRFSameSite = "" + CSRF, _ := NewCSRF(cookieOpts, "verifier") + rw := httptest.NewRecorder() + CSRF.(*csrf).clock = func() time.Time { return testNow } + + // test + CSRF.ClearCookie(rw, req) + + // validate + Expect(rw.Header().Get("Set-Cookie")).To(Equal( + fmt.Sprintf( + "%s=; Path=%s; Domain=%s; Max-Age=0; HttpOnly; Secure; SameSite=Lax", + CSRF.(*csrf).cookieName(), + cookiePath, + cookieDomain, + ), + )) + }) + + It("Call ClearCookie when CSRF SameSite is 'none'. Expected result: CSRF cookie SameSite is None.", func() { + // prepare + cookieOpts.SameSite = sameSiteLax + cookieOpts.CSRFSameSite = sameSiteNone + CSRF, _ := NewCSRF(cookieOpts, "verifier") + rw := httptest.NewRecorder() + CSRF.(*csrf).clock = func() time.Time { return testNow } + + // test + CSRF.ClearCookie(rw, req) + + // validate + Expect(rw.Header().Get("Set-Cookie")).To(Equal( + fmt.Sprintf( + "%s=; Path=%s; Domain=%s; Max-Age=0; HttpOnly; Secure; SameSite=None", + CSRF.(*csrf).cookieName(), + cookiePath, + cookieDomain, + ), + )) + }) + + It("Call ClearCookie when CSRF SameSite is 'strict'. Expected result: CSRF cookie SameSite is Strict.", func() { + // prepare + cookieOpts.SameSite = sameSiteLax + cookieOpts.CSRFSameSite = sameSiteStrict + CSRF, _ := NewCSRF(cookieOpts, "verifier") + rw := httptest.NewRecorder() + CSRF.(*csrf).clock = func() time.Time { return testNow } + + // test + CSRF.ClearCookie(rw, req) + + // validate + Expect(rw.Header().Get("Set-Cookie")).To(Equal( + fmt.Sprintf( + "%s=; Path=%s; Domain=%s; Max-Age=0; HttpOnly; Secure; SameSite=Strict", + CSRF.(*csrf).cookieName(), + cookiePath, + cookieDomain, + ), + )) + }) + + It("Call ClearCookie when CSRF SameSite is 'lax'. Expected result: CSRF cookie SameSite is Lax.", func() { + // prepare + cookieOpts.SameSite = sameSiteStrict + cookieOpts.CSRFSameSite = sameSiteLax + CSRF, _ := NewCSRF(cookieOpts, "verifier") + rw := httptest.NewRecorder() + CSRF.(*csrf).clock = func() time.Time { return testNow } + + // test + CSRF.ClearCookie(rw, req) + + // validate + Expect(rw.Header().Get("Set-Cookie")).To(Equal( + fmt.Sprintf( + "%s=; Path=%s; Domain=%s; Max-Age=0; HttpOnly; Secure; SameSite=Lax", + CSRF.(*csrf).cookieName(), + cookiePath, + cookieDomain, + ), + )) + }) + }) }) diff --git a/pkg/sessions/cookie/session_store.go b/pkg/sessions/cookie/session_store.go index 095bc0e7..c5fab5c0 100644 --- a/pkg/sessions/cookie/session_store.go +++ b/pkg/sessions/cookie/session_store.go @@ -146,6 +146,7 @@ func (s *SessionStore) makeCookie(req *http.Request, name string, value string, value, s.Cookie, expiration, + s.Cookie.SameSite, ) } diff --git a/pkg/sessions/persistence/ticket.go b/pkg/sessions/persistence/ticket.go index 56d6bd9b..98ee32f7 100644 --- a/pkg/sessions/persistence/ticket.go +++ b/pkg/sessions/persistence/ticket.go @@ -227,6 +227,7 @@ func (t *ticket) clearCookie(rw http.ResponseWriter, req *http.Request) { "", t.options, time.Hour*-1, + t.options.SameSite, )) } @@ -250,6 +251,7 @@ func (t *ticket) makeCookie(req *http.Request, value string, expires time.Durati value, t.options, expires, + t.options.SameSite, ), nil } diff --git a/pkg/sessions/tests/session_store_tests.go b/pkg/sessions/tests/session_store_tests.go index 05b67d8d..fe3a324b 100644 --- a/pkg/sessions/tests/session_store_tests.go +++ b/pkg/sessions/tests/session_store_tests.go @@ -422,7 +422,7 @@ func SessionStoreInterfaceTests(in *testInput) { broken := "BrokenSessionFromADifferentSessionImplementation" value, err := encryption.SignedValue(in.cookieOpts.Secret, in.cookieOpts.Name, []byte(broken), time.Now()) Expect(err).ToNot(HaveOccurred()) - cookie := cookiesapi.MakeCookieFromOptions(in.request, in.cookieOpts.Name, value, in.cookieOpts, in.cookieOpts.Expire) + cookie := cookiesapi.MakeCookieFromOptions(in.request, in.cookieOpts.Name, value, in.cookieOpts, in.cookieOpts.Expire, in.cookieOpts.SameSite) in.request.AddCookie(cookie) err = in.ss().Save(in.response, in.request, in.session) From 04c25fca19d948036fd623d47d2f8aa91593e947 Mon Sep 17 00:00:00 2001 From: Joost <439100+jvnoije@users.noreply.github.com> Date: Fri, 20 Feb 2026 11:22:12 +0100 Subject: [PATCH 3/5] Update CHANGELOG.md Signed-off-by: Joost <439100+jvnoije@users.noreply.github.com> --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12f30458..d5227612 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,14 +6,14 @@ ## Important Notes -- [#](https://github.com/oauth2-proxy/oauth2-proxy/pull/) New option "--cookie-csrf-samesite" added, to define the SameSite value of CSRF cookie. If option is not configured, then CSRF cookie SameSite is equal to the one configured for the session cookie. +- [#3347](https://github.com/oauth2-proxy/oauth2-proxy/pull/3347) New option "--cookie-csrf-samesite" added, to define the SameSite value of CSRF cookie. If option is not configured, then CSRF cookie SameSite is equal to the one configured for the session cookie. ## Breaking Changes ## Changes since v7.14.2 - [#3183](https://github.com/oauth2-proxy/oauth2-proxy/pull/3183) fix: allow URL parameters to configure username, password and max idle connection timeout if the matching configuration is empty. -- [#](https://github.com/oauth2-proxy/oauth2-proxy/pull/) Added new option to configure the SameSite value of CSRF cookie (e.g.: "--cookie-csrf-samesite"). +- [#3347](https://github.com/oauth2-proxy/oauth2-proxy/pull/3347) Added new option to configure the SameSite value of CSRF cookie (e.g.: "--cookie-csrf-samesite"). # V7.14.2 From 0805074e2544b80ffc38997fe6e9e24023206be0 Mon Sep 17 00:00:00 2001 From: Joost <439100+jvnoije@users.noreply.github.com> Date: Mon, 16 Mar 2026 10:11:12 +0100 Subject: [PATCH 4/5] Removed release information (review comment) Signed-off-by: Joost <439100+jvnoije@users.noreply.github.com> --- CHANGELOG.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 047596c8..68fa7c24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,8 @@ ## Release Highlights -- It is now possible to set a SameSite in CSRF cookie different from the one defined for the session cookie. - ## Important Notes -- [#3347](https://github.com/oauth2-proxy/oauth2-proxy/pull/3347) New option "--cookie-csrf-samesite" added, to define the SameSite value of CSRF cookie. If option is not configured, then CSRF cookie SameSite is equal to the one configured for the session cookie. - ## Breaking Changes ## Changes since v7.14.3 From 0bf690d44c251223d1caaa4aed6d8ba1be9044f0 Mon Sep 17 00:00:00 2001 From: Joost <439100+jvnoije@users.noreply.github.com> Date: Mon, 16 Mar 2026 10:56:04 +0100 Subject: [PATCH 5/5] All cookie variables in a struct Signed-off-by: Joost <439100+jvnoije@users.noreply.github.com> --- pkg/cookies/cookies.go | 28 +++++--- pkg/cookies/cookies_test.go | 82 +++++++++-------------- pkg/cookies/csrf.go | 40 ++++++----- pkg/cookies/csrf_per_request_test.go | 20 +++--- pkg/sessions/cookie/session_store.go | 37 ++++++---- pkg/sessions/persistence/ticket.go | 39 ++++++----- pkg/sessions/tests/session_store_tests.go | 12 +++- 7 files changed, 144 insertions(+), 114 deletions(-) diff --git a/pkg/cookies/cookies.go b/pkg/cookies/cookies.go index 178f77f4..be5ee451 100644 --- a/pkg/cookies/cookies.go +++ b/pkg/cookies/cookies.go @@ -7,14 +7,24 @@ import ( "strings" "time" - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" requestutil "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests/util" ) +type CookieOptions struct { + Name string + Value string + Domains []string + Expiration time.Duration + SameSite string + Path string + HTTPOnly bool + Secure bool +} + // MakeCookieFromOptions constructs a cookie based on the given *options.CookieOptions, // value and creation time -func MakeCookieFromOptions(req *http.Request, name string, value string, opts *options.Cookie, expiration time.Duration, sameSite string) *http.Cookie { +func MakeCookieFromOptions(req *http.Request, opts *CookieOptions) *http.Cookie { domain := GetCookieDomain(req, opts.Domains) // If nothing matches, create the cookie with the shortest domain if domain == "" && len(opts.Domains) > 0 { @@ -26,18 +36,18 @@ func MakeCookieFromOptions(req *http.Request, name string, value string, opts *o } c := &http.Cookie{ - Name: name, - Value: value, + Name: opts.Name, + Value: opts.Value, Path: opts.Path, Domain: domain, HttpOnly: opts.HTTPOnly, Secure: opts.Secure, - SameSite: ParseSameSite(sameSite), + SameSite: ParseSameSite(opts.SameSite), } - if expiration > time.Duration(0) { - c.MaxAge = int(expiration.Seconds()) - } else if expiration < time.Duration(0) { + if opts.Expiration > time.Duration(0) { + c.MaxAge = int(opts.Expiration.Seconds()) + } else if opts.Expiration < time.Duration(0) { c.MaxAge = -1 } @@ -58,7 +68,7 @@ func GetCookieDomain(req *http.Request, cookieDomains []string) string { return "" } -// Parse a valid http.SameSite value from a user supplied string for use of making cookies. +// ParseSameSite a valid http.SameSite value from a user supplied string for use of making cookies. func ParseSameSite(v string) http.SameSite { switch v { case "lax": diff --git a/pkg/cookies/cookies_test.go b/pkg/cookies/cookies_test.go index 5664dffe..b67f8a69 100644 --- a/pkg/cookies/cookies_test.go +++ b/pkg/cookies/cookies_test.go @@ -5,8 +5,6 @@ import ( "net/http" "time" - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" - middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -82,16 +80,12 @@ var _ = Describe("Cookie Tests", func() { Context("MakeCookieFromOptions", func() { type makeCookieFromOptionsTableInput struct { host string - name string - value string - opts options.Cookie - expiration time.Duration + opts CookieOptions now time.Time expectedOutput int } validName := "_oauth2_proxy" - validSecret := "secretthirtytwobytes+abcdefghijk" domains := []string{"www.cookies.test"} now := time.Now() @@ -106,62 +100,50 @@ var _ = Describe("Cookie Tests", func() { ) Expect(err).ToNot(HaveOccurred()) - Expect(MakeCookieFromOptions(req, in.name, in.value, &in.opts, in.expiration, in.opts.SameSite).MaxAge).To(Equal(in.expectedOutput)) + Expect(MakeCookieFromOptions(req, &in.opts).MaxAge).To(Equal(in.expectedOutput)) }, Entry("persistent cookie", makeCookieFromOptionsTableInput{ - host: "www.cookies.test", - name: validName, - value: "1", - opts: options.Cookie{ - Name: validName, - Secret: validSecret, - Domains: domains, - Path: "", - Expire: time.Hour, - Refresh: 15 * time.Minute, - Secure: true, - HTTPOnly: false, - SameSite: "", + host: "www.cookies.test", + opts: CookieOptions{ + Name: validName, + Value: "1", + Domains: domains, + Expiration: 15 * time.Minute, + SameSite: "", + Path: "", + HTTPOnly: false, + Secure: true, }, - expiration: 15 * time.Minute, now: now, expectedOutput: int((15 * time.Minute).Seconds()), }), Entry("persistent cookie to be cleared", makeCookieFromOptionsTableInput{ - host: "www.cookies.test", - name: validName, - value: "1", - opts: options.Cookie{ - Name: validName, - Secret: validSecret, - Domains: domains, - Path: "", - Expire: time.Hour * -1, - Refresh: 15 * time.Minute, - Secure: true, - HTTPOnly: false, - SameSite: "", + host: "www.cookies.test", + opts: CookieOptions{ + Name: validName, + Value: "1", + Domains: domains, + Expiration: time.Hour * -1, + SameSite: "", + Path: "", + HTTPOnly: false, + Secure: true, }, - expiration: time.Hour * -1, now: now, expectedOutput: -1, }), Entry("session cookie", makeCookieFromOptionsTableInput{ - host: "www.cookies.test", - name: validName, - value: "1", - opts: options.Cookie{ - Name: validName, - Secret: validSecret, - Domains: domains, - Path: "", - Expire: 0, - Refresh: 15 * time.Minute, - Secure: true, - HTTPOnly: false, - SameSite: "", + host: "www.cookies.test", + opts: CookieOptions{ + Name: validName, + Value: "1", + Domains: domains, + Expiration: 0, + SameSite: "", + Path: "", + HTTPOnly: false, + Secure: true, }, - expiration: 0, now: now, expectedOutput: expectedMaxAge, }), diff --git a/pkg/cookies/csrf.go b/pkg/cookies/csrf.go index cef7a029..ce17f164 100644 --- a/pkg/cookies/csrf.go +++ b/pkg/cookies/csrf.go @@ -150,14 +150,18 @@ func (c *csrf) SetCookie(rw http.ResponseWriter, req *http.Request) (*http.Cooki return nil, err } - cookie := MakeCookieFromOptions( - req, - c.cookieName(), - encoded, - c.cookieOpts, - c.cookieOpts.CSRFExpire, - getCSRFSameSite(c.cookieOpts), - ) + csrfCookieOptions := &CookieOptions{ + Name: c.cookieName(), + Value: encoded, + Domains: c.cookieOpts.Domains, + Expiration: c.cookieOpts.CSRFExpire, + SameSite: getCSRFSameSite(c.cookieOpts), + Path: c.cookieOpts.Path, + HTTPOnly: c.cookieOpts.HTTPOnly, + Secure: c.cookieOpts.Secure, + } + + cookie := MakeCookieFromOptions(req, csrfCookieOptions) http.SetCookie(rw, cookie) return cookie, nil @@ -207,14 +211,18 @@ func ClearExtraCsrfCookies(opts *options.Cookie, rw http.ResponseWriter, req *ht // ClearCookie removes the CSRF cookie func (c *csrf) ClearCookie(rw http.ResponseWriter, req *http.Request) { - http.SetCookie(rw, MakeCookieFromOptions( - req, - c.cookieName(), - "", - c.cookieOpts, - time.Hour*-1, - getCSRFSameSite(c.cookieOpts), - )) + csrfCookieOptions := &CookieOptions{ + Name: c.cookieName(), + Value: "", + Domains: c.cookieOpts.Domains, + Expiration: time.Hour * -1, + SameSite: getCSRFSameSite(c.cookieOpts), + Path: c.cookieOpts.Path, + HTTPOnly: c.cookieOpts.HTTPOnly, + Secure: c.cookieOpts.Secure, + } + + http.SetCookie(rw, MakeCookieFromOptions(req, csrfCookieOptions)) } // encodeCookie MessagePack encodes and encrypts the CSRF and then creates a diff --git a/pkg/cookies/csrf_per_request_test.go b/pkg/cookies/csrf_per_request_test.go index 6ec07d83..59ff0a8a 100644 --- a/pkg/cookies/csrf_per_request_test.go +++ b/pkg/cookies/csrf_per_request_test.go @@ -216,14 +216,18 @@ var _ = Describe("CSRF Cookie with non-fixed name Tests", func() { for _, csrf := range []*csrf{privateCSRF1, privateCSRF2, privateCSRF3} { encoded, err := csrf.encodeCookie() Expect(err).ToNot(HaveOccurred()) - cookie := MakeCookieFromOptions( - req, - csrf.cookieName(), - encoded, - csrf.cookieOpts, - csrf.cookieOpts.CSRFExpire, - csrf.cookieOpts.CSRFSameSite, - ) + csrfCookieOptions := &CookieOptions{ + Name: csrf.cookieName(), + Value: encoded, + Domains: csrf.cookieOpts.Domains, + Expiration: csrf.cookieOpts.CSRFExpire, + SameSite: getCSRFSameSite(csrf.cookieOpts), + Path: csrf.cookieOpts.Path, + HTTPOnly: csrf.cookieOpts.HTTPOnly, + Secure: csrf.cookieOpts.Secure, + } + + cookie := MakeCookieFromOptions(req, csrfCookieOptions) cookies = append(cookies, fmt.Sprintf("%v=%v", cookie.Name, cookie.Value)) } diff --git a/pkg/sessions/cookie/session_store.go b/pkg/sessions/cookie/session_store.go index c5fab5c0..a4da3734 100644 --- a/pkg/sessions/cookie/session_store.go +++ b/pkg/sessions/cookie/session_store.go @@ -76,7 +76,17 @@ func (s *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) error { for _, c := range req.Cookies() { if cookieNameRegex.MatchString(c.Name) { - clearCookie := s.makeCookie(req, c.Name, "", time.Hour*-1) + sessionCookieOptions := &pkgcookies.CookieOptions{ + Name: c.Name, + Value: "", + Domains: s.Cookie.Domains, + Expiration: time.Hour * -1, + SameSite: s.Cookie.SameSite, + Path: s.Cookie.Path, + HTTPOnly: s.Cookie.HTTPOnly, + Secure: s.Cookie.Secure, + } + clearCookie := pkgcookies.MakeCookieFromOptions(req, sessionCookieOptions) http.SetCookie(rw, clearCookie) } @@ -117,7 +127,7 @@ func (s *SessionStore) setSessionCookie(rw http.ResponseWriter, req *http.Reques return nil } -// makeSessionCookie creates an http.Cookie containing the authenticated user's +// makeSessionCookie creates a http.Cookie containing the authenticated user's // authentication details func (s *SessionStore) makeSessionCookie(req *http.Request, value []byte, now time.Time) ([]*http.Cookie, error) { strValue := string(value) @@ -132,24 +142,23 @@ func (s *SessionStore) makeSessionCookie(req *http.Request, value []byte, now ti return nil, err } } - c := s.makeCookie(req, s.Cookie.Name, strValue, s.Cookie.Expire) + sessionCookieOptions := &pkgcookies.CookieOptions{ + Name: s.Cookie.Name, + Value: strValue, + Domains: s.Cookie.Domains, + Expiration: s.Cookie.Expire, + SameSite: s.Cookie.SameSite, + Path: s.Cookie.Path, + HTTPOnly: s.Cookie.HTTPOnly, + Secure: s.Cookie.Secure, + } + c := pkgcookies.MakeCookieFromOptions(req, sessionCookieOptions) if len(c.String()) > maxCookieLength { return splitCookie(c), nil } return []*http.Cookie{c}, nil } -func (s *SessionStore) makeCookie(req *http.Request, name string, value string, expiration time.Duration) *http.Cookie { - return pkgcookies.MakeCookieFromOptions( - req, - name, - value, - s.Cookie, - expiration, - s.Cookie.SameSite, - ) -} - // NewCookieSessionStore initialises a new instance of the SessionStore from // the configuration given func NewCookieSessionStore(opts *options.SessionOptions, cookieOpts *options.Cookie) (sessions.SessionStore, error) { diff --git a/pkg/sessions/persistence/ticket.go b/pkg/sessions/persistence/ticket.go index 98ee32f7..c955143a 100644 --- a/pkg/sessions/persistence/ticket.go +++ b/pkg/sessions/persistence/ticket.go @@ -221,14 +221,17 @@ func (t *ticket) setCookie(rw http.ResponseWriter, req *http.Request, s *session // clearCookie removes any cookies that would be where this ticket // would set them func (t *ticket) clearCookie(rw http.ResponseWriter, req *http.Request) { - http.SetCookie(rw, cookies.MakeCookieFromOptions( - req, - t.options.Name, - "", - t.options, - time.Hour*-1, - t.options.SameSite, - )) + cookieOptions := &cookies.CookieOptions{ + Name: t.options.Name, + Value: "", + Domains: t.options.Domains, + Expiration: time.Hour * -1, + SameSite: t.options.SameSite, + Path: t.options.Path, + HTTPOnly: t.options.HTTPOnly, + Secure: t.options.Secure, + } + http.SetCookie(rw, cookies.MakeCookieFromOptions(req, cookieOptions)) } // makeCookie makes a cookie, signing the value if present @@ -245,14 +248,18 @@ func (t *ticket) makeCookie(req *http.Request, value string, expires time.Durati } } - return cookies.MakeCookieFromOptions( - req, - t.options.Name, - value, - t.options, - expires, - t.options.SameSite, - ), nil + cookieOptions := &cookies.CookieOptions{ + Name: t.options.Name, + Value: value, + Domains: t.options.Domains, + Expiration: expires, + SameSite: t.options.SameSite, + Path: t.options.Path, + HTTPOnly: t.options.HTTPOnly, + Secure: t.options.Secure, + } + + return cookies.MakeCookieFromOptions(req, cookieOptions), nil } // makeCipher makes a AES-GCM cipher out of the ticket's secret diff --git a/pkg/sessions/tests/session_store_tests.go b/pkg/sessions/tests/session_store_tests.go index fe3a324b..dd678042 100644 --- a/pkg/sessions/tests/session_store_tests.go +++ b/pkg/sessions/tests/session_store_tests.go @@ -422,7 +422,17 @@ func SessionStoreInterfaceTests(in *testInput) { broken := "BrokenSessionFromADifferentSessionImplementation" value, err := encryption.SignedValue(in.cookieOpts.Secret, in.cookieOpts.Name, []byte(broken), time.Now()) Expect(err).ToNot(HaveOccurred()) - cookie := cookiesapi.MakeCookieFromOptions(in.request, in.cookieOpts.Name, value, in.cookieOpts, in.cookieOpts.Expire, in.cookieOpts.SameSite) + cookieOptions := &cookiesapi.CookieOptions{ + Name: in.cookieOpts.Name, + Value: value, + Domains: in.cookieOpts.Domains, + Expiration: in.cookieOpts.Expire, + SameSite: in.cookieOpts.SameSite, + Path: in.cookieOpts.Path, + HTTPOnly: in.cookieOpts.HTTPOnly, + Secure: in.cookieOpts.Secure, + } + cookie := cookiesapi.MakeCookieFromOptions(in.request, cookieOptions) in.request.AddCookie(cookie) err = in.ss().Save(in.response, in.request, in.session)